mirror of
https://github.com/minio/minio.git
synced 2024-12-24 06:05:55 -05:00
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:
parent
ee20ebe07a
commit
a317a2531c
@ -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 == "" {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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{}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user