puppetserver-ca 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4aae686f8f63d4fb8c675758fd9c910f8608187a
4
- data.tar.gz: 36404974656f555dbd6253aaa6a5792b9b3b0069
3
+ metadata.gz: 9e9242f6653c1f428f154759eaeeae5d45860683
4
+ data.tar.gz: 08769d2347f84fc0c5b29dcc8e8fe4a5272c3674
5
5
  SHA512:
6
- metadata.gz: 4f43caf76164963786227c950886504fa2d3c7ffa583228cbe82256bca53c8a6867ba7c7cb3278bc604e3730f79dcb934146a22bd209642aa1a347fb5ea86811
7
- data.tar.gz: 98b4178cadbc8ec770585a97b5ce9d44a770956d5d85b839cc5553962a1377998853b93a67378519577a13f032cf1ac6bd954641dab013fbeedd480b9e369944
6
+ metadata.gz: 247107b319de6f2c00a62b0bdb9e03f4c4ff694dca4e319d9c5f88d251c800de91b68b504fb92c4d4fabf7e94110935f2fcc200cde320374bc7bf488f33d37a1
7
+ data.tar.gz: d3517342dad610a789ef54b26d17812581332abd69a61e70fd9063237820fdc0e69062557649c66ed7d92c0321922d94a28434ede370209c8de3a6285f0a4fca
data/README.md CHANGED
@@ -13,8 +13,52 @@ You may install it yourself with:
13
13
 
14
14
  ## Usage
15
15
 
16
- This is still a Work in Progress and is not intended for public usage yet.
17
- Use at your own risk!
16
+ For initial CA setup, we provide two options. These need to be run before starting
17
+ Puppet Server for the first time.
18
+
19
+ To set up a default CA, with a self-signed root cert and an intermediate signing cert:
20
+ ```
21
+ puppetserver ca setup
22
+ ```
23
+
24
+ To import a custom CA:
25
+ ```
26
+ puppetserver ca import --cert-bundle certs.pem --crl-chain crls.pem --private-key ca_key.pem
27
+ ```
28
+
29
+ The remaining actions provided by this gem require a running Puppet Server, since
30
+ it primarily uses the CA's API endpoints to do its work. The following examples
31
+ assume that you are using the gem packaged within Puppet Server.
32
+
33
+ To sign a pending certificate request:
34
+ ```
35
+ puppetserver ca sign --certname foo.example.com
36
+ ```
37
+
38
+ To list certificates and CSRs:
39
+ ```
40
+ puppetserver ca list --all
41
+ ```
42
+
43
+ To revoke a signed certificate:
44
+ ```
45
+ puppetserver ca revoke --certname foo.example.com
46
+ ```
47
+
48
+ To revoke the cert and clean up all SSL files for a given certname:
49
+ ```
50
+ puppetserver ca clean --certname foo.example.com
51
+ ```
52
+
53
+ To create a new keypair and certificate for a certname:
54
+ ```
55
+ puppetserver ca generate --certname foo.example.com
56
+ ```
57
+
58
+ For more details, see the help output:
59
+ ```
60
+ puppetserver ca --help
61
+ ```
18
62
 
19
63
  This code in this project is licensed under the Apache Software License v2,
