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(), Description: err.Error(),
HTTPStatusCode: http.StatusServiceUnavailable, HTTPStatusCode: http.StatusServiceUnavailable,
} }
case errors.Is(err, crypto.ErrKESKeyExists):
apiErr = APIError{
Code: "XMinioKMSKeyExists",
Description: err.Error(),
HTTPStatusCode: http.StatusConflict,
}
default: default:
apiErr = errorCodes.ToAPIErrWithErr(toAdminAPIErrCode(ctx, err), err) 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> // KMSKeyStatusHandler - GET /minio/admin/v3/kms/key/status?key-id=<master-key-id>
func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "KMSKeyStatus") 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") keyID := r.URL.Query().Get("key-id")
if keyID == "" { if keyID == "" {
keyID = GlobalKMS.KeyID() keyID = GlobalKMS.DefaultKeyID()
} }
var response = madmin.KMSKeyStatus{ var response = madmin.KMSKeyStatus{
KeyID: keyID, KeyID: keyID,
@ -1541,7 +1569,7 @@ func fetchVaultStatus(cfg config.Config) madmin.Vault {
vault.Status = "disabled" vault.Status = "disabled"
return vault return vault
} }
keyID := GlobalKMS.KeyID() keyID := GlobalKMS.DefaultKeyID()
kmsInfo := GlobalKMS.Info() kmsInfo := GlobalKMS.Info()
if kmsInfo.Endpoint == "" { if kmsInfo.Endpoint == "" {

View File

@ -197,6 +197,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
// -- KMS APIs -- // -- 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)) adminRouter.Methods(http.MethodGet).Path(adminVersion + "/kms/key/status").HandlerFunc(httpTraceAll(adminAPI.KMSKeyStatusHandler))
if !globalIsGateway { if !globalIsGateway {

View File

@ -34,9 +34,9 @@ import (
xnet "github.com/minio/minio/pkg/net" xnet "github.com/minio/minio/pkg/net"
) )
// ErrKESKeyNotFound is the error returned a KES server // ErrKESKeyExists is the error returned a KES server
// when a master key does not exist. // when a master key does exist.
var ErrKESKeyNotFound = NewKESError(http.StatusNotFound, "key does not exist") var ErrKESKeyExists = NewKESError(http.StatusBadRequest, "key does already exist")
// KesConfig contains the configuration required // KesConfig contains the configuration required
// to initialize and connect to a kes server. // to initialize and connect to a kes server.
@ -134,20 +134,27 @@ func NewKes(cfg KesConfig) (KMS, error) {
}, nil }, nil
} }
// KeyID returns the default key ID. // DefaultKeyID returns the default key ID that should be
func (kes *kesService) KeyID() string { // 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 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 { func (kes *kesService) Info() KMSInfo {
return KMSInfo{ return KMSInfo{
Endpoint: kes.endpoint, Endpoint: kes.endpoint,
Name: kes.KeyID(), Name: kes.DefaultKeyID(),
AuthType: "TLS", 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, // GenerateKey returns a new plaintext key, generated by the KMS,
// and a sealed version of this plaintext key encrypted using the // and a sealed version of this plaintext key encrypted using the
// named key referenced by keyID. It also binds the generated key // 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 var plainKey []byte
plainKey, sealedKey, err = kes.client.GenerateDataKey(keyID, context.Bytes()) 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 { if err != nil {
return key, nil, err return key, nil, err
} }
@ -197,28 +198,11 @@ func (kes *kesService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (k
return key, nil 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 // kesClient implements the bare minimum functionality needed for
// MinIO to talk to a KES server. In particular, it implements // MinIO to talk to a KES server. In particular, it implements
// GenerateDataKey (API: /v1/key/generate/) and // • CreateKey (API: /v1/key/create/)
// DecryptDataKey (API: /v1/key/decrypt/). // • GenerateDataKey (API: /v1/key/generate/)
// • DecryptDataKey (API: /v1/key/decrypt/)
type kesClient struct { type kesClient struct {
addr string addr string
httpClient http.Client 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 key generation and unsealing of KMS-generated
// data keys. // data keys.
type KMS interface { type KMS interface {
// KeyID - returns configured KMS key id. // DefaultKeyID returns the default master key ID. It should be
KeyID() string // 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 // GenerateKey generates a new random data key using
// the master key referenced by the keyID. It returns // 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. // match the context used to generate the sealed key.
UnsealKey(keyID string, sealedKey []byte, context Context) (key [32]byte, err error) UnsealKey(keyID string, sealedKey []byte, context Context) (key [32]byte, err error)
// UpdateKey re-wraps the sealedKey if the master key, referenced by // Info returns descriptive information about the KMS,
// `keyID`, has changed in the meantime. This usually happens when the // like the default key ID and authentication method.
// KMS operator performs a key-rotation operation of the master key. Info() KMSInfo
// 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)
} }
type masterKeyKMS struct { type masterKeyKMS struct {
@ -112,7 +106,8 @@ type masterKeyKMS struct {
masterKey [32]byte masterKey [32]byte
} }
// KMSInfo stores the details of KMS // KMSInfo contains some describing information about
// the KMS.
type KMSInfo struct { type KMSInfo struct {
Endpoint string Endpoint string
Name string Name string
@ -125,10 +120,14 @@ type KMSInfo struct {
// to the generated keys. // to the generated keys.
func NewMasterKey(keyID string, key [32]byte) KMS { return &masterKeyKMS{keyID: keyID, masterKey: key} } 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 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) { func (kms *masterKeyKMS) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
if _, err = io.ReadFull(rand.Reader, key[:]); err != nil { if _, err = io.ReadFull(rand.Reader, key[:]); err != nil {
logger.CriticalIf(context.Background(), errOutOfEntropy) logger.CriticalIf(context.Background(), errOutOfEntropy)
@ -166,13 +165,6 @@ func (kms *masterKeyKMS) UnsealKey(keyID string, sealedKey []byte, ctx Context)
return key, nil 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) { func (kms *masterKeyKMS) deriveKey(keyID string, context Context) (key [32]byte) {
if context == nil { if context == nil {
context = Context{} context = Context{}

View File

@ -57,15 +57,6 @@ func TestMasterKeyKMS(t *testing.T) {
if !test.ShouldFail && !bytes.Equal(key[:], unsealedKey[:]) { if !test.ShouldFail && !bytes.Equal(key[:], unsealedKey[:]) {
t.Errorf("Test %d: The generated and unsealed key differ", i) 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 { if !tt.success && err == nil {
t.Error("Unexpected failure") t.Error("Unexpected failure")
} }
if err == nil && kms.KeyID() != tt.expectedKeyID { if err == nil && kms.DefaultKeyID() != tt.expectedKeyID {
t.Errorf("Expected keyID %s, got %s", tt.expectedKeyID, kms.KeyID()) t.Errorf("Expected keyID %s, got %s", tt.expectedKeyID, kms.DefaultKeyID())
} }
}) })
} }

View File

@ -17,6 +17,7 @@ package crypto
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -190,20 +191,34 @@ func (v *vaultService) authenticate() (err error) {
return return
} }
// KeyID - vault configured keyID // DefaultKeyID returns the default key ID that should be
func (v *vaultService) KeyID() string { // 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 return v.config.Key.Name
} }
// Returns - vault info // Info returns some information about the Vault,
func (v *vaultService) Info() (kmsInfo KMSInfo) { // configuration - like the endpoint or authentication
// method.
func (v *vaultService) Info() KMSInfo {
return KMSInfo{ return KMSInfo{
Endpoint: v.config.Endpoint, Endpoint: v.config.Endpoint,
Name: v.config.Key.Name, Name: v.DefaultKeyID(),
AuthType: v.config.Auth.Type, 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, // GenerateKey returns a new plaintext key, generated by the KMS,
// and a sealed version of this plaintext key encrypted using the // and a sealed version of this plaintext key encrypted using the
// named key referenced by keyID. It also binds the generated key // 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 { if globalCacheKMS == nil {
return nil, errKMSNotConfigured 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 { if err != nil {
return nil, err return nil, err
} }
objectKey := crypto.GenerateKey(key, rand.Reader) objectKey := crypto.GenerateKey(key, rand.Reader)
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object) 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 { if etag, ok := metadata["etag"]; ok {
metadata["etag"] = hex.EncodeToString(objectKey.SealETag([]byte(etag))) 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 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 { if err != nil {
return err return err
} }
sealedKey = objectKey.Seal(newKey, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object) 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 return nil
} }
} }
@ -173,14 +173,14 @@ func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]s
if GlobalKMS == nil { if GlobalKMS == nil {
return crypto.ObjectKey{}, errKMSNotConfigured 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 { if err != nil {
return crypto.ObjectKey{}, err return crypto.ObjectKey{}, err
} }
objectKey := crypto.GenerateKey(key, rand.Reader) objectKey := crypto.GenerateKey(key, rand.Reader)
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object) 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 return objectKey, nil
} }
var extKey [32]byte var extKey [32]byte

View File

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

View File

@ -23,6 +23,28 @@ import (
"net/url" "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 // GetKeyStatus requests status information about the key referenced by keyID
// from the KMS connected to a MinIO by performing a Admin-API request. // 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. // It basically hits the `/minio/admin/v3/kms/key/status` API endpoint.