Commit 3963b251 authored by Stan Hu's avatar Stan Hu Committed by Michael Kozono

Support AWS SSE-KMS in backups

AWS supports three different modes for encrypting S3 data:

1. Server-Side Encryption with Amazon S3-Managed Keys (SSE-S3)
2. Server-Side Encryption with Customer Master Keys (CMKs) Stored in AWS
Key Management Service (SSE-KMS)
3. Server-Side Encryption with Customer-Provided Keys (SSE-C)

Previously, SSE-S3 and SSE-C were supported via the
`backup.upload.encryption` and `backup.upload.encryption_key`
configuration options.

SSE-KMS was previously not supported in backups because there was no way
to specify which customer-managed key to use. However, we did support
SSE-KMS with consolidated object storage enabled for other CI artifacts,
attachments, LFS, etc. Note that SSE-C is NOT supported here.

In consolidated object storage, the `storage_options` Hash provides the
`server_side_encryption` and `server_side_encryption_kms_key_id`
parameters that allow admins to configure SSE-KMS. We reuse this
configuration in backups to support SSE-KMS.

Relates to #338764

Changelog: added
parent ae8fd6ad
......@@ -1146,14 +1146,22 @@ production: &base
# # Use multipart uploads when file size reaches 100MB, see
# # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html
# multipart_chunk_size: 104857600
# # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
# # encryption: 'AES256'
# # Specifies Amazon S3 storage class to use for backups (optional)
# # storage_class: 'STANDARD'
# # Turns on AWS Server-Side Encryption with Amazon Customer-Provided Encryption Keys for backups, this is optional
# # This should be set to the 256-bit encryption key for Amazon S3 to use to encrypt or decrypt your data.
# # 'encryption' must also be set in order for this to have any effect.
# # 'encryption' must be set in order for this to have any effect.
# # 'encryption_key' should be set to the 256-bit encryption key for Amazon S3 to use to encrypt or decrypt your data.
# # encryption: 'AES256'
# # encryption_key: '<key>'
# # Specifies Amazon S3 storage class to use for backups, this is optional
# # storage_class: 'STANDARD'
# #
# # Turns on AWS Server-Side Encryption with Amazon S3-Managed keys (optional)
# # https://docs.aws.amazon.com/AmazonS3/latest/userguide/serv-side-encryption.html
# # For SSE-S3, set 'server_side_encryption' to 'AES256'.
# # For SS3-KMS, set 'server_side_encryption' to 'aws:kms'. Set
# # 'server_side_encryption_kms_key_id' to the ARN of customer master key.
# # storage_options:
# # server_side_encryption: 'aws:kms'
# # server_side_encryption_kms_key_id: 'arn:aws:kms:YOUR-KEY-ID-HERE'
## Pseudonymizer exporter
pseudonymizer:
......
......@@ -47,10 +47,12 @@ module Backup
return
end
directory = connect_to_remote_directory(Gitlab.config.backup.upload)
directory = connect_to_remote_directory
upload = directory.files.create(create_attributes)
if directory.files.create(create_attributes)
if upload
progress.puts "done".color(:green)
upload
else
puts "uploading backup to #{remote_directory} failed".color(:red)
raise Backup::Error, 'Backup failed'
......@@ -206,11 +208,16 @@ module Backup
@backup_file_list.map {|item| item.gsub("#{FILE_NAME_SUFFIX}", "")}
end
def connect_to_remote_directory(options)
config = ObjectStorage::Config.new(options)
config.load_provider
def object_storage_config
@object_storage_config ||= begin
config = ObjectStorage::Config.new(Gitlab.config.backup.upload)
config.load_provider
config
end
end
connection = ::Fog::Storage.new(config.credentials)
def connect_to_remote_directory
connection = ::Fog::Storage.new(object_storage_config.credentials)
# We only attempt to create the directory for local backups. For AWS
# and other cloud providers, we cannot guarantee the user will have
......@@ -280,10 +287,8 @@ module Backup
key: remote_target,
body: File.open(File.join(backup_path, tar_file)),
multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
encryption: Gitlab.config.backup.upload.encryption,
encryption_key: Gitlab.config.backup.upload.encryption_key,
storage_class: Gitlab.config.backup.upload.storage_class
}
}.merge(encryption_attributes)
# Google bucket-only policies prevent setting an ACL. In any case, by default,
# all objects are set to the default ACL, which is project-private:
......@@ -293,6 +298,19 @@ module Backup
attrs
end
def encryption_attributes
return object_storage_config.fog_attributes if object_storage_config.aws_server_side_encryption_enabled?
# Use customer-managed keys. Also, this preserves
# backward-compatibility for existing usages of `SSE-S3` that
# don't set `backup.upload.storage_options.server_side_encryption`
# to `'AES256'`.
{
encryption_key: Gitlab.config.backup.upload.encryption_key,
encryption: Gitlab.config.backup.upload.encryption
}
end
def google_provider?
Gitlab.config.backup.upload.connection&.provider&.downcase == 'google'
end
......
......@@ -84,13 +84,16 @@ module ObjectStorage
def fog_attributes
@fog_attributes ||= begin
return {} unless enabled? && aws?
return {} unless server_side_encryption.present?
return {} unless aws_server_side_encryption_enabled?
aws_server_side_encryption_headers.compact
end
end
def aws_server_side_encryption_enabled?
aws? && server_side_encryption.present?
end
private
# This returns a Hash of HTTP encryption headers to send along to S3.
......
......@@ -432,6 +432,77 @@ RSpec.describe Backup::Manager do
end
end
context 'with AWS with server side encryption' do
let(:connection) { ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys) }
let(:encryption_key) { nil }
let(:encryption) { nil }
let(:storage_options) { nil }
before do
stub_backup_setting(
upload: {
connection: {
provider: 'AWS',
aws_access_key_id: 'AWS_ACCESS_KEY_ID',
aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY'
},
remote_directory: 'directory',
multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
encryption: encryption,
encryption_key: encryption_key,
storage_options: storage_options,
storage_class: nil
}
)
connection.directories.create(key: Gitlab.config.backup.upload.remote_directory)
end
context 'with SSE-S3 without using storage_options' do
let(:encryption) { 'AES256' }
it 'sets encryption attributes' do
result = subject.upload
expect(result.key).to be_present
expect(result.encryption).to eq('AES256')
expect(result.encryption_key).to be_nil
expect(result.kms_key_id).to be_nil
end
end
context 'with SSE-C (customer-provided keys) options' do
let(:encryption) { 'AES256' }
let(:encryption_key) { SecureRandom.hex }
it 'sets encryption attributes' do
result = subject.upload
expect(result.key).to be_present
expect(result.encryption).to eq(encryption)
expect(result.encryption_key).to eq(encryption_key)
expect(result.kms_key_id).to be_nil
end
end
context 'with SSE-KMS options' do
let(:storage_options) do
{
server_side_encryption: 'aws:kms',
server_side_encryption_kms_key_id: 'arn:aws:kms:12345'
}
end
it 'sets encryption attributes' do
result = subject.upload
expect(result.key).to be_present
expect(result.encryption).to eq('aws:kms')
expect(result.kms_key_id).to eq('arn:aws:kms:12345')
end
end
end
context 'with Google provider' do
before do
stub_backup_setting(
......
......@@ -188,6 +188,7 @@ RSpec.describe ObjectStorage::Config do
end
context 'with SSE-KMS enabled' do
it { expect(subject.aws_server_side_encryption_enabled?).to be true }
it { expect(subject.server_side_encryption).to eq('AES256') }
it { expect(subject.server_side_encryption_kms_key_id).to eq('arn:aws:12345') }
it { expect(subject.fog_attributes.keys).to match_array(%w(x-amz-server-side-encryption x-amz-server-side-encryption-aws-kms-key-id)) }
......@@ -196,6 +197,7 @@ RSpec.describe ObjectStorage::Config do
context 'with only server side encryption enabled' do
let(:storage_options) { { server_side_encryption: 'AES256' } }
it { expect(subject.aws_server_side_encryption_enabled?).to be true }
it { expect(subject.server_side_encryption).to eq('AES256') }
it { expect(subject.server_side_encryption_kms_key_id).to be_nil }
it { expect(subject.fog_attributes).to eq({ 'x-amz-server-side-encryption' => 'AES256' }) }
......@@ -204,6 +206,7 @@ RSpec.describe ObjectStorage::Config do
context 'without encryption enabled' do
let(:storage_options) { {} }
it { expect(subject.aws_server_side_encryption_enabled?).to be false }
it { expect(subject.server_side_encryption).to be_nil }
it { expect(subject.server_side_encryption_kms_key_id).to be_nil }
it { expect(subject.fog_attributes).to eq({}) }
......@@ -215,6 +218,5 @@ RSpec.describe ObjectStorage::Config do
end
it { expect(subject.enabled?).to be false }
it { expect(subject.fog_attributes).to eq({}) }
end
end
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