Commit 2deaf5bd authored by Jacob Vosmaer's avatar Jacob Vosmaer

Merge branch 'sh-add-s3-encryption' into 'master'

Add support for AWS S3 Server Side Encryption (SSE-KMS)

See merge request gitlab-org/gitlab-workhorse!537
parents 304da8bb 86dacfb2
---
title: Add support for AWS S3 Server Side Encryption (SSE-KMS)
merge_request: 537
author:
type: added
...@@ -44,6 +44,8 @@ type S3Config struct { ...@@ -44,6 +44,8 @@ type S3Config struct {
PathStyle bool `toml:"-"` PathStyle bool `toml:"-"`
Endpoint string `toml:"-"` Endpoint string `toml:"-"`
UseIamProfile bool `toml:"-"` UseIamProfile bool `toml:"-"`
ServerSideEncryption string `toml:"-"` // Server-side encryption mode (e.g. AES256, aws:kms)
SSEKMSKeyID string `toml:"-"` // Server-side encryption key-management service key ID (e.g. arn:aws:xxx)
} }
type RedisConfig struct { type RedisConfig struct {
......
...@@ -276,7 +276,7 @@ func TestSaveFile(t *testing.T) { ...@@ -276,7 +276,7 @@ func TestSaveFile(t *testing.T) {
} }
func TestSaveFileWithWorkhorseClient(t *testing.T) { func TestSaveFileWithWorkhorseClient(t *testing.T) {
s3Creds, s3Config, sess, ts := test.SetupS3(t) s3Creds, s3Config, sess, ts := test.SetupS3(t, "")
defer ts.Close() defer ts.Close()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
......
...@@ -22,6 +22,16 @@ type S3Object struct { ...@@ -22,6 +22,16 @@ type S3Object struct {
uploader uploader
} }
func setEncryptionOptions(input *s3manager.UploadInput, s3Config config.S3Config) {
if s3Config.ServerSideEncryption != "" {
input.ServerSideEncryption = aws.String(s3Config.ServerSideEncryption)
if s3Config.ServerSideEncryption == s3.ServerSideEncryptionAwsKms && s3Config.SSEKMSKeyID != "" {
input.SSEKMSKeyId = aws.String(s3Config.SSEKMSKeyID)
}
}
}
func NewS3Object(ctx context.Context, objectName string, s3Credentials config.S3Credentials, s3Config config.S3Config, deadline time.Time) (*S3Object, error) { func NewS3Object(ctx context.Context, objectName string, s3Credentials config.S3Credentials, s3Config config.S3Config, deadline time.Time) (*S3Object, error) {
pr, pw := io.Pipe() pr, pw := io.Pipe()
objectStorageUploadsOpen.Inc() objectStorageUploadsOpen.Inc()
...@@ -54,11 +64,15 @@ func NewS3Object(ctx context.Context, objectName string, s3Credentials config.S3 ...@@ -54,11 +64,15 @@ func NewS3Object(ctx context.Context, objectName string, s3Credentials config.S3
o.objectName = objectName o.objectName = objectName
uploader := s3manager.NewUploader(sess) uploader := s3manager.NewUploader(sess)
_, err = uploader.UploadWithContext(uploadCtx, &s3manager.UploadInput{ input := &s3manager.UploadInput{
Bucket: aws.String(s3Config.Bucket), Bucket: aws.String(s3Config.Bucket),
Key: aws.String(objectName), Key: aws.String(objectName),
Body: pr, Body: pr,
}) }
setEncryptionOptions(input, s3Config)
_, err = uploader.UploadWithContext(uploadCtx, input)
if err != nil { if err != nil {
o.uploadError = err o.uploadError = err
objectStorageUploadRequestsRequestFailed.Inc() objectStorageUploadRequestsRequestFailed.Inc()
......
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/config" "gitlab.com/gitlab-org/gitlab-workhorse/internal/config"
...@@ -21,7 +22,17 @@ import ( ...@@ -21,7 +22,17 @@ import (
) )
func TestS3ObjectUpload(t *testing.T) { func TestS3ObjectUpload(t *testing.T) {
creds, config, sess, ts := test.SetupS3(t) testCases := []struct {
encryption string
}{
{encryption: ""},
{encryption: s3.ServerSideEncryptionAes256},
{encryption: s3.ServerSideEncryptionAwsKms},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("encryption=%v", tc.encryption), func(t *testing.T) {
creds, config, sess, ts := test.SetupS3(t, tc.encryption)
defer ts.Close() defer ts.Close()
deadline := time.Now().Add(testTimeout) deadline := time.Now().Add(testTimeout)
...@@ -45,6 +56,7 @@ func TestS3ObjectUpload(t *testing.T) { ...@@ -45,6 +56,7 @@ func TestS3ObjectUpload(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
test.S3ObjectExists(t, sess, config, objectName, test.ObjectContent) test.S3ObjectExists(t, sess, config, objectName, test.ObjectContent)
test.CheckS3Metadata(t, sess, config, objectName)
cancel() cancel()
deleted := false deleted := false
...@@ -59,14 +71,16 @@ func TestS3ObjectUpload(t *testing.T) { ...@@ -59,14 +71,16 @@ func TestS3ObjectUpload(t *testing.T) {
}) })
require.True(t, deleted) require.True(t, deleted)
})
}
} }
func TestConcurrentS3ObjectUpload(t *testing.T) { func TestConcurrentS3ObjectUpload(t *testing.T) {
creds, uploadsConfig, uploadsSession, uploadServer := test.SetupS3WithBucket(t, "uploads") creds, uploadsConfig, uploadsSession, uploadServer := test.SetupS3WithBucket(t, "uploads", "")
defer uploadServer.Close() defer uploadServer.Close()
// This will return a separate S3 endpoint // This will return a separate S3 endpoint
_, artifactsConfig, artifactsSession, artifactsServer := test.SetupS3WithBucket(t, "artifacts") _, artifactsConfig, artifactsSession, artifactsServer := test.SetupS3WithBucket(t, "artifacts", "")
defer artifactsServer.Close() defer artifactsServer.Close()
deadline := time.Now().Add(testTimeout) deadline := time.Now().Add(testTimeout)
...@@ -116,7 +130,7 @@ func TestConcurrentS3ObjectUpload(t *testing.T) { ...@@ -116,7 +130,7 @@ func TestConcurrentS3ObjectUpload(t *testing.T) {
} }
func TestS3ObjectUploadCancel(t *testing.T) { func TestS3ObjectUploadCancel(t *testing.T) {
creds, config, _, ts := test.SetupS3(t) creds, config, _, ts := test.SetupS3(t, "")
defer ts.Close() defer ts.Close()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
......
...@@ -21,11 +21,11 @@ import ( ...@@ -21,11 +21,11 @@ import (
"github.com/johannesboyne/gofakes3/backend/s3mem" "github.com/johannesboyne/gofakes3/backend/s3mem"
) )
func SetupS3(t *testing.T) (config.S3Credentials, config.S3Config, *session.Session, *httptest.Server) { func SetupS3(t *testing.T, encryption string) (config.S3Credentials, config.S3Config, *session.Session, *httptest.Server) {
return SetupS3WithBucket(t, "test-bucket") return SetupS3WithBucket(t, "test-bucket", encryption)
} }
func SetupS3WithBucket(t *testing.T, bucket string) (config.S3Credentials, config.S3Config, *session.Session, *httptest.Server) { func SetupS3WithBucket(t *testing.T, bucket string, encryption string) (config.S3Credentials, config.S3Config, *session.Session, *httptest.Server) {
backend := s3mem.New() backend := s3mem.New()
faker := gofakes3.New(backend) faker := gofakes3.New(backend)
ts := httptest.NewServer(faker.Server()) ts := httptest.NewServer(faker.Server())
...@@ -42,6 +42,14 @@ func SetupS3WithBucket(t *testing.T, bucket string) (config.S3Credentials, confi ...@@ -42,6 +42,14 @@ func SetupS3WithBucket(t *testing.T, bucket string) (config.S3Credentials, confi
PathStyle: true, PathStyle: true,
} }
if encryption != "" {
config.ServerSideEncryption = encryption
if encryption == s3.ServerSideEncryptionAwsKms {
config.SSEKMSKeyID = "arn:aws:1234"
}
}
sess, err := session.NewSession(&aws.Config{ sess, err := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials(creds.AwsAccessKeyID, creds.AwsSecretAccessKey, ""), Credentials: credentials.NewStaticCredentials(creds.AwsAccessKeyID, creds.AwsSecretAccessKey, ""),
Endpoint: aws.String(ts.URL), Endpoint: aws.String(ts.URL),
...@@ -75,6 +83,29 @@ func S3ObjectExists(t *testing.T, sess *session.Session, config config.S3Config, ...@@ -75,6 +83,29 @@ func S3ObjectExists(t *testing.T, sess *session.Session, config config.S3Config,
}) })
} }
func CheckS3Metadata(t *testing.T, sess *session.Session, config config.S3Config, objectName string) {
// In a real S3 provider, s3crypto.NewDecryptionClient should probably be used
svc := s3.New(sess)
result, err := svc.GetObject(&s3.GetObjectInput{
Bucket: aws.String(config.Bucket),
Key: aws.String(objectName),
})
require.NoError(t, err)
if config.ServerSideEncryption != "" {
require.Equal(t, aws.String(config.ServerSideEncryption), result.ServerSideEncryption)
if config.ServerSideEncryption == s3.ServerSideEncryptionAwsKms {
require.Equal(t, aws.String(config.SSEKMSKeyID), result.SSEKMSKeyId)
} else {
require.Nil(t, result.SSEKMSKeyId)
}
} else {
require.Nil(t, result.ServerSideEncryption)
require.Nil(t, result.SSEKMSKeyId)
}
}
// S3ObjectDoesNotExist returns true if the object has been deleted, // S3ObjectDoesNotExist returns true if the object has been deleted,
// false otherwise. The return signature is different from // false otherwise. The return signature is different from
// S3ObjectExists because deletion may need to be retried since deferred // S3ObjectExists because deletion may need to be retried since deferred
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment