Skip to content

Commit 17f4c20

Browse files
Andrew Pattersonbalasankarc
authored andcommitted
Add gitlab-ctl generate-secrets command
Add the generate-secrets command to gitlab-ctl which creates a JSON file containing secrets from the gitlab.rb file. The -f|--file option can be used to send the output to a specified file instead of the default /etc/gitlab/gitlab-secrets.json. Changelog: added
1 parent ed89250 commit 17f4c20

File tree

11 files changed

+286
-20
lines changed

11 files changed

+286
-20
lines changed

files/gitlab-config-template/gitlab.rb.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2746,7 +2746,7 @@ external_url 'GENERATED_EXTERNAL_URL'
27462746
##! Note: We do not recommend changing these values unless absolutely necessary
27472747
##! Set to false to only parse secrets from `gitlab-secrets.json` file but not generate them.
27482748
# package['generate_default_secrets'] = true
2749-
##! Set to false to prevent creating `gitlab-secrets.json` file
2749+
##! Set to false to prevent creating the default gitlab-secrets.json` file
27502750
# package['generate_secrets_json_file'] = true
27512751
################################################################################
27522752
################################################################################

files/gitlab-cookbooks/gitlab-pages/libraries/gitlab_pages.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ class << self
2727
def parse_variables
2828
parse_pages_external_url
2929
parse_gitlab_pages_daemon
30-
parse_secrets
30+
# Only call parse_secrets when not generating a defaults secrets file.
31+
parse_secrets unless Gitlab['node'][SecretsHelper::SECRETS_FILE_CHEF_ATTR]
3132
parse_automatic_oauth_registration
3233
end
3334

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#
2+
# Copyright:: Copyright (c) 2018 GitLab Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
require_relative '../../package/libraries/helpers/secrets_helper'
18+
19+
Gitlab[:node] = node
20+
21+
if node['package']['generate_secrets_json_file'] == true
22+
warning_message = <<~EOS
23+
You have enabled writing to the default secrets file location with package['generate_secrets_json_file'] in the gitlab.rb file which is not compatible with this command.
24+
Use 'gitlab-ctl reconfigure' to generate secrets instead and copy the resulting #{SecretsHelper::SECRETS_FILE} file.
25+
EOS
26+
LoggingHelper.warning(warning_message)
27+
return
28+
end
29+
30+
secrets_file = node[SecretsHelper::SECRETS_FILE_CHEF_ATTR] || SecretsHelper::SECRETS_FILE
31+
node.override[SecretsHelper::SKIP_GENERATE_SECRETS_CHEF_ATTR] = false
32+
Gitlab.generate_secrets(node['fqdn'], secrets_file)

files/gitlab-cookbooks/package/libraries/helpers/secrets_helper.rb

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
require 'openssl'
22

33
class SecretsHelper
4+
SECRETS_FILE = '/etc/gitlab/gitlab-secrets.json'.freeze
5+
SECRETS_FILE_CHEF_ATTR = '_gitlab_secrets_file_path'.freeze
6+
SKIP_GENERATE_SECRETS_CHEF_ATTR = '_skip_generate_secrets'.freeze
7+
48
def self.generate_hex(chars)
59
SecureRandom.hex(chars)
610
end
@@ -40,17 +44,17 @@ def self.generate_keypair(bits:, subject:, validity:)
4044
# Load the secrets from disk
4145
#
4246
# @return [Hash] empty if no secrets
43-
def self.load_gitlab_secrets
47+
def self.load_gitlab_secrets(path = SecretsHelper::SECRETS_FILE)
4448
existing_secrets = {}
4549

46-
existing_secrets = Chef::JSONCompat.from_json(File.read("/etc/gitlab/gitlab-secrets.json")) if File.exist?("/etc/gitlab/gitlab-secrets.json")
50+
existing_secrets = Chef::JSONCompat.from_json(File.read(path)) if File.exist?(path)
4751

4852
existing_secrets
4953
end
5054

5155
# Reads the secrets into the Gitlab config singleton
52-
def self.read_gitlab_secrets
53-
existing_secrets = load_gitlab_secrets
56+
def self.read_gitlab_secrets(path = SecretsHelper::SECRETS_FILE)
57+
existing_secrets = load_gitlab_secrets(path)
5458

