spec/chef/cookbooks/package/libraries/secrets_helper_spec.rb (252 lines of code) (raw):
require 'chef_helper'
require 'base64'
RSpec.describe 'secrets' do
let(:chef_run) { ChefSpec::SoloRunner.new.converge('gitlab::default') }
HEX_KEY = /\h{128}/.freeze
ALPHANUMERIC_KEY = /\A[A-Za-z0-9]{32}\Z/m.freeze
RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m.freeze
def stub_gitlab_secrets_json(secrets)
allow(File).to receive(:read).with('/etc/gitlab/gitlab-secrets.json').and_return(JSON.generate(secrets))
end
before do
allow(File).to receive(:directory?).and_call_original
allow(File).to receive(:exist?).and_call_original
allow(File).to receive(:read).and_call_original
allow(File).to receive(:open).and_call_original
allow(File).to receive(:open).with('/etc/gitlab/initial_root_password', 'w', 0600).and_yield(double(:file, write: true)).once
end
context 'when the default directory does not exist' do
it 'does not write secrets to the file' do
allow(File).to receive(:directory?).with('/etc/gitlab').and_return(false)
expect(File).not_to receive(:open).with('/etc/gitlab/gitlab-secrets.json', 'w')
chef_run
end
end
context 'when the default directory does exists' do
let(:file) { double(:file) }
let(:new_secrets) { @new_secrets }
before do
allow(SecretsHelper).to receive(:system)
allow(File).to receive(:directory?).with('/etc/gitlab').and_return(true)
allow(File).to receive(:open).with('/etc/gitlab/gitlab-secrets.json', 'w', 0600).and_yield(file).once
allow(file).to receive(:puts) { |json| @new_secrets = JSON.parse(json) }
allow(file).to receive(:chmod).and_return(true)
end
context 'when there are no existing secrets' do
before do
allow(File).to receive(:exist?).with('/etc/gitlab/gitlab-secrets.json').and_return(false)
chef_run
end
it 'writes new secrets to the file, with different values for each' do
rails_keys = new_secrets['gitlab_rails']
hex_keys = rails_keys.values_at('db_key_base', 'otp_key_base', 'secret_key_base', 'encrypted_settings_key_base')
alphanumeric_keys = rails_keys.values_at(
'active_record_encryption_primary_key',
'active_record_encryption_deterministic_key',
'active_record_encryption_key_derivation_salt'
).flatten
rsa_keys = rails_keys.values_at('openid_connect_signing_key')
expect(rails_keys.to_a.uniq).to eq(rails_keys.to_a)
expect(hex_keys).to all(match(HEX_KEY))
expect(alphanumeric_keys.flatten).to all(match(ALPHANUMERIC_KEY))
expect(rsa_keys).to all(match(RSA_KEY))
end
it 'does not write legacy keys' do
expect(new_secrets).not_to have_key('gitlab_ci')
expect(new_secrets['gitlab_rails']).not_to have_key('jws_private_key')
end
it 'generates an appropriate secret for gitlab-workhorse' do
workhorse_secret = new_secrets['gitlab_workhorse']['secret_token']
expect(Base64.strict_decode64(workhorse_secret).length).to eq(32)
end
it 'generates an appropriate shared secret for gitlab-pages' do
pages_shared_secret = new_secrets['gitlab_pages']['api_secret_key']
expect(Base64.strict_decode64(pages_shared_secret).length).to eq(32)
end
it 'generates an appropriate shared secret for gitlab-kas' do
kas_shared_secret = new_secrets['gitlab_kas']['api_secret_key']
expect(Base64.strict_decode64(kas_shared_secret).length).to eq(32)
end
it 'generates an appropriate shared secret for suggested-reviewers' do
suggested_reviewers_shared_secret = new_secrets['suggested_reviewers']['api_secret_key']
expect(Base64.strict_decode64(suggested_reviewers_shared_secret).length).to eq(32)
end
end
context 'gitlab.rb provided gitlab_pages.api_secret_key' do
before do
allow(Gitlab).to receive(:[]).and_call_original
end
it 'fails when provided gitlab_pages.shared_secret is not 32 bytes' do
stub_gitlab_rb(gitlab_pages: { api_secret_key: SecureRandom.base64(16) })
expect { chef_run }.to raise_error(RuntimeError, /gitlab_pages\['api_secret_key'\] should be exactly 32 bytes/)
end
it 'accepts provided gitlab_pages.api_secret_key when it is 32 bytes' do
api_secret_key = SecureRandom.base64(32)
stub_gitlab_rb(gitlab_pages: { api_secret_key: api_secret_key })
expect { chef_run }.not_to raise_error
expect(new_secrets['gitlab_pages']['api_secret_key']).to eq(api_secret_key)
end
end
context 'gitlab.rb provided gitlab_kas.api_secret_key' do
before do
allow(Gitlab).to receive(:[]).and_call_original
end
it 'fails when provided gitlab_kas.shared_secret is not 32 bytes' do
stub_gitlab_rb(gitlab_kas: { api_secret_key: SecureRandom.base64(16) })
expect { chef_run }.to raise_error(RuntimeError, /gitlab_kas\['api_secret_key'\] should be exactly 32 bytes/)
end
it 'accepts provided gitlab_kas.api_secret_key when it is 32 bytes' do
api_secret_key = SecureRandom.base64(32)
stub_gitlab_rb(gitlab_kas: { api_secret_key: api_secret_key })
expect { chef_run }.not_to raise_error
expect(new_secrets['gitlab_kas']['api_secret_key']).to eq(api_secret_key)
end
end
context 'when there are existing secrets in /etc/gitlab/gitlab-secrets.json' do
before do
allow(SecretsHelper).to receive(:system)
allow(File).to receive(:directory?).with('/etc/gitlab').and_return(true)
allow(File).to receive(:open).with('/etc/gitlab/gitlab-secrets.json', 'w').and_yield(file).once
allow(File).to receive(:exist?).with('/etc/gitlab/gitlab-secrets.json').and_return(true)
end
context 'when secrets are only partially present' do
before do
stub_gitlab_secrets_json(gitlab_ci: { db_key_base: 'json_ci_db_key_base' })
chef_run
end
it 'uses secrets from /etc/gitlab/gitlab-secrets.json where available' do
expect(new_secrets['gitlab_rails']['db_key_base']).to eq('json_ci_db_key_base')
end
it 'falls back further to generating new secrets' do
expect(new_secrets['gitlab_rails']['otp_key_base']).to match(HEX_KEY)
end
end
context 'when secrets exist under legacy keys' do
before do
stub_gitlab_secrets_json(
gitlab_ci: { db_key_base: 'json_ci_db_key_base', secret_token: 'json_ci_secret_token' },
gitlab_rails: {
secret_token: 'json_rails_secret_token',
jws_private_key: 'json_rails_jws_private_key'
}
)
chef_run
end
it 'moves gitlab_ci.db_key_base to gitlab_rails.db_key_base' do
expect(new_secrets['gitlab_rails']['db_key_base']).to eq('json_ci_db_key_base')
end
it 'moves gitlab_rails.secret_token to gitlab_rails.otp_key_base' do
expect(new_secrets['gitlab_rails']['otp_key_base']).to eq('json_rails_secret_token')
end
it 'moves gitlab_ci.db_key_base to gitlab_rails.secret_key_base' do
expect(new_secrets['gitlab_rails']['secret_key_base']).to eq('json_ci_db_key_base')
end
it 'moves gitlab_rails.jws_private_key to gitlab_rails.openid_connect_signing_key' do
expect(new_secrets['gitlab_rails']['openid_connect_signing_key']).to eq('json_rails_jws_private_key')
end
it 'ignores other, unused, secrets' do
expect(new_secrets.inspect).not_to include('json_ci_secret_token')
end
end
end
context 'when there are existing secrets in /etc/gitlab/gitlab.rb and /etc/gitlab/gitlab-secrets.json' do
before do
allow(Gitlab).to receive(:[]).and_call_original
allow(File).to receive(:exist?).with('/etc/gitlab/gitlab-secrets.json').and_return(true)
end
context 'when secrets are only partially present' do
before do
stub_gitlab_secrets_json(
gitlab_ci: { db_key_base: 'json_ci_db_key_base' },
gitlab_rails: {
secret_token: 'json_rails_secret_token',
jws_private_key: 'json_rails_jws_private_key'
}
)
stub_gitlab_rb(gitlab_ci: { db_key_base: 'rb_ci_db_key_base' })
chef_run
end
it 'uses secrets from /etc/gitlab/gitlab.rb when available' do
expect(new_secrets['gitlab_rails']['db_key_base']).to eq('rb_ci_db_key_base')
end
it 'falls back to secrets from /etc/gitlab/gitlab-secrets.json' do
expect(new_secrets['gitlab_rails']['otp_key_base']).to eq('json_rails_secret_token')
end
it 'falls back further to generating new secrets' do
expect(new_secrets['gitlab_shell']['secret_token']).to match(HEX_KEY)
end
end
context 'when secrets exist under legacy keys' do
before do
stub_gitlab_rb(gitlab_ci: { db_key_base: 'rb_ci_db_key_base',
secret_token: 'rb_ci_secret_token' })
stub_gitlab_secrets_json(
gitlab_rails: {
secret_token: 'json_rails_secret_token',
jws_private_key: 'json_rails_jws_private_key',
encrypted_settings_key_base: 'encrypted_settings_key_base',
active_record_encryption_primary_key: ['primary_key'],
active_record_encryption_deterministic_key: ['deterministic_key'],
active_record_encryption_key_derivation_salt: 'key_derivation_salt'
}
)
chef_run
end
it 'moves gitlab_ci.db_key_base to gitlab_rails.db_key_base' do
expect(new_secrets['gitlab_rails']['db_key_base']).to eq('rb_ci_db_key_base')
end
it 'moves gitlab_rails.secret_token to gitlab_rails.otp_key_base' do
expect(new_secrets['gitlab_rails']['otp_key_base']).to eq('json_rails_secret_token')
end
it 'moves gitlab_ci.db_key_base to gitlab_rails.secret_key_base' do
expect(new_secrets['gitlab_rails']['secret_key_base']).to eq('rb_ci_db_key_base')
end
it 'moves gitlab_rails.jws_private_key to gitlab_rails.openid_connect_signing_key' do
expect(new_secrets['gitlab_rails']['openid_connect_signing_key']).to eq('json_rails_jws_private_key')
end
it 'ignores other, unused, secrets' do
expect(new_secrets.inspect).not_to include('rb_ci_secret_token')
end
it 'writes the correct data to secrets.yml' do
expect(chef_run).to create_templatesymlink('Create a secrets.yml and create a symlink to Rails root').with_variables(
'secrets' => {
'production' => {
'db_key_base' => 'rb_ci_db_key_base',
'secret_key_base' => 'rb_ci_db_key_base',
'otp_key_base' => 'json_rails_secret_token',
'encrypted_settings_key_base' => 'encrypted_settings_key_base',
'openid_connect_signing_key' => 'json_rails_jws_private_key',
'active_record_encryption_primary_key' => ['primary_key'],
'active_record_encryption_deterministic_key' => ['deterministic_key'],
'active_record_encryption_key_derivation_salt' => 'key_derivation_salt'
}
}
)
end
it 'deletes the secret file' do
expect(chef_run).to delete_file('/var/opt/gitlab/gitlab-rails/etc/secret')
expect(chef_run).to delete_file('/opt/gitlab/embedded/service/gitlab-rails/.secret')
end
end
context 'when secrets are ambiguous and cannot be migrated automatically' do
before { stub_gitlab_secrets_json({}) }
it 'fails when gitlab_ci.db_key_base and gitlab_rails.db_key_base are different' do
stub_gitlab_rb(
gitlab_rails: { db_key_base: 'rb_rails_db_key_base' },
gitlab_ci: { db_key_base: 'rb_ci_db_key_base' }
)
expect(File).not_to receive(:open).with('/etc/gitlab/gitlab-secrets.json', 'w')
expect { chef_run }.to raise_error(RuntimeError, /db_key_base/)
end
it 'fails when the secret file does not match gitlab_rails.otp_key_base' do
secret_file = '/var/opt/gitlab/gitlab-rails/etc/secret'
stub_gitlab_rb(gitlab_rails: { otp_key_base: 'rb_rails_otp_key_base',
secret_key_base: 'rb_rails_secret_key_base' })
allow(File).to receive(:exist?).with(secret_file).and_return(true)
allow(File).to receive(:read).with(secret_file).and_return('secret_key_base')
expect { chef_run }.to raise_error(RuntimeError, /otp_key_base/)
end
end
end
end
end