puppetserver-ca 1.0.0 → 1.1.0

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 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