5559
existing_secrets.each do |k, v|
5660
if Gitlab[k]
@@ -59,7 +63,7 @@ def self.read_gitlab_secrets
5963
Gitlab[k][pk] ||= p
6064
end
6165
else
62-
warn("Ignoring section #{k} in /etc/gitlab/gitlab-secrets.json, does not exist in gitlab.rb")
66+
warn("Ignoring section #{k} in #{path}, does not exist in gitlab.rb")
6367
end
6468
end
6569
end
@@ -130,11 +134,11 @@ def self.gather_gitlab_secrets # rubocop:disable Metrics/AbcSize
130134
secret_tokens
131135
end
132136

133-
def self.write_to_gitlab_secrets
137+
def self.write_to_gitlab_secrets(path = SECRETS_FILE)
134138
secret_tokens = gather_gitlab_secrets
135139

136-
if File.directory?('/etc/gitlab')
137-
File.open('/etc/gitlab/gitlab-secrets.json', 'w', 0600) do |f|
140+
if File.directory?(File.dirname(path))
141+
File.open(path, 'w', 0600) do |f|
138142
f.puts(Chef::JSONCompat.to_json_pretty(secret_tokens))
139143
f.chmod(0600)
140144
end

files/gitlab-cookbooks/package/libraries/settings_dsl.rb

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,18 @@ def load_roles
162162
end
163163
end
164164

165-
def generate_secrets(node_name)
165+
def generate_secrets(node_name, path = SecretsHelper::SECRETS_FILE)
166+
# Gitlab['node'][SecretsHelper::SKIP_GENERATE_SECRETS_CHEF_ATTR] is set to
167+
# true if we are calling from the 'gitlab-ctrl generate-secrets' command
168+
# and we are running the 'config(-ee)' recipe in which case we will generate
169+
# secrets in the 'generate_secrets' recipe where it will then be set to
170+
# false.
171+
return if Gitlab['node'][SecretsHelper::SKIP_GENERATE_SECRETS_CHEF_ATTR] == true
172+
173+
force_write_secrets = !Gitlab['node'][SecretsHelper::SECRETS_FILE_CHEF_ATTR].nil?
174+
166175
# guards against creating secrets on non-bootstrap node
167-
SecretsHelper.read_gitlab_secrets
176+
SecretsHelper.read_gitlab_secrets(path)
168177
generate_default_secrets = Gitlab['package']['generate_default_secrets'] != false
169178

170179
Chef::Log.info("Generating default secrets") if generate_default_secrets
@@ -175,18 +184,18 @@ def generate_secrets(node_name)
175184
handler.validate_secrets if handler.respond_to?(:validate_secrets)
176185
end
177186

178-
if Gitlab['package']['generate_secrets_json_file'] == false
187+
if Gitlab['package']['generate_secrets_json_file'] == false && !force_write_secrets
179188
return unless generate_default_secrets
180189

181190
warning_message = <<~EOS
182-
You've enabled generating default secrets but have disabled writing them to gitlab-secrets.json file.
191+
You've enabled generating default secrets but have disabled writing them to #{path} file.
183192
This results in secrets not persisting across `gitlab-ctl reconfigure` runs and can cause issues with functionality.
184193
EOS
185194

