Skip to content

Adds support to S3 server side encryption using AWS KMS #3651

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jan 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## master / unreleased

* [CHANGE] Ingester: don't update internal "last updated" timestamp of TSDB if tenant only sends invalid samples. This affects how "idle" time is computed. #3727
* [FEATURE] Adds support to S3 server side encryption using KMS. Deprecated `-<prefix>.s3.sse-encryption`, you should use the following CLI flags that have been added. #3651
- `-<prefix>.s3.sse.type`
- `-<prefix>.s3.sse.kms-key-id`
- `-<prefix>.s3.sse.kms-encryption-context`
* [ENHANCEMENT] Ingester: exposed metric `cortex_ingester_oldest_unshipped_block_timestamp_seconds`, tracking the unix timestamp of the oldest TSDB block not shipped to the storage yet. #3705

## 1.7.0 in progress
Expand Down
51 changes: 48 additions & 3 deletions docs/configuration/config-file-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1237,7 +1237,8 @@ storage:
# CLI flag: -ruler.storage.s3.insecure
[insecure: <boolean> | default = false]

# Enable AES256 AWS Server Side Encryption
# Enable AWS Server Side Encryption [Deprecated: Use .sse instead. if
# s3.sse-encryption is enabled, it assumes .sse.type SSE-S3]
# CLI flag: -ruler.storage.s3.sse-encryption
[sse_encryption: <boolean> | default = false]

Expand All @@ -1260,6 +1261,20 @@ storage:
# CLI flag: -ruler.storage.s3.signature-version
[signature_version: <string> | default = "v4"]

sse:
# Enable AWS Server Side Encryption. Only SSE-S3 and SSE-KMS are supported
# CLI flag: -ruler.storage.s3.sse.type
[type: <string> | default = ""]

# KMS Key ID used to encrypt objects in S3
# CLI flag: -ruler.storage.s3.sse.kms-key-id
[kms_key_id: <string> | default = ""]

# KMS Encryption Context used for object encryption. It expects a JSON as
# a string.
# CLI flag: -ruler.storage.s3.sse.kms-encryption-context
[kms_encryption_context: <string> | default = ""]

swift:
# OpenStack Swift authentication API version. 0 to autodetect.
# CLI flag: -ruler.storage.swift.auth-version
Expand Down Expand Up @@ -1677,7 +1692,8 @@ storage:
# CLI flag: -alertmanager.storage.s3.insecure
[insecure: <boolean> | default = false]

# Enable AES256 AWS Server Side Encryption
# Enable AWS Server Side Encryption [Deprecated: Use .sse instead. if
# s3.sse-encryption is enabled, it assumes .sse.type SSE-S3]
# CLI flag: -alertmanager.storage.s3.sse-encryption
[sse_encryption: <boolean> | default = false]

Expand All @@ -1700,6 +1716,20 @@ storage:
# CLI flag: -alertmanager.storage.s3.signature-version
[signature_version: <string> | default = "v4"]

sse:
# Enable AWS Server Side Encryption. Only SSE-S3 and SSE-KMS are supported
# CLI flag: -alertmanager.storage.s3.sse.type
[type: <string> | default = ""]

# KMS Key ID used to encrypt objects in S3
# CLI flag: -alertmanager.storage.s3.sse.kms-key-id
[kms_key_id: <string> | default = ""]

# KMS Encryption Context used for object encryption. It expects a JSON as
# a string.
# CLI flag: -alertmanager.storage.s3.sse.kms-encryption-context
[kms_encryption_context: <string> | default = ""]

local:
# Path at which alertmanager configurations are stored.
# CLI flag: -alertmanager.storage.local.path
Expand Down Expand Up @@ -2194,7 +2224,8 @@ aws:
# CLI flag: -s3.insecure
[insecure: <boolean> | default = false]

# Enable AES256 AWS Server Side Encryption
# Enable AWS Server Side Encryption [Deprecated: Use .sse instead. if
# s3.sse-encryption is enabled, it assumes .sse.type SSE-S3]
# CLI flag: -s3.sse-encryption
[sse_encryption: <boolean> | default = false]

