From c987313431614f4648357449036900f09bff9eb7 Mon Sep 17 00:00:00 2001 From: Poorna Krishnamoorthy Date: Mon, 21 Dec 2020 16:21:33 -0800 Subject: [PATCH] Encrypt remote target if kms is configured (#11034) Co-authored-by: Poorna Krishnamoorthy --- cmd/admin-bucket-handlers.go | 1 - cmd/bucket-metadata-sys.go | 6 +- cmd/bucket-metadata.go | 143 +++++++++++++++++++++++++++-------- cmd/bucket-metadata_gen.go | 35 +++++++-- cmd/bucket-targets.go | 31 ++++++++ 5 files changed, 178 insertions(+), 38 deletions(-) diff --git a/cmd/admin-bucket-handlers.go b/cmd/admin-bucket-handlers.go index b8f6fd238..ac8ddf574 100644 --- a/cmd/admin-bucket-handlers.go +++ b/cmd/admin-bucket-handlers.go @@ -194,7 +194,6 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http. writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL) return } - if err = globalBucketMetadataSys.Update(bucket, bucketTargetsFile, tgtBytes); err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) return diff --git a/cmd/bucket-metadata-sys.go b/cmd/bucket-metadata-sys.go index 1abb3a392..d2db2c4fa 100644 --- a/cmd/bucket-metadata-sys.go +++ b/cmd/bucket-metadata-sys.go @@ -24,6 +24,7 @@ import ( "sync" "github.com/minio/minio-go/v7/pkg/tags" + "github.com/minio/minio/cmd/crypto" "github.com/minio/minio/cmd/logger" bucketsse "github.com/minio/minio/pkg/bucket/encryption" "github.com/minio/minio/pkg/bucket/lifecycle" @@ -168,7 +169,10 @@ func (sys *BucketMetadataSys) Update(bucket string, configFile string, configDat } meta.ReplicationConfigXML = configData case bucketTargetsFile: - meta.BucketTargetsConfigJSON = configData + meta.BucketTargetsConfigJSON, meta.BucketTargetsConfigMetaJSON, err = encryptBucketMetadata(meta.Name, configData, crypto.Context{bucket: meta.Name, bucketTargetsFile: bucketTargetsFile}) + if err != nil { + return fmt.Errorf("Error encrypting bucket target metadata %w", err) + } default: return fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile) } diff --git a/cmd/bucket-metadata.go b/cmd/bucket-metadata.go index 667ce6bc4..2a8a9adaa 100644 --- a/cmd/bucket-metadata.go +++ b/cmd/bucket-metadata.go @@ -19,6 +19,7 @@ package cmd import ( "bytes" "context" + "crypto/rand" "encoding/binary" "encoding/json" "encoding/xml" @@ -28,6 +29,7 @@ import ( "time" "github.com/minio/minio-go/v7/pkg/tags" + "github.com/minio/minio/cmd/crypto" "github.com/minio/minio/cmd/logger" bucketsse "github.com/minio/minio/pkg/bucket/encryption" "github.com/minio/minio/pkg/bucket/lifecycle" @@ -37,6 +39,7 @@ import ( "github.com/minio/minio/pkg/bucket/versioning" "github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/madmin" + "github.com/minio/sio" ) const ( @@ -61,31 +64,33 @@ var ( // bucketMetadataFormat refers to the format. // bucketMetadataVersion can be used to track a rolling upgrade of a field. type BucketMetadata struct { - Name string - Created time.Time - LockEnabled bool // legacy not used anymore. - PolicyConfigJSON []byte - NotificationConfigXML []byte - LifecycleConfigXML []byte - ObjectLockConfigXML []byte - VersioningConfigXML []byte - EncryptionConfigXML []byte - TaggingConfigXML []byte - QuotaConfigJSON []byte - ReplicationConfigXML []byte - BucketTargetsConfigJSON []byte + Name string + Created time.Time + LockEnabled bool // legacy not used anymore. + PolicyConfigJSON []byte + NotificationConfigXML []byte + LifecycleConfigXML []byte + ObjectLockConfigXML []byte + VersioningConfigXML []byte + EncryptionConfigXML []byte + TaggingConfigXML []byte + QuotaConfigJSON []byte + ReplicationConfigXML []byte + BucketTargetsConfigJSON []byte + BucketTargetsConfigMetaJSON []byte // Unexported fields. Must be updated atomically. - policyConfig *policy.Policy - notificationConfig *event.Config - lifecycleConfig *lifecycle.Lifecycle - objectLockConfig *objectlock.Config - versioningConfig *versioning.Versioning - sseConfig *bucketsse.BucketSSEConfig - taggingConfig *tags.Tags - quotaConfig *madmin.BucketQuota - replicationConfig *replication.Config - bucketTargetConfig *madmin.BucketTargets + policyConfig *policy.Policy + notificationConfig *event.Config + lifecycleConfig *lifecycle.Lifecycle + objectLockConfig *objectlock.Config + versioningConfig *versioning.Versioning + sseConfig *bucketsse.BucketSSEConfig + taggingConfig *tags.Tags + quotaConfig *madmin.BucketQuota + replicationConfig *replication.Config + bucketTargetConfig *madmin.BucketTargets + bucketTargetConfigMeta map[string]string } // newBucketMetadata creates BucketMetadata with the supplied name and Created to Now. @@ -100,7 +105,8 @@ func newBucketMetadata(name string) BucketMetadata { versioningConfig: &versioning.Versioning{ XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/", }, - bucketTargetConfig: &madmin.BucketTargets{}, + bucketTargetConfig: &madmin.BucketTargets{}, + bucketTargetConfigMeta: make(map[string]string), } } @@ -140,16 +146,16 @@ func (b *BucketMetadata) Load(ctx context.Context, api ObjectLayer, name string) func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (BucketMetadata, error) { b := newBucketMetadata(bucket) err := b.Load(ctx, objectAPI, b.Name) - if err == nil { - return b, b.convertLegacyConfigs(ctx, objectAPI) - } - - if !errors.Is(err, errConfigNotFound) { + if err != nil && !errors.Is(err, errConfigNotFound) { return b, err } // Old bucket without bucket metadata. Hence we migrate existing settings. - return b, b.convertLegacyConfigs(ctx, objectAPI) + if err := b.convertLegacyConfigs(ctx, objectAPI); err != nil { + return b, err + } + // migrate unencrypted remote targets + return b, b.migrateTargetConfig(ctx, objectAPI) } // parseAllConfigs will parse all configs and populate the private fields. @@ -234,7 +240,8 @@ func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLa } if len(b.BucketTargetsConfigJSON) != 0 { - if err = json.Unmarshal(b.BucketTargetsConfigJSON, b.bucketTargetConfig); err != nil { + b.bucketTargetConfig, err = parseBucketTargetConfig(b.Name, b.BucketTargetsConfigJSON, b.BucketTargetsConfigMetaJSON) + if err != nil { return err } } else { @@ -354,6 +361,7 @@ func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error { if err != nil { return err } + configFile := path.Join(bucketConfigPrefix, b.Name, bucketMetadataFile) return saveConfig(ctx, api, configFile, data) } @@ -373,3 +381,76 @@ func deleteBucketMetadata(ctx context.Context, obj objectDeleter, bucket string) } return nil } + +// migrate config for remote targets by encrypting data if currently unencrypted and kms is configured. +func (b *BucketMetadata) migrateTargetConfig(ctx context.Context, objectAPI ObjectLayer) error { + var err error + // early return if no targets or already encrypted + if len(b.BucketTargetsConfigJSON) == 0 || GlobalKMS == nil || len(b.BucketTargetsConfigMetaJSON) != 0 { + return nil + } + + encBytes, metaBytes, err := encryptBucketMetadata(b.Name, b.BucketTargetsConfigJSON, crypto.Context{b.Name: b.Name, bucketTargetsFile: bucketTargetsFile}) + if err != nil { + return err + } + + b.BucketTargetsConfigJSON = encBytes + b.BucketTargetsConfigMetaJSON = metaBytes + return b.Save(ctx, objectAPI) +} + +// encrypt bucket metadata if kms is configured. +func encryptBucketMetadata(bucket string, input []byte, kmsContext crypto.Context) (output, metabytes []byte, err error) { + var sealedKey crypto.SealedKey + if GlobalKMS == nil { + output = input + return + } + var ( + key [32]byte + encKey []byte + ) + metadata := make(map[string]string) + key, encKey, err = GlobalKMS.GenerateKey(GlobalKMS.DefaultKeyID(), kmsContext) + if err != nil { + return + } + outbuf := bytes.NewBuffer(nil) + objectKey := crypto.GenerateKey(key, rand.Reader) + sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, "") + crypto.S3.CreateMetadata(metadata, GlobalKMS.DefaultKeyID(), encKey, sealedKey) + _, err = sio.Encrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20}) + if err != nil { + return output, metabytes, err + } + metabytes, err = json.Marshal(metadata) + if err != nil { + return + } + return outbuf.Bytes(), metabytes, nil +} + +// decrypt bucket metadata if kms is configured. +func decryptBucketMetadata(input []byte, bucket string, meta map[string]string, kmsContext crypto.Context) ([]byte, error) { + if GlobalKMS == nil { + return nil, errKMSNotConfigured + } + keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(meta) + + if err != nil { + return nil, err + } + extKey, err := GlobalKMS.UnsealKey(keyID, kmsKey, kmsContext) + if err != nil { + return nil, err + } + var objectKey crypto.ObjectKey + if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), bucket, ""); err != nil { + return nil, err + } + outbuf := bytes.NewBuffer(nil) + _, err = sio.Decrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20}) + + return outbuf.Bytes(), err +} diff --git a/cmd/bucket-metadata_gen.go b/cmd/bucket-metadata_gen.go index 3457f2454..fd869aa18 100644 --- a/cmd/bucket-metadata_gen.go +++ b/cmd/bucket-metadata_gen.go @@ -102,6 +102,12 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) { err = msgp.WrapError(err, "BucketTargetsConfigJSON") return } + case "BucketTargetsConfigMetaJSON": + z.BucketTargetsConfigMetaJSON, err = dc.ReadBytes(z.BucketTargetsConfigMetaJSON) + if err != nil { + err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON") + return + } default: err = dc.Skip() if err != nil { @@ -115,9 +121,9 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) { // EncodeMsg implements msgp.Encodable func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) { - // map header, size 13 + // map header, size 14 // write "Name" - err = en.Append(0x8d, 0xa4, 0x4e, 0x61, 0x6d, 0x65) + err = en.Append(0x8e, 0xa4, 0x4e, 0x61, 0x6d, 0x65) if err != nil { return } @@ -246,15 +252,25 @@ func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) { err = msgp.WrapError(err, "BucketTargetsConfigJSON") return } + // write "BucketTargetsConfigMetaJSON" + err = en.Append(0xbb, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e) + if err != nil { + return + } + err = en.WriteBytes(z.BucketTargetsConfigMetaJSON) + if err != nil { + err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON") + return + } return } // MarshalMsg implements msgp.Marshaler func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) - // map header, size 13 + // map header, size 14 // string "Name" - o = append(o, 0x8d, 0xa4, 0x4e, 0x61, 0x6d, 0x65) + o = append(o, 0x8e, 0xa4, 0x4e, 0x61, 0x6d, 0x65) o = msgp.AppendString(o, z.Name) // string "Created" o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64) @@ -292,6 +308,9 @@ func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) { // string "BucketTargetsConfigJSON" o = append(o, 0xb7, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e) o = msgp.AppendBytes(o, z.BucketTargetsConfigJSON) + // string "BucketTargetsConfigMetaJSON" + o = append(o, 0xbb, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e) + o = msgp.AppendBytes(o, z.BucketTargetsConfigMetaJSON) return } @@ -391,6 +410,12 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "BucketTargetsConfigJSON") return } + case "BucketTargetsConfigMetaJSON": + z.BucketTargetsConfigMetaJSON, bts, err = msgp.ReadBytesBytes(bts, z.BucketTargetsConfigMetaJSON) + if err != nil { + err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON") + return + } default: bts, err = msgp.Skip(bts) if err != nil { @@ -405,6 +430,6 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *BucketMetadata) Msgsize() (s int) { - s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) return } diff --git a/cmd/bucket-targets.go b/cmd/bucket-targets.go index c690db897..d2380fdf9 100644 --- a/cmd/bucket-targets.go +++ b/cmd/bucket-targets.go @@ -19,6 +19,7 @@ package cmd import ( "context" "encoding/hex" + "encoding/json" "net/http" "strings" "sync" @@ -27,6 +28,7 @@ import ( minio "github.com/minio/minio-go/v7" miniogo "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/minio/minio/cmd/crypto" "github.com/minio/minio/pkg/bucket/versioning" "github.com/minio/minio/pkg/madmin" sha256 "github.com/minio/sha256-simd" @@ -391,3 +393,32 @@ func generateARN(t *madmin.BucketTarget) string { } return arn.String() } + +// Returns parsed target config. If KMS is configured, remote target is decrypted +func parseBucketTargetConfig(bucket string, cdata, cmetadata []byte) (*madmin.BucketTargets, error) { + var ( + data []byte + err error + t madmin.BucketTargets + meta map[string]string + ) + if len(cdata) == 0 { + return nil, nil + } + data = cdata + if len(cmetadata) != 0 { + if err := json.Unmarshal(cmetadata, &meta); err != nil { + return nil, err + } + if crypto.S3.IsEncrypted(meta) { + if data, err = decryptBucketMetadata(cdata, bucket, meta, crypto.Context{bucket: bucket, bucketTargetsFile: bucketTargetsFile}); err != nil { + return nil, err + } + } + } + + if err = json.Unmarshal(data, &t); err != nil { + return nil, err + } + return &t, nil +}