186195
LoggingHelper.warning(warning_message)
187196
else
188-
Chef::Log.info("Generating gitlab-secrets.json file")
189-
SecretsHelper.write_to_gitlab_secrets
197+
Chef::Log.info("Generating #{path} file")
198+
SecretsHelper.write_to_gitlab_secrets(path)
190199
end
191200
end
192201

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
#
13+
14+
require "#{base_path}/embedded/service/omnibus-ctl/lib/gitlab_ctl/generate_secrets"
15+
require "#{base_path}/embedded/service/omnibus-ctl/lib/gitlab_ctl/util"
16+
17+
add_command 'generate-secrets', 'Generates secrets used in gitlab.rb', 2 do |cmd_name|
18+
begin
19+
options = GitlabCtl::GenerateSecrets.parse_options(ARGV)
20+
rescue OptionParser::ParseError => e
21+
warn "#{e}\n\n#{GitlabCtl::GenerateSecrets::USAGE}"
22+
exit 128
23+
end
24+
25+
json_attributes = %(
26+
{
27+
"run_list": [
28+
"recipe[#{GitlabCtl::Util.master_cookbook}::config]",
29+
"recipe[gitlab::generate_secrets]"
30+
],
31+
"_gitlab_secrets_file_path": "#{options[:secrets_path]}",
32+
"_skip_generate_secrets": true
33+
}).strip
34+
command = %W( cinc-client
35+
--log_level info
36+
--local-mode
37+
--config #{base_path}/embedded/cookbooks/solo.rb
38+
--json-attributes /dev/stdin)
39+
40+
cmd = GitlabCtl::Util.run_command(command.join(" "), live: true, input: json_attributes)
41+
remove_old_node_state
42+
Kernel.exit 1 unless cmd.status.success?
43+
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
#
13+
14+
# For testing purposes, if the first path cannot be found load the second
15+
begin
16+
require_relative '../../../../cookbooks/package/libraries/helpers/secrets_helper'
17+
rescue LoadError
18+
require_relative '../../../gitlab-cookbooks/package/libraries/helpers/secrets_helper'
19+
end
20+
21+
module GitlabCtl
22+
class GenerateSecrets
23+
USAGE ||= <<~EOS.freeze
24+
Usage:
25+
gitlab-ctl generate-secrets -f|--file FILE
26+
27+
f, --file=FILE Output secrets to file
28+
EOS
29+
class << self
30+
def parse_options(args)
31+
options = {}
32+
33+
OptionParser.new do |opts|
34+
opts.on('-fFILE', '--file=FILE', "Output secrets to file )") do |f|
35+
options[:secrets_path] = f
36+
end
37+
opts.on('-h', '--help', 'Usage help') do
38+
Kernel.puts USAGE
39+
Kernel.exit 0
40+
end
41+
end.parse!(args)
42+
raise OptionParser::ParseError, "Option --file must be specified." unless options.key?(:secrets_path)
43+
44+
options
45+
end
46+
end
47+
end
48+
end

files/gitlab-ctl-commands/lib/gitlab_ctl/util.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ def get_command_output(command, user = nil, timeout = nil)
2323
shell_out.stdout
2424
end
2525

26-
def run_command(command, live: false, user: nil, timeout: nil, env: {})
26+
def run_command(command, live: false, user: nil, timeout: nil, env: {}, input: nil)
2727
timeout = Mixlib::ShellOut::DEFAULT_READ_TIMEOUT if timeout.nil?
2828
shell_out = Mixlib::ShellOut.new(command, timeout: timeout, environment: env)
2929
shell_out.user = user unless user.nil?
30+
shell_out.input = input if input
3031
shell_out.live_stdout = $stdout if live
3132
shell_out.live_stderr = $stderr if live
3233
shell_out.run_command
@@ -172,6 +173,10 @@ def public_attributes_broken?(attribute_key = "gitlab")
172173