20
64
  please see the included [License](https://github.com/puppetlabs/puppetserver-ca-cli/blob/master/LICENSE.md)
@@ -1,9 +1,12 @@
1
1
  require 'puppetserver/ca/utils/cli_parsing'
2
2
  require 'puppetserver/ca/host'
3
3
  require 'puppetserver/ca/certificate_authority'
4
+ require 'puppetserver/ca/local_certificate_authority'
5
+ require 'puppetserver/ca/x509_loader'
4
6
  require 'puppetserver/ca/config/puppet'
5
7
  require 'puppetserver/ca/utils/file_system'
6
8
  require 'puppetserver/ca/utils/signing_digest'
9
+ require 'puppetserver/ca/utils/config'
7
10
 
8
11
  module Puppetserver
9
12
  module Ca
@@ -20,13 +23,27 @@ module Puppetserver
20
23
  BANNER = <<-BANNER
21
24
  Usage:
22
25
  puppetserver ca generate [--help]
23
- puppetserver ca generate [--config PATH] [--certname NAME[,NAME]]
26
+ puppetserver ca generate --certname NAME[,NAME] [--config PATH]
24
27
  [--subject-alt-names NAME[,NAME]]
28
+ [--ca-client]
25
29
 
26
30
  Description:
27
31
  Generates a new certificate signed by the intermediate CA
28
32
  and stores generated keys and certs on disk.
29
33
 
34
+ If the `--ca-client` flag is passed, the cert will be generated
35
+ offline, without using Puppet Server's signing code, and will add
36
+ a special extension authorizing it to talk to the CA API. This can
37
+ be used for regenerating the master's host cert, or for manually
38
+ setting up other nodes to be CA clients. Do not distribute certs
39
+ generated this way to any node that you do not intend to have
40
+ administrative access to the CA (e.g. the ability to sign a cert).
41
+
42
+ Since the `--ca-client` causes a cert to be generated offline, it
43
+ should ONLY be used when Puppet Server is NOT running, to avoid
44
+ conflicting with the actions of the CA service. This will be
45
+ mandatory in a future release.
46
+
30
47
  To determine the target location, the default puppet.conf
31
48
  is consulted for custom values. If using a custom puppet.conf
32
49
  provide it with the --config flag
@@ -56,6 +73,11 @@ BANNER
56
73
  'Subject alternative names for the generated cert') do |sans|
57
74
  parsed['subject-alt-names'] = sans
58
75
  end
