From 61b83825ccedb5eeb1b39d99c7520b00f4f5f961 Mon Sep 17 00:00:00 2001 From: James Bach Date: Tue, 25 Aug 2020 12:13:36 +0100 Subject: [PATCH] Adds SSE-KMS and SSE-C config to S3 Objstore (#3064) * Adds SSE config to S3 Objstore Signed-off-by: James Bach * Document sse_config block in storage.md Signed-off-by: James Bach * run make docs Signed-off-by: James Bach * Add SSE config options to changelog Signed-off-by: James Bach * Update changelog Signed-off-by: James Bach * Define explicit SSE 'Type' Signed-off-by: James Bach * Update docs again for real Signed-off-by: James Bach * Fix bad logic for SSE-KMS Signed-off-by: James Bach * Adds example policy for KMS documentation Signed-off-by: James Bach * Changes SSE strings into constants Signed-off-by: James Bach * Make SSE error message prettier Signed-off-by: James Bach * Export consant with comments Signed-off-by: James Bach * Group consts in pkg/s3 Signed-off-by: James Bach --- CHANGELOG.md | 11 +++++ docs/storage.md | 38 ++++++++++++++++- pkg/objstore/s3/s3.go | 66 ++++++++++++++++++++++++++--- pkg/objstore/s3/s3_test.go | 85 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f5002132..0c80b412e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ We use *breaking* word for marking changes that are not backward compatible (rel :warning: **WARNING** :warning: Thanos Rule's `/api/v1/rules` endpoint no longer returns the old, deprecated `partial_response_strategy`. The old, deprecated value has been fixed to `WARN` for quite some time. _Please_ use `partialResponseStrategy`. +:warning: **WARNING** :warning: The `sse_encryption` value is now deprecated in favour of `sse_config`. If you used `sse_encryption`, the migration strategy is to set up the following block: + +```yaml + +--- +sse_config: + type: SSE-S3 +``` + + ### Fixed - [#2937](https://github.com/thanos-io/thanos/pull/2937) Receive: Fixing auto-configuration of --receive.local-endpoint @@ -29,6 +39,7 @@ We use *breaking* word for marking changes that are not backward compatible (rel - [#2976](https://github.com/thanos-io/thanos/pull/2976) Query: Better rounding for incoming query timestamps. - [#2929](https://github.com/thanos-io/thanos/pull/2929) Mixin: Fix expression for 'unhealthy sidecar' alert and also increase the timeout for 10 minutes. - [#3024](https://github.com/thanos-io/thanos/pull/3024) Query: consider group name and file for deduplication +- [#3064](https://github.com/thanos-io/thanos/pull/3064) s3: Add SSE/SSE-KMS/SSE-C configuration. ### Added diff --git a/docs/storage.md b/docs/storage.md index 26dac3509b..715fb4212c 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -83,7 +83,6 @@ config: access_key: "" insecure: false signature_version2: false - encrypt_sse: false secret_key: "" put_user_metadata: {} http_config: @@ -93,6 +92,11 @@ config: trace: enable: false part_size: 134217728 + sse_config: + type: "" + kms_key_id: "" + kms_encryption_context: {} + encryption_key: "" ``` At a minimum, you will need to provide a value for the `bucket`, `endpoint`, `access_key`, and `secret_key` keys. The rest of the keys are optional. @@ -115,6 +119,38 @@ For debug and testing purposes you can set * `trace.enable: true` to enable the minio client's verbose logging. Each request and response will be logged into the debug logger, so debug level logging must be enabled for this functionality. +#### S3 Server-Side Encryption + +SSE can be configued using the `sse_config`. [SSE-S3](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html), [SSE-KMS](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html), and [SSE-C](https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html) are supported. + +* If type is set to `SSE-S3` you do not need to configure other options. + +* If type is set to `SSE-KMS` you must set `kms_key_id`. The `kms_encryption_context` is optional, as [AWS provides a default encryption context](https://docs.aws.amazon.com/kms/latest/developerguide/services-s3.html#s3-encryption-context). + +* If type is set to `SSE-C` you must provide a path to the encryption key using `encryption_key`. + +If the SSE Config block is set but the `type` is not one of `SSE-S3`, `SSE-KMS`, or `SSE-C`, an error is raised. + +You will also need to apply the following AWS IAM policy for the user to access the KMS key: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "KMSAccess", + "Effect": "Allow", + "Action": [ + "kms:GenerateDataKey", + "kms:Encrypt", + "kms:Decrypt" + ], + "Resource": "arn:aws:kms:::key/" + } + ] +} +``` + #### Credentials By default Thanos will try to retrieve credentials from the following sources: diff --git a/pkg/objstore/s3/s3.go b/pkg/objstore/s3/s3.go index ad05670d0e..789e629010 100644 --- a/pkg/objstore/s3/s3.go +++ b/pkg/objstore/s3/s3.go @@ -9,6 +9,7 @@ import ( "crypto/tls" "fmt" "io" + "io/ioutil" "net" "net/http" "os" @@ -31,8 +32,19 @@ import ( "gopkg.in/yaml.v2" ) -// DirDelim is the delimiter used to model a directory structure in an object store bucket. -const DirDelim = "/" +const ( + // DirDelim is the delimiter used to model a directory structure in an object store bucket. + DirDelim = "/" + + // SSEKMS is the name of the SSE-KMS method for objectstore encryption. + SSEKMS = "SSE-KMS" + + // SSEC is the name of the SSE-C method for objstore encryption. + SSEC = "SSE-C" + + // SSES3 is the name of the SSE-S3 method for objstore encryption. + SSES3 = "SSE-S3" +) var DefaultConfig = Config{ PutUserMetadata: map[string]string{}, @@ -53,13 +65,22 @@ type Config struct { AccessKey string `yaml:"access_key"` Insecure bool `yaml:"insecure"` SignatureV2 bool `yaml:"signature_version2"` - SSEEncryption bool `yaml:"encrypt_sse"` SecretKey string `yaml:"secret_key"` PutUserMetadata map[string]string `yaml:"put_user_metadata"` HTTPConfig HTTPConfig `yaml:"http_config"` TraceConfig TraceConfig `yaml:"trace"` // PartSize used for multipart upload. Only used if uploaded object size is known and larger than configured PartSize. - PartSize uint64 `yaml:"part_size"` + PartSize uint64 `yaml:"part_size"` + SSEConfig SSEConfig `yaml:"sse_config"` +} + +// SSEConfig deals with the configuration of SSE for Minio. The following options are valid: +// kmsencryptioncontext == https://docs.aws.amazon.com/kms/latest/developerguide/services-s3.html#s3-encryption-context +type SSEConfig struct { + Type string `yaml:"type"` + KMSKeyID string `yaml:"kms_key_id"` + KMSEncryptionContext map[string]string `yaml:"kms_encryption_context"` + EncryptionKey string `yaml:"encryption_key"` } type TraceConfig struct { @@ -173,8 +194,32 @@ func NewBucketWithConfig(logger log.Logger, config Config, component string) (*B client.SetAppInfo(fmt.Sprintf("thanos-%s", component), fmt.Sprintf("%s (%s)", version.Version, runtime.Version())) var sse encrypt.ServerSide - if config.SSEEncryption { - sse = encrypt.NewSSE() + if config.SSEConfig.Type != "" { + switch config.SSEConfig.Type { + case SSEKMS: + sse, err = encrypt.NewSSEKMS(config.SSEConfig.KMSKeyID, config.SSEConfig.KMSEncryptionContext) + if err != nil { + return nil, errors.Wrap(err, "initialize s3 client SSE-KMS") + } + + case SSEC: + key, err := ioutil.ReadFile(config.SSEConfig.EncryptionKey) + if err != nil { + return nil, err + } + + sse, err = encrypt.NewSSEC(key) + if err != nil { + return nil, errors.Wrap(err, "initialize s3 client SSE-C") + } + + case SSES3: + sse = encrypt.NewSSE() + + default: + sseErrMsg := errors.Errorf("Unsupported type %q was provided. Supported types are SSE-S3, SSE-KMS, SSE-C", config.SSEConfig.Type) + return nil, errors.Wrap(sseErrMsg, "Initialize s3 client SSE Config") + } } if config.TraceConfig.Enable { @@ -211,6 +256,15 @@ func validate(conf Config) error { if conf.AccessKey != "" && conf.SecretKey == "" { return errors.New("no s3 secret_key specified while access_key is present in config file; either both should be present in config or envvars/IAM should be used.") } + + if conf.SSEConfig.Type == SSEC && conf.SSEConfig.EncryptionKey == "" { + return errors.New("encryption_key must be set if sse_config.type is set to 'SSE-C'") + } + + if conf.SSEConfig.Type == SSEKMS && conf.SSEConfig.KMSKeyID == "" { + return errors.New("kms_key_id must be set if sse_config.type is set to 'SSE-KMS'") + } + return nil } diff --git a/pkg/objstore/s3/s3_test.go b/pkg/objstore/s3/s3_test.go index 3ef3ea19e2..8ce2595bc0 100644 --- a/pkg/objstore/s3/s3_test.go +++ b/pkg/objstore/s3/s3_test.go @@ -24,6 +24,91 @@ insecure: false`) } } +func TestParseConfig_SSEConfig(t *testing.T) { + input := []byte(`bucket: abdd +endpoint: "s3-endpoint" +sse_config: + type: SSE-S3`) + + cfg, err := parseConfig(input) + testutil.Ok(t, err) + testutil.Ok(t, validate(cfg)) + + input2 := []byte(`bucket: abdd +endpoint: "s3-endpoint" +sse_config: + type: SSE-C`) + + cfg, err = parseConfig(input2) + testutil.Ok(t, err) + testutil.NotOk(t, validate(cfg)) + + input3 := []byte(`bucket: abdd +endpoint: "s3-endpoint" +sse_config: + type: SSE-C + kms_key_id: qweasd`) + + cfg, err = parseConfig(input3) + testutil.Ok(t, err) + testutil.NotOk(t, validate(cfg)) + + input4 := []byte(`bucket: abdd +endpoint: "s3-endpoint" +sse_config: + type: SSE-C + encryption_key: /some/file`) + + cfg, err = parseConfig(input4) + testutil.Ok(t, err) + testutil.Ok(t, validate(cfg)) + + input5 := []byte(`bucket: abdd +endpoint: "s3-endpoint" +sse_config: + type: SSE-KMS`) + + cfg, err = parseConfig(input5) + testutil.Ok(t, err) + testutil.NotOk(t, validate(cfg)) + + input6 := []byte(`bucket: abdd +endpoint: "s3-endpoint" +sse_config: + type: SSE-KMS + kms_key_id: abcd1234-ab12-cd34-1234567890ab`) + + cfg, err = parseConfig(input6) + testutil.Ok(t, err) + testutil.Ok(t, validate(cfg)) + + input7 := []byte(`bucket: abdd +endpoint: "s3-endpoint" +sse_config: + type: SSE-KMS + kms_key_id: abcd1234-ab12-cd34-1234567890ab + kms_encryption_context: + key: value + something: else + a: b`) + + cfg, err = parseConfig(input7) + testutil.Ok(t, err) + testutil.Ok(t, validate(cfg)) + + input8 := []byte(`bucket: abdd +endpoint: "s3-endpoint" +sse_config: + type: SSE-MagicKey + kms_key_id: abcd1234-ab12-cd34-1234567890ab + encryption_key: /some/file`) + + cfg, err = parseConfig(input8) + testutil.Ok(t, err) + // Since the error handling for "proper type" if done as we're setting up the bucket. + testutil.Ok(t, validate(cfg)) +} + func TestParseConfig_DefaultHTTPConfig(t *testing.T) { input := []byte(`bucket: abcd insecure: false`)