173174
!get_public_node_attributes.key?(attribute_key)
174175
end
176+
177+
def master_cookbook
178+
File.directory?('/opt/gitlab/embedded/cookbooks/gitlab-ee') ? 'gitlab-ee' : 'gitlab'
179+
end
175180
end
176181
end
177182
end
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# These tests confirm that calling the generate_secrets recipe works correctly
2+
# both when using the default path and and when using anoptional path to the
3+
# secrets file. It tests that the file is created and contains secrets. It does
4+
# not exhaustively test the resulting secrets file contents. Those tests are
5+
# left to secrets_helper_spec.rb.
6+
7+
require 'chef_helper'
8+
require_relative '../../../../../files/gitlab-cookbooks/package/libraries/helpers/secrets_helper'
9+
10+
RSpec.describe 'generate_secrets' do
11+
let(:chef_runner) { ChefSpec::SoloRunner.new }
12+
let(:chef_run) { chef_runner.converge('gitlab::generate_secrets') }
13+
let(:node) { chef_runner.node }
14+
15+
optional_path = '/etc/mygitlab/mysecrets.json'.freeze
16+
hex_key = /\h{128}/.freeze
17+
rsa_key = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m.freeze
18+
default_secrets_error_regexp = %r{You have enabled writing to the default secrets file location with package\['generate_secrets_json_file'].*}
19+
20+
def stub_gitlab_secrets_json(secrets)
21+
allow(File).to receive(:read).with(SecretsHelper::SECRETS_FILE).and_return(JSON.generate(secrets))
22+
end
23+
24+
def stub_check_secrets
25+
rails_keys = new_secrets['gitlab_rails']
26+
hex_keys = rails_keys.values_at('db_key_base', 'otp_key_base', 'secret_key_base', 'encrypted_settings_key_base')
27+
rsa_keys = rails_keys.values_at('openid_connect_signing_key', 'ci_jwt_signing_key')
28+
29+
expect(rails_keys.to_a.uniq).to eq(rails_keys.to_a)
30+
expect(hex_keys).to all(match(hex_key))
31+
expect(rsa_keys).to all(match(rsa_key))
32+
end
33+
34+
before do
35+
allow(File).to receive(:directory?).and_call_original
36+
allow(File).to receive(:exist?).and_call_original
37+
allow(File).to receive(:read).and_call_original
38+
allow(File).to receive(:open).and_call_original
39+
end
40+
41+
context 'when default directory does not exist' do
42+
it 'does not write secrets to the file' do
43+
allow(File).to receive(:directory?).with(File.dirname(SecretsHelper::SECRETS_FILE)).and_return(false)
44+
expect(File).not_to receive(:open).with(SecretsHelper::SECRETS_FILE, 'w')
45+
46+
chef_run
47+
end
48+
end
49+
50+
context 'when optional path directory does not exist' do
51+
it 'does not write secrets to the file' do
52+
allow(File).to receive(:directory?).with(File.dirname(optional_path)).and_return(false)
53+
expect(File).not_to receive(:open).with(optional_path, 'w')
54+
55+
node.normal[SecretsHelper::SECRETS_FILE_CHEF_ATTR] = optional_path
56+
chef_run
57+
end
58+
end
59+
60+
context 'when optional path directory does exists and we request the optional secret path' do
61+
let(:file) { double(:file) }
62+
let(:new_secrets) { @new_secrets }
63+
before do
64+
allow(SecretsHelper).to receive(:system)
65+
allow(File).to receive(:directory?).with(File.dirname(optional_path)).and_return(true)
66+
67+
allow(file).to receive(:puts) { |json| @new_secrets = JSON.parse(json) }
68+
allow(file).to receive(:chmod).and_return(true)
69+
end
70+
71+
context 'when there are no existing secrets and generate_secrets_json_file = false' do
72+
before do
73+
allow(File).to receive(:open).with(optional_path, 'w', 0600).and_yield(file).once
74+
75+
node.normal[SecretsHelper::SECRETS_FILE_CHEF_ATTR] = optional_path
76+
node.normal['package']['generate_secrets_json_file'] = false
77+
chef_run
78+
end
79+
80+
it 'writes new secrets to the file, with different values for each' do
81+
rails_keys = new_secrets['gitlab_rails']
82+
hex_keys = rails_keys.values_at('db_key_base', 'otp_key_base', 'secret_key_base', 'encrypted_settings_key_base')
83+
rsa_keys = rails_keys.values_at('openid_connect_signing_key', 'ci_jwt_signing_key')
84+
85+
expect(rails_keys.to_a.uniq).to eq(rails_keys.to_a)
86+
expect(hex_keys).to all(match(hex_key))
87+
expect(rsa_keys).to all(match(rsa_key))
88+
end
89+
end
90+
end
91+
92+
context 'when there are no existing secrets and generate_secrets_json_file = true' do
93+
before do
94+
allow(File).to receive(:directory?).with(optional_path).and_return(false)
95+
allow(File).to receive(:exist?).with(File.dirname(optional_path)).and_return(true)
96+
node.normal[SecretsHelper::SECRETS_FILE_CHEF_ATTR] = optional_path
97+
node.normal['package']['generate_secrets_json_file'] = true
98+
end
99+
100+
it 'does not write secrets to the file' do
101+
expect(File).not_to receive(:directory?).with(File.dirname(optional_path))
102+
expect(File).not_to receive(:open).with(optional_path, 'w')
103+
expect(LoggingHelper).to receive(:warning).with(default_secrets_error_regexp)
104+
105+
chef_run
106+
end
107+
end
108+
109+
context 'when there are no existing secrets and generate_secrets_json_file is not set' do
110+
before do
111+
allow(File).to receive(:directory?).with(optional_path).and_return(false)
112+
allow(File).to receive(:exist?).with(File.dirname(optional_path)).and_return(true)
113+
node.normal[SecretsHelper::SECRETS_FILE_CHEF_ATTR] = optional_path
114+
end
115+
116+
it 'does not write secrets to the file' do
117+
expect(File).not_to receive(:directory?).with(File.dirname(optional_path))
118+
expect(File).not_to receive(:open).with(optional_path, 'w')
119+
expect(LoggingHelper).to receive(:warning).with(default_secrets_error_regexp)
120+
121+
chef_run
122+
end
123+
end
124+
end

0 commit comments

Comments
 (0)