76
+ opts.on('--ca-client',
77
+ 'Whether this cert will be used to request CA actions.\
78
+ Causes the cert to be generated offline.') do |ca_client|
79
+ parsed['ca-client'] = true
80
+ end
59
81
  end
60
82
  end
61
83
 
@@ -103,27 +125,66 @@ BANNER
103
125
 
104
126
  # Load, resolve, and validate puppet config settings
105
127
  settings_overrides = {}
106
- # Since puppet expects the key to be called 'dns_alt_names', we need to use that here
107
- # to ensure that the overriding works correctly.
108
- settings_overrides[:dns_alt_names] = input['subject-alt-names'] unless input['subject-alt-names'].empty?
109
128
  puppet = Config::Puppet.new(config_path)
110
129
  puppet.load(settings_overrides)
111
130
  return 1 if CliParsing.handle_errors(@logger, puppet.errors)
112
131
 
132
+ # We don't want generate to respect the alt names setting, since it is usually
133
+ # used to generate certs for other nodes
134
+ alt_names = input['subject-alt-names']
135
+
113
136
  # Load most secure signing digest we can for csr signing.
114
137
  signer = SigningDigest.new
115
138
  return 1 if CliParsing.handle_errors(@logger, signer.errors)
116
139
 
117
140
  # Generate and save certs and associated keys
118
- all_passed = generate_certs(certnames, puppet.settings, signer.digest)
141
+ if input['ca-client']
142
+ all_passed = generate_authorized_certs(certnames, alt_names, puppet.settings, signer.digest)
143
+ else
144
+ all_passed = generate_certs(certnames, alt_names, puppet.settings, signer.digest)
145
+ end
119
146
  return all_passed ? 0 : 1
120
147
  end
121
148
 
149
+ # Certs authorized to talk to the CA API need to be signed offline,
150
+ # in order to securely add the special auth extension.
151
+ def generate_authorized_certs(certnames, alt_names, settings, digest)
152
+ # Make sure we have all the directories where we will be writing files
153
+ FileSystem.ensure_dirs([settings[:ssldir],
154
+ settings[:certdir],
155
+ settings[:privatekeydir],
156
+ settings[:publickeydir]])
157
+
158
+ ca = Puppetserver::Ca::LocalCertificateAuthority.new(digest, settings)
159
+ ca_cert, ca_key = ca.load_ca
160
+ return false if CliParsing.handle_errors(@logger, ca.errors)
161
+
162
+ passed = certnames.map do |certname|
163
+ errors = check_for_existing_ssl_files(certname, settings)
164
+ next false if CliParsing.handle_errors(@logger, errors)
165
+
166
+ current_alt_names = process_alt_names(alt_names, certname)
167
+
168
+ # For certs signed offline, any alt names are added directly to the cert,
169
+ # rather than to the CSR.
170
+ key, csr = generate_key_csr(certname, settings, digest)
171
+ next false unless csr
172
+
173
+ cert = ca.sign_authorized_cert(ca_key, ca_cert, csr, current_alt_names)
174
+ next false unless save_file(cert.to_pem, certname, settings[:certdir], "Certificate")
175
+ next false unless save_file(cert.to_pem, certname, settings[:signeddir], "Certificate")
176
+ next false unless save_keys(certname, settings, key)
177
+ ca.update_serial_file(cert.serial + 1)
178
+ true
179
+ end
180
+ passed.all?
181
+ end
182
+
122
183
  # Generate csrs and keys, then submit them to CA, request for the CA to sign
123
184
  # them, download the signed certificates from the CA, and finally save
124
185
  # the signed certs and associated keys. Returns true if all certs were
125
186
  # successfully created and saved.
126
- def generate_certs(certnames, settings, digest)
187
+ def generate_certs(certnames, alt_names, settings, digest)
127
188
  # Make sure we have all the directories where we will be writing files
128
189
  FileSystem.ensure_dirs([settings[:ssldir],
129
190
  settings[:certdir],
@@ -133,13 +194,18 @@ BANNER
133
194
  ca = Puppetserver::Ca::CertificateAuthority.new(@logger, settings)
134
195
 
135
196
  passed = certnames.map do |certname|
136
- key, csr = generate_key_csr(certname, settings, digest)
137
- return false unless csr
138
- return false unless ca.submit_certificate_request(certname, csr)
139
- return false unless ca.sign_certs([certname])
197
+ errors = check_for_existing_ssl_files(certname, settings)
198
+ next false if CliParsing.handle_errors(@logger, errors)
199
+
200
+ current_alt_names = process_alt_names(alt_names, certname)
201
+
202
+ key, csr = generate_key_csr(certname, settings, digest, current_alt_names)
203
+ next false unless csr
204
+ next false unless ca.submit_certificate_request(certname, csr)
205
+ next false unless ca.sign_certs([certname])
140
206
  if result = ca.get_certificate(certname)
141
- save_file(result.body, certname, settings[:certdir], "Certificate")
142
- save_keys(certname, settings, key)
207
+ next false unless save_file(result.body, certname, settings[:certdir], "Certificate")
208
+ next false unless save_keys(certname, settings, key)
143
209
  true
144
210
  else
145
211
  false
@@ -148,14 +214,16 @@ BANNER
148
214
  passed.all?
149
215
  end
150
216
 
151
- def generate_key_csr(certname, settings, digest)
217
+ # For certs signed offline, any alt names are added directly to the cert,
218
+ # rather than to the CSR.
219
+ def generate_key_csr(certname, settings, digest, alt_names = '')
152
220
  host = Puppetserver::Ca::Host.new(digest)
153
221
  private_key = host.create_private_key(settings[:keylength])
154
222
  extensions = []
155
- if !settings[:subject_alt_names].empty?
223
+ if !alt_names.empty?
156
224
  ef = OpenSSL::X509::ExtensionFactory.new
157
225
  extensions << ef.create_extension("subjectAltName",
158
- settings[:subject_alt_names],
226
+ alt_names,
159
227
  false)
160
228
  end
161
229
  csr = host.create_csr(name: certname,
@@ -169,15 +237,44 @@ BANNER
169
237
 
170
238
  def save_keys(certname, settings, key)
171
239
  public_key = key.public_key
172
- save_file(key, certname, settings[:privatekeydir], "Private key")
173
- save_file(public_key, certname, settings[:publickeydir], "Public key")
240
+ return false unless save_file(key, certname, settings[:privatekeydir], "Private key")
241
+ return false unless save_file(public_key, certname, settings[:publickeydir], "Public key")
242
+ true
174
243
  end
175
244
 
176
245
  def save_file(content, certname, dir, type)
177
246
  location = File.join(dir, "#{certname}.pem")
178
- @logger.warn "#{type} #{certname}.pem already exists, overwriting" if File.exist?(location)
179
- FileSystem.write_file(location, content, 0640)
180
- @logger.inform "Successfully saved #{type.downcase} for #{certname} to #{location}"
247
+ if File.exist?(location)
248
+ @logger.err "#{type} #{certname}.pem already exists. Please delete it if you really want to regenerate it."
249
+ false
250
+ else
251
+ FileSystem.write_file(location, content, 0640)
252
+ @logger.inform "Successfully saved #{type.downcase} for #{certname} to #{location}"
253
+ true
254
+ end
255
+ end
256
+
257
+ def check_for_existing_ssl_files(certname, settings)
258
+ files = [ File.join(settings[:certdir], "#{certname}.pem"),
259
+ File.join(settings[:privatekeydir], "#{certname}.pem"),
260
+ File.join(settings[:publickeydir], "#{certname}.pem"),
261
+ File.join(settings[:signeddir], "#{certname}.pem"), ]
262
+ errors = Puppetserver::Ca::Utils::FileSystem.check_for_existing_files(files)
263
+ if !errors.empty?
264
+ errors << "Please delete these files if you really want to generate a new cert for #{certname}."
265
+ end
266
+ errors
267
+ end
268
+
269
+ def process_alt_names(alt_names, certname)
270
+ return '' if alt_names.empty?
271
+
272
+ current_alt_names = alt_names.dup
273
+ # When validating the cert, OpenSSL will ignore the CN field if
274
+ # altnames are present, so we need to ensure that the certname is
275
+ # also listed among the alt names.
276
+ current_alt_names += ",DNS:#{certname}"
277
+ current_alt_names = Puppetserver::Ca::Utils::Config.munge_alt_names(current_alt_names)
181
278
  end
182
279
  end
183
280
  end
@@ -96,8 +96,12 @@ Options:
96
96
  end
97
97
 
98
98
  certs.each do |cert|
99
+ # In newer versions of the CA api we return subjcet_alt_names
100
+ # in addition to dns_alt_names, this field includes DNS alt
101
+ # names but also IP alt names.
102
+ alt_names = cert["subject_alt_names"] || cert["dns_alt_names"]
99
103
  @logger.inform " #{cert["name"]}".ljust(padded + 6) + " (SHA256) " + " #{cert["fingerprints"]["SHA256"]}" +
100
- (cert["dns_alt_names"].empty? ? "" : "\talt names: #{cert["dns_alt_names"]}")
104
+ (alt_names.empty? ? "" : "\talt names: #{alt_names}")
101
105
  end
102
106
  end
103
107
 
@@ -22,8 +22,6 @@ module Puppetserver
22
22
  # A regex describing valid formats with groups for capturing the value and units
23
23
  TTL_FORMAT = /^(\d+)(y|d|h|m|s)?$/
24
24
 
25
- include Puppetserver::Ca::Utils::Config
26
-
27
25
  def self.parse(config_path)
28
26
  instance = new(config_path)
29
27
  instance.load
@@ -49,7 +47,7 @@ module Puppetserver
49
47
  # start/stop it you must be root.
50
48
  def user_specific_conf_dir
51
49
  @user_specific_conf_dir ||=
52
- if running_as_root?
50
+ if Puppetserver::Ca::Utils::Config.running_as_root?
53
51
  '/etc/puppetlabs/puppet'
54
52
  else
55
53
  "#{ENV['HOME']}/.puppetlabs/etc/puppet"
@@ -161,7 +159,7 @@ module Puppetserver
161
159
  # Some special cases where we need to manipulate config settings:
162
160
  settings[:ca_ttl] = munge_ttl_setting(settings[:ca_ttl])
163
161
  settings[:certificate_revocation] = parse_crl_usage(settings[:certificate_revocation])
164
- settings[:subject_alt_names] = munge_alt_names(settings[:subject_alt_names])
162
+ settings[:subject_alt_names] = Puppetserver::Ca::Utils::Config.munge_alt_names(settings[:subject_alt_names])
165
163
  settings[:keylength] = settings[:keylength].to_i
166
164
 
167
165
  settings.each do |key, value|
@@ -231,18 +229,6 @@ module Puppetserver
231
229
  end
232
230
  end
233
231
 
234
- def munge_alt_names(names)
235
- raw_names = names.split(/\s*,\s*/).map(&:strip)
236
- munged_names = raw_names.map do |name|
237
- # Prepend the DNS tag if no tag was specified
238
- if !name.start_with?("IP:") && !name.start_with?("DNS:")
239
- "DNS:#{name}"
240
- else
241
- name
242
- end
243
- end.sort.uniq.join(", ")
244
- end
245
-
246
232
  def parse_crl_usage(setting)
247
233
  case setting.to_s
248
234
  when 'true', 'chain'
@@ -8,8 +8,6 @@ module Puppetserver
8
8
  # Puppetserver or any TK config service. Uses the ruby-hocon gem for parsing.
9
9
  class PuppetServer
10
10
 
11
- include Puppetserver::Ca::Utils::Config
12
-
13
11
  def self.parse(config_path = nil)
14
12
  instance = new(config_path)
15
13
  instance.load
@@ -50,7 +48,7 @@ module Puppetserver
50
48
  # Note that Puppet Server runs as the [pe-]puppet user but to
51
49
  # start/stop it you must be root.
52
50
  def user_specific_ca_dir
53
- if running_as_root?
51
+ if Puppetserver::Ca::Utils::Config.running_as_root?
54
52
  '/etc/puppetlabs/puppetserver/ca'
55
53
  else
56
54
  "#{ENV['HOME']}/.puppetlabs/etc/puppetserver/ca"
@@ -1,4 +1,5 @@
1
1
  require 'puppetserver/ca/host'
2
+ require 'puppetserver/ca/utils/file_system'
2
3
 
3
4
  require 'openssl'
4
5
 
@@ -39,10 +40,11 @@ module Puppetserver
39
40
  @digest = digest
40
41
  @host = Host.new(digest)
41
42
  @settings = settings
43
+ @errors = []
42
44
  end
43
45
 
44
46
  def errors
45
- @host.errors
47
+ @errors += @host.errors
46
48
  end
47
49
 
48
50
  def valid_until
@@ -62,6 +64,14 @@ module Puppetserver
62
64
  format_time(cert.not_after), cert.subject]
63
65
  end
64
66
 
67
+ def next_serial(serial_file)
68
+ if File.exist?(serial_file)
69
+ File.read(serial_file).to_i
70
+ else
71
+ 1
72
+ end
73
+ end
74
+
65
75
  def format_time(time)
66
76
  time.strftime('%Y-%m-%dT%H:%M:%S%Z')
67
77
  end
@@ -73,33 +83,63 @@ module Puppetserver
73
83
  @settings[:hostpubkey])
74
84
  if master_key
75
85
  master_csr = @host.create_csr(name: @settings[:certname], key: master_key)
76
- master_cert = sign_master_cert(ca_key, ca_cert, master_csr)
86
+ if @settings[:subject_alt_names].empty?
87
+ alt_names = "DNS:puppet, DNS:#{@settings[:certname]}"
88
+ else
89
+ alt_names = @settings[:subject_alt_names]
90
+ end
91
+
92
+ master_cert = sign_authorized_cert(ca_key, ca_cert, master_csr, alt_names)
77
93
  end
78
94
 
79
95
  return master_key, master_cert
80
96
  end
81
97
 
82
- def sign_master_cert(int_key, int_cert, csr)
98
+ # Used when generating certificates offline.
99
+ def load_ca
100
+ signing_cert = nil
101
+ key = nil
102
+
103
+ if File.exist?(@settings[:cacert]) && File.exist?(@settings[:cakey]) && File.exist?(@settings[:cacrl])
104
+ loader = Puppetserver::Ca::X509Loader.new(@settings[:cacert], @settings[:cakey], @settings[:cacrl])
105
+ if loader.errors.empty?
106
+ signing_cert = loader.certs[0]
107
+ key = loader.key
108
+ else
109
+ @errors += loader.errors
110
+ end
111
+ else
112
+ @errors << "CA not initialized. Please set up your CA before attempting to generate certs offline."
113
+ end
114
+
115
+ return signing_cert, key
116
+ end
117
+
118
+ def sign_authorized_cert(int_key, int_cert, csr, alt_names = '')
83
119
  cert = OpenSSL::X509::Certificate.new
84
120
  cert.public_key = csr.public_key
85
121
  cert.subject = csr.subject
86
122
  cert.issuer = int_cert.subject
87
123
  cert.version = 2
88
- cert.serial = 1
124
+ cert.serial = next_serial(@settings[:serial])
89
125
  cert.not_before = CERT_VALID_FROM
90
126
  cert.not_after = valid_until
91
127
 
92
128
  return unless add_custom_extensions(cert)
93
129
 
94
130
  ef = extension_factory_for(int_cert, cert)
95
- add_master_extensions(cert, ef)
96
- add_subject_alt_names_extension(cert, ef)
131
+ add_authorized_extensions(cert, ef)
132
+
133
+ if !alt_names.empty?
134
+ add_subject_alt_names_extension(alt_names, cert, ef)
135
+ end
136
+
97
137
  cert.sign(int_key, @digest)
98
138
 
99
139
  cert
100
140
  end
101
141
 
102
- def add_master_extensions(cert, ef)
142
+ def add_authorized_extensions(cert, ef)
103
143
  MASTER_EXTENSIONS.each do |ext|
104
144
  extension = ef.create_extension(*ext)
105
145
  cert.add_extension(extension)
@@ -110,14 +150,8 @@ module Puppetserver
110
150
  cert.add_extension(cli_auth_ext)
111
151
  end
112
152
 
113
- def add_subject_alt_names_extension(cert, ef)
114
- sans =
115
- if @settings[:subject_alt_names].empty?
116
- "DNS:puppet, DNS:#{@settings[:certname]}"
117
- else
118
- @settings[:subject_alt_names]
119
- end
120
- alt_names_ext = ef.create_extension("subjectAltName", sans, false)
153
+ def add_subject_alt_names_extension(alt_names, cert, ef)
154
+ alt_names_ext = ef.create_extension("subjectAltName", alt_names, false)
121
155
  cert.add_extension(alt_names_ext)
122
156
  end
123
157
 
@@ -216,6 +250,10 @@ module Puppetserver
216
250
 
217
251
  cert
218
252
  end
253
+
254
+ def update_serial_file(serial)
255
+ Puppetserver::Ca::Utils::FileSystem.write_file(@settings[:serial], serial, 0644)
256
+ end
219
257
  end
220
258
  end
221
259
  end
@@ -3,10 +3,22 @@ module Puppetserver
3
3
  module Utils
4
4
  module Config
5
5
 
6
- def running_as_root?
6
+ def self.running_as_root?
7
7
  !Gem.win_platform? && Process::UID.eid == 0
8
8
  end
9
9
 
10
+ def self.munge_alt_names(names)
11
+ raw_names = names.split(/\s*,\s*/).map(&:strip)
12
+ munged_names = raw_names.map do |name|
13
+ # Prepend the DNS tag if no tag was specified
14
+ if !name.start_with?("IP:") && !name.start_with?("DNS:")
15
+ "DNS:#{name}"
16
+ else
17
+ name
18
+ end
19
+ end.sort.uniq.join(", ")
20
+ end
21
+
10
22
  end
11
23
  end
12
24
  end
@@ -1,5 +1,5 @@
1
1
  module Puppetserver
2
2
  module Ca
3
- VERSION = "1.0.0"
3
+ VERSION = "1.1.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puppetserver-ca
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-13 00:00:00.000000000 Z
11
+ date: 2018-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: facter