From 3963b2511f01c84ab60b272ea10d4c47dba2ac02 Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Tue, 31 Aug 2021 19:13:48 +0000 Subject: [PATCH] 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 --- config/gitlab.yml.example | 20 +++++--- lib/backup/manager.rb | 36 +++++++++---- lib/object_storage/config.rb | 7 ++- spec/lib/backup/manager_spec.rb | 71 ++++++++++++++++++++++++++ spec/lib/object_storage/config_spec.rb | 4 +- 5 files changed, 120 insertions(+), 18 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index f7b1c2f7567e6..a5e3b8b24e7b3 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -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: diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 52810b0fb3594..6c5350082e888 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -47,10 +47,12 @@ def upload 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 @@ def available_timestamps @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 @@ def create_attributes 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 @@ def create_attributes 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 diff --git a/lib/object_storage/config.rb b/lib/object_storage/config.rb index f933d4e48661a..82d9fc070437c 100644 --- a/lib/object_storage/config.rb +++ b/lib/object_storage/config.rb @@ -84,13 +84,16 @@ def azure? 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. diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index 2cc1bf41d18da..32eea82cfdfa1 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -432,6 +432,77 @@ 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( diff --git a/spec/lib/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb index 0ead2a1d2699e..21b8a44b3d637 100644 --- a/spec/lib/object_storage/config_spec.rb +++ b/spec/lib/object_storage/config_spec.rb @@ -188,6 +188,7 @@ 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 @@ 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 @@ 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 @@ end it { expect(subject.enabled?).to be false } - it { expect(subject.fog_attributes).to eq({}) } end end -- GitLab