Expand All @@ -2217,6 +2248,20 @@ aws:
# CLI flag: -s3.signature-version
[signature_version: <string> | default = "v4"]

sse:
# Enable AWS Server Side Encryption. Only SSE-S3 and SSE-KMS are supported
# CLI flag: -s3.sse.type
[type: <string> | default = ""]

# KMS Key ID used to encrypt objects in S3
# CLI flag: -s3.sse.kms-key-id
[kms_key_id: <string> | default = ""]

# KMS Encryption Context used for object encryption. It expects a JSON as a
# string.
# CLI flag: -s3.sse.kms-encryption-context
[kms_encryption_context: <string> | default = ""]

azure:
# Azure Cloud environment. Supported values are: AzureGlobal, AzureChinaCloud,
# AzureGermanCloud, AzureUSGovernment.
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/v1-guarantees.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Cortex is an actively developed project and we want to encourage the introductio

Currently experimental features are:

- S3 Server Side Encryption (SSE) using KMS.
- Azure blob storage.
- Zone awareness based replication.
- Shuffle sharding (both read and write path).
Expand Down
14 changes: 13 additions & 1 deletion integration/e2e/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package e2edb
import (
"fmt"
"net/url"
"strings"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
Expand All @@ -19,11 +20,17 @@ const (

// NewMinio returns minio server, used as a local replacement for S3.
func NewMinio(port int, bktName string) *e2e.HTTPService {
minioKESGithubContent := "https://raw.githubusercontent.com/minio/kes/master"
commands := []string{
"curl -sSL --tlsv1.2 -O '%s/root.key' -O '%s/root.cert'",
"mkdir -p /data/%s && minio server --address :%v --quiet /data",
}

m := e2e.NewHTTPService(
fmt.Sprintf("minio-%v", port),
images.Minio,
// Create the "cortex" bucket before starting minio
e2e.NewCommandWithoutEntrypoint("sh", "-c", fmt.Sprintf("mkdir -p /data/%s && minio server --address :%v --quiet /data", bktName, port)),
e2e.NewCommandWithoutEntrypoint("sh", "-c", fmt.Sprintf(strings.Join(commands, " && "), minioKESGithubContent, minioKESGithubContent, bktName, port)),
e2e.NewHTTPReadinessProbe(port, "/minio/health/ready", 200, 200),
port,
)
Expand All @@ -32,6 +39,11 @@ func NewMinio(port int, bktName string) *e2e.HTTPService {
"MINIO_SECRET_KEY": MinioSecretKey,
"MINIO_BROWSER": "off",
"ENABLE_HTTPS": "0",
// https://docs.min.io/docs/minio-kms-quickstart-guide.html
"MINIO_KMS_KES_ENDPOINT": "https://play.min.io:7373",
"MINIO_KMS_KES_KEY_FILE": "root.key",
"MINIO_KMS_KES_CERT_FILE": "root.cert",
"MINIO_KMS_KES_KEY_NAME": "my-minio-key",
})
return m
}
Expand Down
26 changes: 26 additions & 0 deletions integration/s3_storage_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,32 @@ func TestS3Client(t *testing.T) {
SecretAccessKey: e2edb.MinioSecretKey,
},
},
{
name: "config-with-deprecated-sse",
cfg: s3.S3Config{
Endpoint: minio.HTTPEndpoint(),
BucketNames: bucketName,
S3ForcePathStyle: true,
Insecure: true,
AccessKeyID: e2edb.MinioAccessKey,
SecretAccessKey: e2edb.MinioSecretKey,
SSEEncryption: true,
},
},
{
name: "config-with-sse-s3",
cfg: s3.S3Config{
Endpoint: minio.HTTPEndpoint(),
BucketNames: bucketName,
S3ForcePathStyle: true,
Insecure: true,
AccessKeyID: e2edb.MinioAccessKey,
SecretAccessKey: e2edb.MinioSecretKey,
SSEConfig: s3.SSEConfig{
Type: "SSE-S3",
},
},
},
}

