openvoxserver-ca 3.0.0.pre.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/dependabot.yml +17 -0
- data/.github/release.yml +41 -0
- data/.github/workflows/gem_release.yaml +106 -0
- data/.github/workflows/prepare_release.yml +28 -0
- data/.github/workflows/release.yml +28 -0
- data/.github/workflows/unit_tests.yaml +45 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +15 -0
- data/CODEOWNERS +4 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +15 -0
- data/Gemfile +20 -0
- data/LICENSE +202 -0
- data/README.md +118 -0
- data/Rakefile +30 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/puppetserver-ca +10 -0
- data/lib/puppetserver/ca/action/clean.rb +109 -0
- data/lib/puppetserver/ca/action/delete.rb +286 -0
- data/lib/puppetserver/ca/action/enable.rb +140 -0
- data/lib/puppetserver/ca/action/generate.rb +330 -0
- data/lib/puppetserver/ca/action/import.rb +196 -0
- data/lib/puppetserver/ca/action/list.rb +253 -0
- data/lib/puppetserver/ca/action/migrate.rb +97 -0
- data/lib/puppetserver/ca/action/prune.rb +289 -0
- data/lib/puppetserver/ca/action/revoke.rb +108 -0
- data/lib/puppetserver/ca/action/setup.rb +188 -0
- data/lib/puppetserver/ca/action/sign.rb +146 -0
- data/lib/puppetserver/ca/certificate_authority.rb +418 -0
- data/lib/puppetserver/ca/cli.rb +145 -0
- data/lib/puppetserver/ca/config/puppet.rb +309 -0
- data/lib/puppetserver/ca/config/puppetserver.rb +84 -0
- data/lib/puppetserver/ca/errors.rb +40 -0
- data/lib/puppetserver/ca/host.rb +176 -0
- data/lib/puppetserver/ca/local_certificate_authority.rb +304 -0
- data/lib/puppetserver/ca/logger.rb +49 -0
- data/lib/puppetserver/ca/stub.rb +17 -0
- data/lib/puppetserver/ca/utils/cli_parsing.rb +67 -0
- data/lib/puppetserver/ca/utils/config.rb +61 -0
- data/lib/puppetserver/ca/utils/file_system.rb +109 -0
- data/lib/puppetserver/ca/utils/http_client.rb +232 -0
- data/lib/puppetserver/ca/utils/inventory.rb +84 -0
- data/lib/puppetserver/ca/utils/signing_digest.rb +27 -0
- data/lib/puppetserver/ca/version.rb +5 -0
- data/lib/puppetserver/ca/x509_loader.rb +170 -0
- data/lib/puppetserver/ca.rb +7 -0
- data/openvoxserver-ca.gemspec +31 -0
- data/tasks/spec.rake +15 -0
- data/tasks/vox.rake +19 -0
- metadata +154 -0
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
require 'puppetserver/ca/config/puppet'
|
4
|
+
require 'puppetserver/ca/errors'
|
5
|
+
require 'puppetserver/ca/local_certificate_authority'
|
6
|
+
require 'puppetserver/ca/utils/cli_parsing'
|
7
|
+
require 'puppetserver/ca/utils/file_system'
|
8
|
+
require 'puppetserver/ca/utils/signing_digest'
|
9
|
+
|
10
|
+
module Puppetserver
|
11
|
+
module Ca
|
12
|
+
module Action
|
13
|
+
class Enable
|
14
|
+
include Puppetserver::Ca::Utils
|
15
|
+
|
16
|
+
SUMMARY = "Setup infrastructure CRL based on a node inventory."
|
17
|
+
BANNER = <<-BANNER
|
18
|
+
Usage:
|
19
|
+
puppetserver ca enable [--help]
|
20
|
+
puppetserver ca enable [--infracrl]
|
21
|
+
|
22
|
+
Description:
|
23
|
+
Performs actions necessary to enable certain CA modes.
|
24
|
+
|
25
|
+
--infracrl
|
26
|
+
Creates auxiliary files necessary to use the infrastructure-only CRL.
|
27
|
+
Assumes the existence of an `infra_inventory.txt` file in the CA
|
28
|
+
directory listing the certnames of the infrastructure nodes in the
|
29
|
+
Puppet installation. Generates the the empty CRL to be populated with
|
30
|
+
revoked infrastructure nodes.
|
31
|
+
|
32
|
+
Options:
|
33
|
+
BANNER
|
34
|
+
|
35
|
+
def initialize(logger)
|
36
|
+
@logger = logger
|
37
|
+
end
|
38
|
+
|
39
|
+
def run(input)
|
40
|
+
# Validate config_path provided
|
41
|
+
config_path = input['config']
|
42
|
+
if config_path
|
43
|
+
errors = FileSystem.validate_file_paths(config_path)
|
44
|
+
return 1 if Errors.handle_with_usage(@logger, errors)
|
45
|
+
end
|
46
|
+
|
47
|
+
puppet = Config::Puppet.new(config_path)
|
48
|
+
puppet.load(logger: @logger)
|
49
|
+
settings = puppet.settings
|
50
|
+
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
51
|
+
|
52
|
+
if input['infracrl']
|
53
|
+
errors = enable_infra_crl(settings)
|
54
|
+
return 1 if Errors.handle_with_usage(@logger, errors)
|
55
|
+
end
|
56
|
+
|
57
|
+
return 0
|
58
|
+
end
|
59
|
+
|
60
|
+
def enable_infra_crl(settings)
|
61
|
+
inventory_file = File.join(settings[:cadir], 'infra_inventory.txt')
|
62
|
+
if !File.exist?(inventory_file)
|
63
|
+
error = <<-ERR
|
64
|
+
Please create an inventory file at '#{inventory_file}' with the certnames of your
|
65
|
+
infrastructure nodes before proceeding with infra CRL setup!"
|
66
|
+
ERR
|
67
|
+
return [error]
|
68
|
+
end
|
69
|
+
|
70
|
+
infra_crl = File.join(settings[:cadir], 'infra_crl.pem')
|
71
|
+
|
72
|
+
file_errors = check_for_existing_infra_files(infra_crl)
|
73
|
+
return file_errors if !file_errors.empty?
|
74
|
+
|
75
|
+
errors = create_infra_crl_chain(settings)
|
76
|
+
return errors if !errors.empty?
|
77
|
+
|
78
|
+
@logger.inform "Infra CRL files created."
|
79
|
+
return []
|
80
|
+
end
|
81
|
+
|
82
|
+
def check_for_existing_infra_files(files)
|
83
|
+
file_errors = FileSystem.check_for_existing_files(files)
|
84
|
+
if !file_errors.empty?
|
85
|
+
notice = <<-MSG
|
86
|
+
If you would really like to reinitialize your infrastructure CRL, please delete
|
87
|
+
the existing files and run this command again.
|
88
|
+
MSG
|
89
|
+
file_errors << notice
|
90
|
+
end
|
91
|
+
return file_errors
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_infra_crl_chain(settings)
|
95
|
+
# Load most secure signing digest we can for cers/crl/csr signing.
|
96
|
+
signer = SigningDigest.new
|
97
|
+
return signer.errors if signer.errors.any?
|
98
|
+
|
99
|
+
ca = LocalCertificateAuthority.new(signer.digest, settings)
|
100
|
+
return ca.errors if ca.errors.any?
|
101
|
+
|
102
|
+
infra_crl = ca.create_crl_for(ca.cert, ca.key)
|
103
|
+
|
104
|
+
# Drop the full leaf CRL from the chain
|
105
|
+
crl_chain = ca.crl_chain.drop(1)
|
106
|
+
# Add the new clean CRL, that will be populated with infra nodes only
|
107
|
+
# as they are revoked
|
108
|
+
crl_chain.unshift(infra_crl)
|
109
|
+
FileSystem.write_file(File.join(settings[:cadir], 'infra_crl.pem'), crl_chain, 0644)
|
110
|
+
|
111
|
+
[]
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse(cli_args)
|
115
|
+
results = {}
|
116
|
+
parser = self.class.parser(results)
|
117
|
+
errors = CliParsing.parse_with_errors(parser, cli_args)
|
118
|
+
errors_were_handled = Errors.handle_with_usage(@logger, errors, parser.help)
|
119
|
+
exit_code = errors_were_handled ? 1 : nil
|
120
|
+
return results, exit_code
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.parser(parsed = {})
|
124
|
+
OptionParser.new do |opts|
|
125
|
+
opts.banner = BANNER
|
126
|
+
opts.on('--help', 'Display this command-specific help output') do |help|
|
127
|
+
parsed['help'] = true
|
128
|
+
end
|
129
|
+
opts.on('--config CONF', 'Path to puppet.conf') do |conf|
|
130
|
+
parsed['config'] = conf
|
131
|
+
end
|
132
|
+
opts.on('--infracrl', "Create auxiliary files for the infrastructure-only CRL.") do |infracrl|
|
133
|
+
parsed['infracrl'] = true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,330 @@
|
|
1
|
+
require 'puppetserver/ca/certificate_authority'
|
2
|
+
require 'puppetserver/ca/config/puppet'
|
3
|
+
require 'puppetserver/ca/errors'
|
4
|
+
require 'puppetserver/ca/host'
|
5
|
+
require 'puppetserver/ca/local_certificate_authority'
|
6
|
+
require 'puppetserver/ca/utils/cli_parsing'
|
7
|
+
require 'puppetserver/ca/utils/config'
|
8
|
+
require 'puppetserver/ca/utils/file_system'
|
9
|
+
require 'puppetserver/ca/utils/signing_digest'
|
10
|
+
require 'puppetserver/ca/x509_loader'
|
11
|
+
|
12
|
+
module Puppetserver
|
13
|
+
module Ca
|
14
|
+
module Action
|
15
|
+
class Generate
|
16
|
+
|
17
|
+
include Puppetserver::Ca::Utils
|
18
|
+
|
19
|
+
# Only allow printing ascii characters, excluding /
|
20
|
+
VALID_CERTNAME = /\A[ -.0-~]+\Z/
|
21
|
+
CERTNAME_BLOCKLIST = %w{--all --config}
|
22
|
+
|
23
|
+
SUMMARY = "Generate a new certificate signed by the CA"
|
24
|
+
BANNER = <<-BANNER
|
25
|
+
Usage:
|
26
|
+
puppetserver ca generate [--help]
|
27
|
+
puppetserver ca generate --certname NAME[,NAME] [--config PATH]
|
28
|
+
[--subject-alt-names NAME[,NAME]]
|
29
|
+
[--ca-client [--force]]
|
30
|
+
|
31
|
+
Description:
|
32
|
+
Generates a new certificate signed by the intermediate CA
|
33
|
+
and stores generated keys and certs on disk.
|
34
|
+
|
35
|
+
If the `--ca-client` flag is passed, the cert will be generated
|
36
|
+
offline, without using Puppet Server's signing code, and will add
|
37
|
+
a special extension authorizing it to talk to the CA API. This can
|
38
|
+
be used for regenerating the server's host cert, or for manually
|
39
|
+
setting up other nodes to be CA clients. Do not distribute certs
|
40
|
+
generated this way to any node that you do not intend to have
|
41
|
+
administrative access to the CA (e.g. the ability to sign a cert).
|
42
|
+
|
43
|
+
Since the `--ca-client` causes a cert to be generated offline, it
|
44
|
+
should ONLY be used when Puppet Server is NOT running, to avoid
|
45
|
+
conflicting with the actions of the CA service. This will be
|
46
|
+
mandatory in a future release.
|
47
|
+
|
48
|
+
Options:
|
49
|
+
BANNER
|
50
|
+
def initialize(logger)
|
51
|
+
@logger = logger
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.parser(parsed = {})
|
55
|
+
parsed['certnames'] = []
|
56
|
+
parsed['subject-alt-names'] = ''
|
57
|
+
OptionParser.new do |opts|
|
58
|
+
opts.banner = BANNER
|
59
|
+
opts.on('--certname NAME[,NAME]', Array,
|
60
|
+
'One or more comma separated certnames') do |certs|
|
61
|
+
parsed['certnames'] += certs
|
62
|
+
end
|
63
|
+
opts.on('--help', 'Display this command-specific help output') do |help|
|
64
|
+
parsed['help'] = true
|
65
|
+
end
|
66
|
+
opts.on('--config CONF', 'Path to puppet.conf') do |conf|
|
67
|
+
parsed['config'] = conf
|
68
|
+
end
|
69
|
+
opts.on('--subject-alt-names NAME[,NAME]',
|
70
|
+
'Subject alternative names for the generated cert') do |sans|
|
71
|
+
parsed['subject-alt-names'] = sans
|
72
|
+
end
|
73
|
+
opts.on('--ca-client',
|
74
|
+
'Whether this cert will be used to request CA actions.',
|
75
|
+
'Causes the cert to be generated offline.') do |ca_client|
|
76
|
+
parsed['ca-client'] = true
|
77
|
+
end
|
78
|
+
opts.on('--force', 'Suppress errors when signing cert offline.',
|
79
|
+
"To be used with '--ca-client'") do |force|
|
80
|
+
parsed['force'] = true
|
81
|
+
end
|
82
|
+
opts.on('--ttl TTL', 'The time-to-live for each cert generated and signed') do |ttl|
|
83
|
+
parsed['ttl'] = ttl
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse(args)
|
89
|
+
results = {}
|
90
|
+
parser = self.class.parser(results)
|
91
|
+
|
92
|
+
errors = CliParsing.parse_with_errors(parser, args)
|
93
|
+
|
94
|
+
if results['certnames'].empty?
|
95
|
+
errors << ' At least one certname is required to generate'
|
96
|
+
else
|
97
|
+
results['certnames'].each do |certname|
|
98
|
+
if CERTNAME_BLOCKLIST.include?(certname)
|
99
|
+
errors << " Cannot manage cert named `#{certname}` from " +
|
100
|
+
"the CLI, if needed use the HTTP API directly"
|
101
|
+
end
|
102
|
+
|
103
|
+
if certname.match(/\p{Upper}/)
|
104
|
+
errors << " Certificate names must be lower case"
|
105
|
+
end
|
106
|
+
|
107
|
+
unless certname =~ VALID_CERTNAME
|
108
|
+
errors << " Certname #{certname} must not contain unprintable or non-ASCII characters"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
errors_were_handled = Errors.handle_with_usage(@logger, errors, parser.help)
|
114
|
+
|
115
|
+
exit_code = errors_were_handled ? 1 : nil
|
116
|
+
|
117
|
+
return results, exit_code
|
118
|
+
end
|
119
|
+
|
120
|
+
def run(input)
|
121
|
+
certnames = input['certnames']
|
122
|
+
config_path = input['config']
|
123
|
+
|
124
|
+
# Validate config_path provided
|
125
|
+
if config_path
|
126
|
+
errors = FileSystem.validate_file_paths(config_path)
|
127
|
+
return 1 if Errors.handle_with_usage(@logger, errors)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Load, resolve, and validate puppet config settings
|
131
|
+
settings_overrides = {}
|
132
|
+
puppet = Config::Puppet.new(config_path)
|
133
|
+
puppet.load(cli_overrides: settings_overrides, logger: @logger)
|
134
|
+
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
135
|
+
|
136
|
+
# We don't want generate to respect the alt names setting, since it is usually
|
137
|
+
# used to generate certs for other nodes
|
138
|
+
alt_names = input['subject-alt-names']
|
139
|
+
|
140
|
+
# Load most secure signing digest we can for csr signing.
|
141
|
+
signer = SigningDigest.new
|
142
|
+
return 1 if Errors.handle_with_usage(@logger, signer.errors)
|
143
|
+
|
144
|
+
# Generate and save certs and associated keys
|
145
|
+
if input['ca-client']
|
146
|
+
# Refuse to generate certs offline if the CA service is running
|
147
|
+
begin
|
148
|
+
return 1 if HttpClient.check_server_online(puppet.settings, @logger)
|
149
|
+
rescue Puppetserver::Ca::ConnectionFailed => e
|
150
|
+
base_message = "Could not determine whether Puppet Server is online."
|
151
|
+
if input['force']
|
152
|
+
@logger.inform("#{base_message} Connection check failed with " \
|
153
|
+
"error: #{e.wrapped}\nContinuing with certificate signing.")
|
154
|
+
else
|
155
|
+
@logger.inform("#{base_message} If you are certain that the " \
|
156
|
+
"Puppetserver service is stopped, run this command again " \
|
157
|
+
"with the '--force' flag.")
|
158
|
+
raise e
|
159
|
+
end
|
160
|
+
end
|
161
|
+
all_passed = generate_authorized_certs(certnames, alt_names, puppet.settings, signer.digest)
|
162
|
+
else
|
163
|
+
all_passed = generate_certs(certnames, alt_names, puppet.settings, signer.digest, input['ttl'])
|
164
|
+
end
|
165
|
+
return all_passed ? 0 : 1
|
166
|
+
end
|
167
|
+
|
168
|
+
# Certs authorized to talk to the CA API need to be signed offline,
|
169
|
+
# in order to securely add the special auth extension.
|
170
|
+
def generate_authorized_certs(certnames, alt_names, settings, digest)
|
171
|
+
# Make sure we have all the directories where we will be writing files
|
172
|
+
FileSystem.ensure_dirs([settings[:ssldir],
|
173
|
+
settings[:certdir],
|
174
|
+
settings[:privatekeydir],
|
175
|
+
settings[:publickeydir]])
|
176
|
+
|
177
|
+
ca = Puppetserver::Ca::LocalCertificateAuthority.new(digest, settings)
|
178
|
+
return false if Errors.handle_with_usage(@logger, ca.errors)
|
179
|
+
|
180
|
+
passed = certnames.map do |certname|
|
181
|
+
errors = check_for_existing_ssl_files(certname, settings)
|
182
|
+
next false if Errors.handle_with_usage(@logger, errors)
|
183
|
+
|
184
|
+
current_alt_names = process_alt_names(alt_names, certname)
|
185
|
+
|
186
|
+
# For certs signed offline, any alt names are added directly to the cert,
|
187
|
+
# rather than to the CSR.
|
188
|
+
key, csr = generate_key_csr(certname, settings, digest)
|
189
|
+
next false unless csr
|
190
|
+
|
191
|
+
cert = ca.sign_authorized_cert(csr, current_alt_names)
|
192
|
+
next false unless save_file(cert.to_pem, certname, settings[:certdir], "Certificate")
|
193
|
+
next false unless save_file(cert.to_pem, certname, settings[:signeddir], "Certificate")
|
194
|
+
next false unless save_keys(certname, settings, key)
|
195
|
+
ca.update_serial_file(cert.serial + 1)
|
196
|
+
true
|
197
|
+
end
|
198
|
+
passed.all?
|
199
|
+
end
|
200
|
+
|
201
|
+
# Generate csrs and keys, then submit them to CA, request for the CA to sign
|
202
|
+
# them, download the signed certificates from the CA, and finally save
|
203
|
+
# the signed certs and associated keys. Returns true if all certs were
|
204
|
+
# successfully created and saved. Takes a ttl to use if certificates
|
205
|
+
# are signed by this CLI, not autosigned by the CA. if ttl is nil, uses
|
206
|
+
# the CA's settings.
|
207
|
+
def generate_certs(certnames, alt_names, settings, digest, ttl)
|
208
|
+
# Make sure we have all the directories where we will be writing files
|
209
|
+
FileSystem.ensure_dirs([settings[:ssldir],
|
210
|
+
settings[:certdir],
|
211
|
+
settings[:privatekeydir],
|
212
|
+
settings[:publickeydir]])
|
213
|
+
|
214
|
+
ca = Puppetserver::Ca::CertificateAuthority.new(@logger, settings)
|
215
|
+
|
216
|
+
passed = certnames.map do |certname|
|
217
|
+
errors = check_for_existing_ssl_files(certname, settings)
|
218
|
+
next false if Errors.handle_with_usage(@logger, errors)
|
219
|
+
|
220
|
+
current_alt_names = process_alt_names(alt_names, certname)
|
221
|
+
|
222
|
+
next false unless submit_csr(certname, ca, settings, digest, current_alt_names)
|
223
|
+
|
224
|
+
# Check if the CA autosigned the cert
|
225
|
+
next acquire_signed_cert(ca, certname, settings, ttl)
|
226
|
+
end
|
227
|
+
passed.all?
|
228
|
+
end
|
229
|
+
|
230
|
+
# Try to download a signed certificate; sign the cert with the given ttl if it needs
|
231
|
+
# signing before download.
|
232
|
+
def acquire_signed_cert(ca, certname, settings, ttl)
|
233
|
+
if download_cert(ca, certname, settings)
|
234
|
+
@logger.inform "Certificate for #{certname} was autosigned."
|
235
|
+
if ttl
|
236
|
+
@logger.warn "ttl was specified, but the CA autosigned the CSR. Unable to specify #{ttl} for #{certname}"
|
237
|
+
end
|
238
|
+
true
|
239
|
+
else
|
240
|
+
false unless ca.sign_certs([certname], ttl)
|
241
|
+
download_cert(ca, certname, settings)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def submit_csr(certname, ca, settings, digest, alt_names)
|
246
|
+
key, csr = generate_key_csr(certname, settings, digest, alt_names)
|
247
|
+
return false unless csr
|
248
|
+
# Always save the keys, since soemtimes the server saves the CSR
|
249
|
+
# even when it returns a 400 (e.g. when the CSR contains alt names
|
250
|
+
# but the server isn't configured to sign such certs)
|
251
|
+
return false unless save_keys(certname, settings, key)
|
252
|
+
return false unless ca.submit_certificate_request(certname, csr)
|
253
|
+
true
|
254
|
+
end
|
255
|
+
|
256
|
+
def download_cert(ca, certname, settings)
|
257
|
+
if result = ca.get_certificate(certname)
|
258
|
+
return false unless save_file(result.body, certname, settings[:certdir], "Certificate")
|
259
|
+
true
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# For certs signed offline, any alt names are added directly to the cert,
|
264
|
+
# rather than to the CSR.
|
265
|
+
def generate_key_csr(certname, settings, digest, alt_names = '')
|
266
|
+
host = Puppetserver::Ca::Host.new(digest)
|
267
|
+
private_key = host.create_private_key(settings[:keylength])
|
268
|
+
extensions = []
|
269
|
+
if !alt_names.empty?
|
270
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
271
|
+
extensions << ef.create_extension("subjectAltName",
|
272
|
+
alt_names,
|
273
|
+
false)
|
274
|
+
end
|
275
|
+
csr = host.create_csr(name: certname,
|
276
|
+
key: private_key,
|
277
|
+
cli_extensions: extensions,
|
278
|
+
csr_attributes_path: settings[:csr_attributes])
|
279
|
+
return if Errors.handle_with_usage(@logger, host.errors)
|
280
|
+
|
281
|
+
return private_key, csr
|
282
|
+
end
|
283
|
+
|
284
|
+
def save_keys(certname, settings, key)
|
285
|
+
public_key = key.public_key
|
286
|
+
return false unless save_file(key, certname, settings[:privatekeydir], "Private key")
|
287
|
+
return false unless save_file(public_key, certname, settings[:publickeydir], "Public key")
|
288
|
+
true
|
289
|
+
end
|
290
|
+
|
291
|
+
def save_file(content, certname, dir, type)
|
292
|
+
location = File.join(dir, "#{certname}.pem")
|
293
|
+
if File.exist?(location)
|
294
|
+
@logger.err "#{type} #{certname}.pem already exists. Please delete it if you really want to regenerate it."
|
295
|
+
false
|
296
|
+
else
|
297
|
+
FileSystem.write_file(location, content, 0640)
|
298
|
+
@logger.inform "Successfully saved #{type.downcase} for #{certname} to #{location}"
|
299
|
+
true
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def check_for_existing_ssl_files(certname, settings)
|
304
|
+
files = [ File.join(settings[:certdir], "#{certname}.pem"),
|
305
|
+
File.join(settings[:privatekeydir], "#{certname}.pem"),
|
306
|
+
File.join(settings[:publickeydir], "#{certname}.pem"),
|
307
|
+
File.join(settings[:signeddir], "#{certname}.pem"), ]
|
308
|
+
errors = Puppetserver::Ca::Utils::FileSystem.check_for_existing_files(files)
|
309
|
+
if !errors.empty?
|
310
|
+
errors << "Please delete these files if you really want to generate a new cert for #{certname}."
|
311
|
+
end
|
312
|
+
errors
|
313
|
+
end
|
314
|
+
|
315
|
+
def process_alt_names(alt_names, certname)
|
316
|
+
# It is recommended (and sometimes enforced) to always include
|
317
|
+
# the certname as a SAN, see RFC 2818 https://tools.ietf.org/html/rfc2818#section-3.1.
|
318
|
+
return "DNS:#{certname}" if alt_names.empty?
|
319
|
+
|
320
|
+
current_alt_names = alt_names.dup
|
321
|
+
# When validating the cert, OpenSSL will ignore the CN field if
|
322
|
+
# altnames are present, so we need to ensure that the certname is
|
323
|
+
# also listed among the alt names.
|
324
|
+
current_alt_names += ",DNS:#{certname}"
|
325
|
+
current_alt_names = Puppetserver::Ca::Utils::Config.munge_alt_names(current_alt_names)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
require 'puppetserver/ca/config/puppet'
|
4
|
+
require 'puppetserver/ca/errors'
|
5
|
+
require 'puppetserver/ca/local_certificate_authority'
|
6
|
+
require 'puppetserver/ca/utils/cli_parsing'
|
7
|
+
require 'puppetserver/ca/utils/config'
|
8
|
+
require 'puppetserver/ca/utils/file_system'
|
9
|
+
require 'puppetserver/ca/utils/signing_digest'
|
10
|
+
require 'puppetserver/ca/x509_loader'
|
11
|
+
|
12
|
+
module Puppetserver
|
13
|
+
module Ca
|
14
|
+
module Action
|
15
|
+
class Import
|
16
|
+
include Puppetserver::Ca::Utils
|
17
|
+
|
18
|
+
SUMMARY = "Import an external CA chain and generate server PKI"
|
19
|
+
BANNER = <<-BANNER
|
20
|
+
Usage:
|
21
|
+
puppetserver ca import [--help]
|
22
|
+
puppetserver ca import [--config PATH] [--certname NAME]
|
23
|
+
[--subject-alt-names NAME[,NAME]]
|
24
|
+
--private-key PATH --cert-bundle PATH --crl-chain PATH
|
25
|
+
|
26
|
+
Description:
|
27
|
+
Given a private key, cert bundle, and a crl chain,
|
28
|
+
validate and import to the Puppet Server CA.
|
29
|
+
|
30
|
+
Note that the cert and crl provided for the leaf CA must not
|
31
|
+
have already issued or revoked any certificates.
|
32
|
+
|
33
|
+
Options:
|
34
|
+
BANNER
|
35
|
+
|
36
|
+
def initialize(logger)
|
37
|
+
@logger = logger
|
38
|
+
end
|
39
|
+
|
40
|
+
def run(input)
|
41
|
+
bundle_path = input['cert-bundle']
|
42
|
+
key_path = input['private-key']
|
43
|
+
chain_path = input['crl-chain']
|
44
|
+
config_path = input['config']
|
45
|
+
|
46
|
+
files = [bundle_path, key_path, chain_path, config_path].compact
|
47
|
+
|
48
|
+
errors = FileSystem.validate_file_paths(files)
|
49
|
+
return 1 if Errors.handle_with_usage(@logger, errors)
|
50
|
+
|
51
|
+
loader = X509Loader.new(bundle_path, key_path, chain_path)
|
52
|
+
return 1 if Errors.handle_with_usage(@logger, loader.errors)
|
53
|
+
|
54
|
+
settings_overrides = {}
|
55
|
+
settings_overrides[:certname] = input['certname'] unless input['certname'].empty?
|
56
|
+
settings_overrides[:dns_alt_names] = input['subject-alt-names'] unless input['subject-alt-names'].empty?
|
57
|
+
|
58
|
+
puppet = Config::Puppet.new(config_path)
|
59
|
+
puppet.load(cli_overrides: settings_overrides, logger: @logger)
|
60
|
+
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
61
|
+
|
62
|
+
# Load most secure signing digest we can for cers/crl/csr signing.
|
63
|
+
signer = SigningDigest.new
|
64
|
+
return 1 if Errors.handle_with_usage(@logger, signer.errors)
|
65
|
+
|
66
|
+
errors = import(loader, puppet.settings, signer.digest)
|
67
|
+
return 1 if Errors.handle_with_usage(@logger, errors)
|
68
|
+
|
69
|
+
@logger.inform "Import succeeded. Find your files in #{puppet.settings[:cadir]}"
|
70
|
+
return 0
|
71
|
+
end
|
72
|
+
|
73
|
+
def import(loader, settings, signing_digest)
|
74
|
+
ca = Puppetserver::Ca::LocalCertificateAuthority.new(signing_digest, settings)
|
75
|
+
ca.initialize_ssl_components(loader)
|
76
|
+
server_key, server_cert = ca.create_server_cert
|
77
|
+
return ca.errors if ca.errors.any?
|
78
|
+
|
79
|
+
FileSystem.ensure_dirs([settings[:ssldir],
|
80
|
+
settings[:cadir],
|
81
|
+
settings[:certdir],
|
82
|
+
settings[:privatekeydir],
|
83
|
+
settings[:publickeydir],
|
84
|
+
settings[:signeddir]])
|
85
|
+
|
86
|
+
public_files = [
|
87
|
+
[settings[:cacert], loader.certs],
|
88
|
+
[settings[:cacrl], loader.crls],
|
89
|
+
[settings[:cadir] + '/infra_crl.pem', loader.crls],
|
90
|
+
[settings[:localcacert], loader.certs],
|
91
|
+
[settings[:hostcrl], loader.crls],
|
92
|
+
[settings[:hostpubkey], server_key.public_key],
|
93
|
+
[settings[:hostcert], server_cert],
|
94
|
+
[settings[:cert_inventory], ca.inventory_entry(server_cert)],
|
95
|
+
[settings[:capub], loader.key.public_key],
|
96
|
+
[settings[:cadir] + '/infra_inventory.txt', ''],
|
97
|
+
[settings[:cadir] + '/infra_serials', ''],
|
98
|
+
[settings[:serial], "002"],
|
99
|
+
[File.join(settings[:signeddir], "#{settings[:certname]}.pem"), server_cert]
|
100
|
+
]
|
101
|
+
|
102
|
+
private_files = [
|
103
|
+
[settings[:hostprivkey], server_key],
|
104
|
+
[settings[:cakey], loader.key],
|
105
|
+
]
|
106
|
+
|
107
|
+
files_to_check = public_files + private_files
|
108
|
+
# We don't want to error if server's keys exist. Certain workflows
|
109
|
+
# allow the agent to have already be installed with keys and then
|
110
|
+
# upgraded to be a server. The host class will honor keys, if both
|
111
|
+
# public and private exist, and error if only one exists - as is
|
112
|
+
# previous behavior.
|
113
|
+
files_to_check = files_to_check.map(&:first) - [settings[:hostpubkey], settings[:hostprivkey]]
|
114
|
+
errors = FileSystem.check_for_existing_files(files_to_check)
|
115
|
+
|
116
|
+
if !errors.empty?
|
117
|
+
instructions = <<-ERR
|
118
|
+
If you would really like to replace your CA, please delete the existing files first.
|
119
|
+
Note that any certificates that were issued by this CA will become invalid if you
|
120
|
+
replace it!
|
121
|
+
ERR
|
122
|
+
errors << instructions
|
123
|
+
return errors
|
124
|
+
end
|
125
|
+
|
126
|
+
public_files.each do |location, content|
|
127
|
+
FileSystem.write_file(location, content, 0644)
|
128
|
+
end
|
129
|
+
|
130
|
+
private_files.each do |location, content|
|
131
|
+
FileSystem.write_file(location, content, 0640)
|
132
|
+
end
|
133
|
+
|
134
|
+
Puppetserver::Ca::Utils::Config.symlink_to_old_cadir(settings[:cadir], settings[:confdir])
|
135
|
+
|
136
|
+
return []
|
137
|
+
end
|
138
|
+
|
139
|
+
def check_flag_usage(results)
|
140
|
+
if results['cert-bundle'].nil? || results['private-key'].nil? || results['crl-chain'].nil?
|
141
|
+
' Missing required argument' + "\n" +
|
142
|
+
' --cert-bundle, --private-key, --crl-chain are required'
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse(args)
|
147
|
+
results = {}
|
148
|
+
parser = self.class.parser(results)
|
149
|
+
|
150
|
+
errors = CliParsing.parse_with_errors(parser, args)
|
151
|
+
|
152
|
+
if err = check_flag_usage(results)
|
153
|
+
errors << err
|
154
|
+
end
|
155
|
+
|
156
|
+
errors_were_handled = Errors.handle_with_usage(@logger, errors, parser.help)
|
157
|
+
|
158
|
+
exit_code = errors_were_handled ? 1 : nil
|
159
|
+
|
160
|
+
return results, exit_code
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.parser(parsed = {})
|
164
|
+
parsed['certname'] = ''
|
165
|
+
parsed['subject-alt-names'] = ''
|
166
|
+
OptionParser.new do |opts|
|
167
|
+
opts.banner = BANNER
|
168
|
+
opts.on('--help', 'Display this command-specific help output') do |help|
|
169
|
+
parsed['help'] = true
|
170
|
+
end
|
171
|
+
opts.on('--config CONF', 'Path to puppet.conf') do |conf|
|
172
|
+
parsed['config'] = conf
|
173
|
+
end
|
174
|
+
opts.on('--private-key KEY', 'Path to PEM encoded key') do |key|
|
175
|
+
parsed['private-key'] = key
|
176
|
+
end
|
177
|
+
opts.on('--cert-bundle BUNDLE', 'Path to PEM encoded bundle') do |bundle|
|
178
|
+
parsed['cert-bundle'] = bundle
|
179
|
+
end
|
180
|
+
opts.on('--crl-chain CHAIN', 'Path to PEM encoded chain') do |chain|
|
181
|
+
parsed['crl-chain'] = chain
|
182
|
+
end
|
183
|
+
opts.on('--certname NAME',
|
184
|
+
'Common name to use for the server cert') do |name|
|
185
|
+
parsed['certname'] = name
|
186
|
+
end
|
187
|
+
opts.on('--subject-alt-names NAME[,NAME]',
|
188
|
+
'Subject alternative names for the server cert') do |sans|
|
189
|
+
parsed['subject-alt-names'] = sans
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|