admin: new API for creating KMS master keys (#9982)

This commit adds a new admin API for creating master keys.
An admin client can send a POST request to:
```
/minio/admin/v3/kms/key/create?key-id=<keyID>
```

The name / ID of the new key is specified as request
query parameter `key-id=<ID>`.

Creating new master keys requires KES - it does not work with
the native Vault KMS (deprecated) nor with a static master key
(deprecated).

Further, this commit removes the `UpdateKey` method from the `KMS`
interface. This method is not needed and not used anymore.
This commit is contained in:
Andreas Auernhammer 2020-07-09 03:50:43 +02:00 committed by GitHub
parent ee20ebe07a
commit a317a2531c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 118 additions and 83 deletions

View File

@ -938,6 +938,12 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
Description: err.Error(),
HTTPStatusCode: http.StatusServiceUnavailable,
}
case errors.Is(err, crypto.ErrKESKeyExists):
apiErr = APIError{
Code: "XMinioKMSKeyExists",
Description: err.Error(),
HTTPStatusCode: http.StatusConflict,
}
default:
apiErr = errorCodes.ToAPIErrWithErr(toAdminAPIErrCode(ctx, err), err)
}
@ -1090,6 +1096,28 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
}
}
// KMSCreateKeyHandler - POST /minio/admin/v3/kms/key/create?key-id=<master-key-id>
func (a adminAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "KMSCreateKey")
defer logger.AuditLog(w, r, "KMSCreateKey", mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSCreateKeyAdminAction)
if objectAPI == nil {
return
}
if GlobalKMS == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
return
}
if err := GlobalKMS.CreateKey(r.URL.Query().Get("key-id")); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
writeSuccessResponseHeadersOnly(w)
}
// KMSKeyStatusHandler - GET /minio/admin/v3/kms/key/status?key-id=<master-key-id>
func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "KMSKeyStatus")
@ -1108,7 +1136,7 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
keyID := r.URL.Query().Get("key-id")
if keyID == "" {
keyID = GlobalKMS.KeyID()
keyID = GlobalKMS.DefaultKeyID()
}
var response = madmin.KMSKeyStatus{
KeyID: keyID,
@ -1541,7 +1569,7 @@ func fetchVaultStatus(cfg config.Config) madmin.Vault {
vault.Status = "disabled"
return vault
}
keyID := GlobalKMS.KeyID()
keyID := GlobalKMS.DefaultKeyID()
kmsInfo := GlobalKMS.Info()
if kmsInfo.Endpoint == "" {

View File

@ -197,6 +197,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
// -- KMS APIs --
//
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/kms/key/create").HandlerFunc(httpTraceAll(adminAPI.KMSCreateKeyHandler)).Queries("key-id", "{key-id:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/kms/key/status").HandlerFunc(httpTraceAll(adminAPI.KMSKeyStatusHandler))
if !globalIsGateway {

View File

@ -34,9 +34,9 @@ import (
xnet "github.com/minio/minio/pkg/net"
)
// ErrKESKeyNotFound is the error returned a KES server
// when a master key does not exist.
var ErrKESKeyNotFound = NewKESError(http.StatusNotFound, "key does not exist")
// ErrKESKeyExists is the error returned a KES server
// when a master key does exist.
var ErrKESKeyExists = NewKESError(http.StatusBadRequest, "key does already exist")
// KesConfig contains the configuration required
// to initialize and connect to a kes server.
@ -134,20 +134,27 @@ func NewKes(cfg KesConfig) (KMS, error) {
}, nil
}
// KeyID returns the default key ID.
func (kes *kesService) KeyID() string {
// DefaultKeyID returns the default key ID that should be
// used for SSE-S3 or SSE-KMS when the S3 client does not
// provide an explicit key ID.
func (kes *kesService) DefaultKeyID() string {
return kes.defaultKeyID
}
// Info returns some status information about the KMS.
// Info returns some information about the KES,
// configuration - like the endpoint or authentication
// method.
func (kes *kesService) Info() KMSInfo {
return KMSInfo{
Endpoint: kes.endpoint,
Name: kes.KeyID(),
Name: kes.DefaultKeyID(),
AuthType: "TLS",
}
}
// CreateKey tries to create a new master key with the given keyID.
func (kes *kesService) CreateKey(keyID string) error { return kes.client.CreateKey(keyID) }
// GenerateKey returns a new plaintext key, generated by the KMS,
// and a sealed version of this plaintext key encrypted using the
// named key referenced by keyID. It also binds the generated key
@ -158,12 +165,6 @@ func (kes *kesService) GenerateKey(keyID string, ctx Context) (key [32]byte, sea
var plainKey []byte
plainKey, sealedKey, err = kes.client.GenerateDataKey(keyID, context.Bytes())
if err == ErrKESKeyNotFound { // Try to create the key if it does not exist.
if err = kes.client.CreateKey(keyID); err != nil {
return key, nil, err
}
plainKey, sealedKey, err = kes.client.GenerateDataKey(keyID, context.Bytes())
}
if err != nil {
return key, nil, err
}
@ -197,28 +198,11 @@ func (kes *kesService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (k
return key, nil
}
// UpdateKey re-wraps the sealedKey if the master key referenced by the keyID
// has been changed by the KMS operator - i.e. the master key has been rotated.
// If the master key hasn't changed since the sealedKey has been created / updated
// it may return the same sealedKey as rotatedKey.
//
// The context must be same context as the one provided while
// generating the plaintext key / sealedKey.
func (kes *kesService) UpdateKey(keyID string, sealedKey []byte, ctx Context) ([]byte, error) {
_, err := kes.UnsealKey(keyID, sealedKey, ctx)
if err != nil {
return nil, err
}
// Currently a kes server does not support key rotation (of the same key)
// Therefore, we simply return the same sealedKey.
return sealedKey, nil
}
// kesClient implements the bare minimum functionality needed for
// MinIO to talk to a KES server. In particular, it implements
// GenerateDataKey (API: /v1/key/generate/) and
// DecryptDataKey (API: /v1/key/decrypt/).
// • CreateKey (API: /v1/key/create/)
// • GenerateDataKey (API: /v1/key/generate/)
// • DecryptDataKey (API: /v1/key/decrypt/)
type kesClient struct {
addr string
httpClient http.Client

View File

@ -72,8 +72,14 @@ func (c Context) WriteTo(w io.Writer) (n int64, err error) {
// data key generation and unsealing of KMS-generated
// data keys.
type KMS interface {
// KeyID - returns configured KMS key id.
KeyID() string
// DefaultKeyID returns the default master key ID. It should be
// used for SSE-S3 and whenever a S3 client requests SSE-KMS but
// does not specify an explicit SSE-KMS key ID.
DefaultKeyID() string
// CreateKey creates a new master key with the given key ID
// at the KMS.
CreateKey(keyID string) error
// GenerateKey generates a new random data key using
// the master key referenced by the keyID. It returns
@ -90,21 +96,9 @@ type KMS interface {
// match the context used to generate the sealed key.
UnsealKey(keyID string, sealedKey []byte, context Context) (key [32]byte, err error)
// UpdateKey re-wraps the sealedKey if the master key, referenced by
// `keyID`, has changed in the meantime. This usually happens when the
// KMS operator performs a key-rotation operation of the master key.
// UpdateKey fails if the provided sealedKey cannot be decrypted using
// the master key referenced by keyID.
//
// UpdateKey makes no guarantees whatsoever about whether the returned
// rotatedKey is actually different from the sealedKey. If nothing has
// changed at the KMS or if the KMS does not support updating generated
// keys this method may behave like a NOP and just return the sealedKey
// itself.
UpdateKey(keyID string, sealedKey []byte, context Context) (rotatedKey []byte, err error)
// Returns KMSInfo
Info() (kmsInfo KMSInfo)
// Info returns descriptive information about the KMS,
// like the default key ID and authentication method.
Info() KMSInfo
}
type masterKeyKMS struct {
@ -112,7 +106,8 @@ type masterKeyKMS struct {
masterKey [32]byte
}
// KMSInfo stores the details of KMS
// KMSInfo contains some describing information about
// the KMS.
type KMSInfo struct {
Endpoint string
Name string
@ -125,10 +120,14 @@ type KMSInfo struct {
// to the generated keys.
func NewMasterKey(keyID string, key [32]byte) KMS { return &masterKeyKMS{keyID: keyID, masterKey: key} }
func (kms *masterKeyKMS) KeyID() string {
func (kms *masterKeyKMS) DefaultKeyID() string {
return kms.keyID
}
func (kms *masterKeyKMS) CreateKey(keyID string) error {
return errors.New("crypto: creating keys is not supported by a static master key")
}
func (kms *masterKeyKMS) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
if _, err = io.ReadFull(rand.Reader, key[:]); err != nil {
logger.CriticalIf(context.Background(), errOutOfEntropy)
@ -166,13 +165,6 @@ func (kms *masterKeyKMS) UnsealKey(keyID string, sealedKey []byte, ctx Context)
return key, nil
}
func (kms *masterKeyKMS) UpdateKey(keyID string, sealedKey []byte, ctx Context) ([]byte, error) {
if _, err := kms.UnsealKey(keyID, sealedKey, ctx); err != nil {
return nil, err
}
return sealedKey, nil // The master key cannot update data keys -> Do nothing.
}
func (kms *masterKeyKMS) deriveKey(keyID string, context Context) (key [32]byte) {
if context == nil {
context = Context{}

View File

@ -57,15 +57,6 @@ func TestMasterKeyKMS(t *testing.T) {
if !test.ShouldFail && !bytes.Equal(key[:], unsealedKey[:]) {
t.Errorf("Test %d: The generated and unsealed key differ", i)
}
rotatedKey, err := kms.UpdateKey(test.UnsealKeyID, sealedKey, test.UnsealContext)
if err == nil && test.ShouldFail {
t.Errorf("Test %d: KMS updated the generated key successfully but should have failed", i)
}
if !test.ShouldFail && !bytes.Equal(rotatedKey, sealedKey[:]) {
t.Errorf("Test %d: The updated and sealed key differ", i)
}
}
}

View File

@ -51,8 +51,8 @@ func TestParseMasterKey(t *testing.T) {
if !tt.success && err == nil {
t.Error("Unexpected failure")
}
if err == nil && kms.KeyID() != tt.expectedKeyID {
t.Errorf("Expected keyID %s, got %s", tt.expectedKeyID, kms.KeyID())
if err == nil && kms.DefaultKeyID() != tt.expectedKeyID {
t.Errorf("Expected keyID %s, got %s", tt.expectedKeyID, kms.DefaultKeyID())
}
})
}

View File

@ -17,6 +17,7 @@ package crypto
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"strings"
"time"
@ -190,20 +191,34 @@ func (v *vaultService) authenticate() (err error) {
return
}
// KeyID - vault configured keyID
func (v *vaultService) KeyID() string {
// DefaultKeyID returns the default key ID that should be
// used for SSE-S3 or SSE-KMS when the S3 client does not
// provide an explicit key ID.
func (v *vaultService) DefaultKeyID() string {
return v.config.Key.Name
}
// Returns - vault info
func (v *vaultService) Info() (kmsInfo KMSInfo) {
// Info returns some information about the Vault,
// configuration - like the endpoint or authentication
// method.
func (v *vaultService) Info() KMSInfo {
return KMSInfo{
Endpoint: v.config.Endpoint,
Name: v.config.Key.Name,
Name: v.DefaultKeyID(),
AuthType: v.config.Auth.Type,
}
}
// CreateKey is a stub that exists such that the Vault
// client implements the KMS interface. It always returns
// a not-implemented error.
//
// Creating keys requires a KES instance between MinIO and Vault.
func (v *vaultService) CreateKey(keyID string) error {
// Creating new keys requires KES.
return errors.New("crypto: creating keys is not supported by Vault")
}
// GenerateKey returns a new plaintext key, generated by the KMS,
// and a sealed version of this plaintext key encrypted using the
// named key referenced by keyID. It also binds the generated key

View File

@ -640,14 +640,14 @@ func newCacheEncryptMetadata(bucket, object string, metadata map[string]string)
if globalCacheKMS == nil {
return nil, errKMSNotConfigured
}
key, encKey, err := globalCacheKMS.GenerateKey(globalCacheKMS.KeyID(), crypto.Context{bucket: pathJoin(bucket, object)})
key, encKey, err := globalCacheKMS.GenerateKey(globalCacheKMS.DefaultKeyID(), crypto.Context{bucket: pathJoin(bucket, object)})
if err != nil {
return nil, err
}
objectKey := crypto.GenerateKey(key, rand.Reader)
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
crypto.S3.CreateMetadata(metadata, globalCacheKMS.KeyID(), encKey, sealedKey)
crypto.S3.CreateMetadata(metadata, globalCacheKMS.DefaultKeyID(), encKey, sealedKey)
if etag, ok := metadata["etag"]; ok {
metadata["etag"] = hex.EncodeToString(objectKey.SealETag([]byte(etag)))

View File

@ -157,12 +157,12 @@ func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map
return err
}
newKey, encKey, err := GlobalKMS.GenerateKey(GlobalKMS.KeyID(), crypto.Context{bucket: path.Join(bucket, object)})
newKey, encKey, err := GlobalKMS.GenerateKey(GlobalKMS.DefaultKeyID(), crypto.Context{bucket: path.Join(bucket, object)})
if err != nil {
return err
}
sealedKey = objectKey.Seal(newKey, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
crypto.S3.CreateMetadata(metadata, GlobalKMS.KeyID(), encKey, sealedKey)
crypto.S3.CreateMetadata(metadata, GlobalKMS.DefaultKeyID(), encKey, sealedKey)
return nil
}
}
@ -173,14 +173,14 @@ func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]s
if GlobalKMS == nil {
return crypto.ObjectKey{}, errKMSNotConfigured
}
key, encKey, err := GlobalKMS.GenerateKey(GlobalKMS.KeyID(), crypto.Context{bucket: path.Join(bucket, object)})
key, encKey, err := GlobalKMS.GenerateKey(GlobalKMS.DefaultKeyID(), crypto.Context{bucket: path.Join(bucket, object)})
if err != nil {
return crypto.ObjectKey{}, err
}
objectKey := crypto.GenerateKey(key, rand.Reader)
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
crypto.S3.CreateMetadata(metadata, GlobalKMS.KeyID(), encKey, sealedKey)
crypto.S3.CreateMetadata(metadata, GlobalKMS.DefaultKeyID(), encKey, sealedKey)
return objectKey, nil
}
var extKey [32]byte

View File

@ -41,6 +41,8 @@ const (
TraceAdminAction = "admin:ServerTrace"
// ConsoleLogAdminAction - allow listing console logs on terminal
ConsoleLogAdminAction = "admin:ConsoleLog"
// KMSCreateKeyAdminAction - allow creating a new KMS master key
KMSCreateKeyAdminAction = "admin:KMSCreateKey"
// KMSKeyStatusAdminAction - allow getting KMS key status
KMSKeyStatusAdminAction = "admin:KMSKeyStatus"
// ServerInfoAdminAction - allow listing server info

View File

@ -23,6 +23,28 @@ import (
"net/url"
)
// CreateKey tries to create a new master key with the given keyID
// at the KMS connected to a MinIO server.
func (adm *AdminClient) CreateKey(ctx context.Context, keyID string) error {
// POST /minio/admin/v3/kms/key/create?key-id=<keyID>
qv := url.Values{}
qv.Set("key-id", keyID)
reqData := requestData{
relPath: adminAPIPrefix + "/kms/key/create",
queryValues: qv,
}
resp, err := adm.executeMethod(ctx, http.MethodPost, reqData)
if err != nil {
return err
}
defer closeResponse(resp)
if resp.StatusCode != http.StatusOK {
return httpRespToErrorResponse(resp)
}
return nil
}
// GetKeyStatus requests status information about the key referenced by keyID
// from the KMS connected to a MinIO by performing a Admin-API request.
// It basically hits the `/minio/admin/v3/kms/key/status` API endpoint.