From a317a2531cacf1981ced5b1effe81168e582e0d5 Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Thu, 9 Jul 2020 03:50:43 +0200 Subject: [PATCH] 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= ``` The name / ID of the new key is specified as request query parameter `key-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. --- cmd/admin-handlers.go | 32 ++++++++++++++++++++-- cmd/admin-router.go | 1 + cmd/crypto/kes.go | 50 ++++++++++++---------------------- cmd/crypto/kms.go | 44 ++++++++++++------------------ cmd/crypto/kms_test.go | 9 ------ cmd/crypto/parse_test.go | 4 +-- cmd/crypto/vault.go | 25 +++++++++++++---- cmd/disk-cache-backend.go | 4 +-- cmd/encryption-v1.go | 8 +++--- pkg/iam/policy/admin-action.go | 2 ++ pkg/madmin/kms-commands.go | 22 +++++++++++++++ 11 files changed, 118 insertions(+), 83 deletions(-) diff --git a/cmd/admin-handlers.go b/cmd/admin-handlers.go index d539a0e8f..3bdfb8bda 100644 --- a/cmd/admin-handlers.go +++ b/cmd/admin-handlers.go @@ -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= +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= 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 == "" { diff --git a/cmd/admin-router.go b/cmd/admin-router.go index 0fe823f73..2f599fe03 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -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 { diff --git a/cmd/crypto/kes.go b/cmd/crypto/kes.go index 10f3c0b8d..c977dec96 100644 --- a/cmd/crypto/kes.go +++ b/cmd/crypto/kes.go @@ -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 diff --git a/cmd/crypto/kms.go b/cmd/crypto/kms.go index cc7a097b9..8db9a8dd4 100644 --- a/cmd/crypto/kms.go +++ b/cmd/crypto/kms.go @@ -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{} diff --git a/cmd/crypto/kms_test.go b/cmd/crypto/kms_test.go index b6f86be95..b2e7239ba 100644 --- a/cmd/crypto/kms_test.go +++ b/cmd/crypto/kms_test.go @@ -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) - } - } } diff --git a/cmd/crypto/parse_test.go b/cmd/crypto/parse_test.go index b0aaf6ac4..a6abd2a48 100644 --- a/cmd/crypto/parse_test.go +++ b/cmd/crypto/parse_test.go @@ -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()) } }) } diff --git a/cmd/crypto/vault.go b/cmd/crypto/vault.go index b108f68d5..8674a6179 100644 --- a/cmd/crypto/vault.go +++ b/cmd/crypto/vault.go @@ -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 diff --git a/cmd/disk-cache-backend.go b/cmd/disk-cache-backend.go index 8c85713f4..c12829cba 100644 --- a/cmd/disk-cache-backend.go +++ b/cmd/disk-cache-backend.go @@ -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))) diff --git a/cmd/encryption-v1.go b/cmd/encryption-v1.go index 27ff56a9f..b1ed6e291 100644 --- a/cmd/encryption-v1.go +++ b/cmd/encryption-v1.go @@ -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 diff --git a/pkg/iam/policy/admin-action.go b/pkg/iam/policy/admin-action.go index b293f8435..afcbe1c72 100644 --- a/pkg/iam/policy/admin-action.go +++ b/pkg/iam/policy/admin-action.go @@ -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 diff --git a/pkg/madmin/kms-commands.go b/pkg/madmin/kms-commands.go index 064b4dce8..318683573 100644 --- a/pkg/madmin/kms-commands.go +++ b/pkg/madmin/kms-commands.go @@ -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= + 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.