for _, tt := range tests {
Expand Down
61 changes: 44 additions & 17 deletions pkg/chunk/aws/s3_storage_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type S3Config struct {
SSEEncryption bool `yaml:"sse_encryption"`
HTTPConfig HTTPConfig `yaml:"http_config"`
SignatureVersion string `yaml:"signature_version"`
SSEConfig SSEConfig `yaml:"sse"`

Inject InjectRequestMiddleware `yaml:"-"`
}
Expand Down Expand Up @@ -100,7 +101,11 @@ func (cfg *S3Config) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) {
f.StringVar(&cfg.AccessKeyID, prefix+"s3.access-key-id", "", "AWS Access Key ID")
f.StringVar(&cfg.SecretAccessKey, prefix+"s3.secret-access-key", "", "AWS Secret Access Key")
f.BoolVar(&cfg.Insecure, prefix+"s3.insecure", false, "Disable https on s3 connection.")
f.BoolVar(&cfg.SSEEncryption, prefix+"s3.sse-encryption", false, "Enable AES256 AWS Server Side Encryption")

// TODO Remove in Cortex 1.9.0
f.BoolVar(&cfg.SSEEncryption, prefix+"s3.sse-encryption", false, "Enable AWS Server Side Encryption [Deprecated: Use .sse instead. if s3.sse-encryption is enabled, it assumes .sse.type SSE-S3]")

cfg.SSEConfig.RegisterFlagsWithPrefix(prefix+"s3.sse.", f)

f.DurationVar(&cfg.HTTPConfig.IdleConnTimeout, prefix+"s3.http.idle-conn-timeout", 90*time.Second, "The maximum amount of time an idle connection will be held open.")
f.DurationVar(&cfg.HTTPConfig.ResponseHeaderTimeout, prefix+"s3.http.response-header-timeout", 0, "If non-zero, specifies the amount of time to wait for a server's response headers after fully writing the request.")
Expand All @@ -117,9 +122,9 @@ func (cfg *S3Config) Validate() error {
}

type S3ObjectClient struct {
bucketNames []string
S3 s3iface.S3API
sseEncryption *string
bucketNames []string
S3 s3iface.S3API
sseConfig *SSEParsedConfig
}

// NewS3ObjectClient makes a new S3-backed ObjectClient.
Expand All @@ -140,19 +145,34 @@ func NewS3ObjectClient(cfg S3Config) (*S3ObjectClient, error) {
s3Client.Handlers.Sign.Swap(v4.SignRequestHandler.Name, v2SignRequestHandler(cfg))
}

var sseEncryption *string
if cfg.SSEEncryption {
sseEncryption = aws.String("AES256")
sseCfg, err := buildSSEParsedConfig(cfg)
if err != nil {
return nil, errors.Wrap(err, "failed to build SSE config")
}

client := S3ObjectClient{
S3: s3Client,
bucketNames: bucketNames,
sseEncryption: sseEncryption,
S3: s3Client,
bucketNames: bucketNames,
sseConfig: sseCfg,
}
return &client, nil
}

func buildSSEParsedConfig(cfg S3Config) (*SSEParsedConfig, error) {
if cfg.SSEConfig.Type != "" {
return NewSSEParsedConfig(cfg.SSEConfig)
}

// deprecated, but if used it assumes SSE-S3 type
if cfg.SSEEncryption {
return NewSSEParsedConfig(SSEConfig{
Type: SSES3,
})
}

return nil, nil
}

func v2SignRequestHandler(cfg S3Config) request.NamedHandler {
return request.NamedHandler{
Name: "v2.SignRequestHandler",
Expand Down Expand Up @@ -324,15 +344,22 @@ func (a *S3ObjectClient) GetObject(ctx context.Context, objectKey string) (io.Re
return resp.Body, nil
}

// Put object into the store
// PutObject into the store
func (a *S3ObjectClient) PutObject(ctx context.Context, objectKey string, object io.ReadSeeker) error {
return instrument.CollectedRequest(ctx, "S3.PutObject", s3RequestDuration, instrument.ErrorCode, func(ctx context.Context) error {
_, err := a.S3.PutObjectWithContext(ctx, &s3.PutObjectInput{
Body: object,
Bucket: aws.String(a.bucketFromKey(objectKey)),
Key: aws.String(objectKey),
ServerSideEncryption: a.sseEncryption,
})
putObjectInput := &s3.PutObjectInput{
Body: object,
Bucket: aws.String(a.bucketFromKey(objectKey)),
Key: aws.String(objectKey),
}

if a.sseConfig != nil {
putObjectInput.ServerSideEncryption = aws.String(a.sseConfig.ServerSideEncryption)
putObjectInput.SSEKMSKeyId = a.sseConfig.KMSKeyID
putObjectInput.SSEKMSEncryptionContext = a.sseConfig.KMSEncryptionContext
}

_, err := a.S3.PutObjectWithContext(ctx, putObjectInput)
return err
})
}
Expand Down
86 changes: 86 additions & 0 deletions pkg/chunk/aws/sse_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package aws

import (
"encoding/base64"
"encoding/json"
"flag"

"github.com/pkg/errors"
)

const (
// SSEKMS config type constant to configure S3 server side encryption using KMS
// https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html
SSEKMS = "SSE-KMS"
sseKMSType = "aws:kms"
// SSES3 config type constant to configure S3 server side encryption with AES-256
// https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html
SSES3 = "SSE-S3"
sseS3Type = "AES256"
)

// SSEParsedConfig configures server side encryption (SSE)
// struct used internally to configure AWS S3
type SSEParsedConfig struct {
ServerSideEncryption string
KMSKeyID *string
KMSEncryptionContext *string
Comment on lines +26 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a big fan of pointers in config structs. You can achieve the same assuming empty string means not configured (which should be fine in this context).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AWS lib uses string pointers several times in there. Removing string pointers from sse_config.go (SSEEncryptionConfig) means that I will have to treat that on the file s3_storage_client.go, which has more logic that should have.

}

// SSEConfig configures S3 server side encryption
// struct that is going to receive user input (through config file or CLI)
type SSEConfig struct {
Type string `yaml:"type"`
KMSKeyID string `yaml:"kms_key_id"`
KMSEncryptionContext string `yaml:"kms_encryption_context"`
}

// RegisterFlagsWithPrefix adds the flags required to config this to the given FlagSet
func (cfg *SSEConfig) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) {
f.StringVar(&cfg.Type, prefix+"type", "", "Enable AWS Server Side Encryption. Only SSE-S3 and SSE-KMS are supported")
f.StringVar(&cfg.KMSKeyID, prefix+"kms-key-id", "", "KMS Key ID used to encrypt objects in S3")
f.StringVar(&cfg.KMSEncryptionContext, prefix+"kms-encryption-context", "", "KMS Encryption Context used for object encryption. It expects a JSON as a string.")
}

// NewSSEParsedConfig creates a struct to configure server side encryption (SSE)
func NewSSEParsedConfig(cfg SSEConfig) (*SSEParsedConfig, error) {
switch cfg.Type {
case SSES3:
return &SSEParsedConfig{
ServerSideEncryption: sseS3Type,
}, nil
case SSEKMS:
if cfg.KMSKeyID == "" {
return nil, errors.New("KMS key id must be passed when SSE-KMS encryption is selected")
}

parsedKMSEncryptionContext, err := parseKMSEncryptionContext(cfg.KMSEncryptionContext)
if err != nil {
return nil, errors.Wrap(err, "failed to parse KMS encryption context")
}

return &SSEParsedConfig{
ServerSideEncryption: sseKMSType,
KMSKeyID: &cfg.KMSKeyID,
KMSEncryptionContext: parsedKMSEncryptionContext,
}, nil
default:
return nil, errors.New("SSE type is empty or invalid")
}
}

func parseKMSEncryptionContext(kmsEncryptionContext string) (*string, error) {
if kmsEncryptionContext == "" {
return nil, nil
}

// validates if kmsEncryptionContext is a valid JSON
jsonKMSEncryptionContext, err := json.Marshal(json.RawMessage(kmsEncryptionContext))
if err != nil {
return nil, errors.Wrap(err, "failed to marshal KMS encryption context")
}

parsedKMSEncryptionContext := base64.StdEncoding.EncodeToString([]byte(jsonKMSEncryptionContext))

return &parsedKMSEncryptionContext, nil
}
Loading