puppetserver-ca 0.6.0 → 0.7.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: a64b08fb770d1dc3f6837b80a4115756a143484d
4
- data.tar.gz: 9874789b46a28996379ec33f5f51b70946564c9b
3
+ metadata.gz: 1d0f3441468d38c6a0385a7c2ed7e3056e9e7407
4
+ data.tar.gz: 84e643ade9ca258be16166e16b1577dd454ec9f8
5
5
  SHA512:
6
- metadata.gz: c729aa2b55a1b080f773ae31ef103296c4eb3ffac7b3aeba2f789f8df7cceebd2ed443dfc7af96c6f02aecac4922997fed1f4ea6baae999f2c372e581cbbc5d4
7
- data.tar.gz: c64afc116c5e301fe58577bb83161baccf3e0a2ba20d580806f8696724e1568b7b0b0b9c42d4e5c6c09dedc004af9c8f1692316a1e080bb0df88229ab211ae59
6
+ metadata.gz: 6dd35a352ff2ca7aec331efd573781224471e03efcd451fd35d5c13416b6f2d2cceec34339d0ff9016840a77af321b9821d6fff1f2d6d42a568316786b8e19bd
7
+ data.tar.gz: 6e460d0db0cf328e17968950741eade841198ce165e306b5ca07e81343d1c29045b3baab88ce0be78c90c3a50c969fc4323c6e626f195814bd7fba6f08707e78
@@ -15,11 +15,11 @@ module Puppetserver
15
15
 
16
16
  CERTNAME_BLACKLIST = %w{--all --config}
17
17
 
18
- SUMMARY = 'Clean files from the CA for certificate(s)'
18
+ SUMMARY = 'Revoke cert(s) and remove related files from CA'
19
19
  BANNER = <<-BANNER
20
20
  Usage:
21
21
  puppetserver ca clean [--help]
22
- puppetserver ca clean [--config] --certname CERTNAME[,ADDLCERTNAME]
22
+ puppetserver ca clean [--config] --certname NAME[,NAME]
23
23
 
24
24
  Description:
25
25
  Given one or more valid certnames, instructs the CA to revoke certificates
@@ -34,11 +34,11 @@ BANNER
34
34
  parsed['certnames'] = []
35
35
  OptionParser.new do |o|
36
36
  o.banner = BANNER
37
- o.on('--certname foo,bar', Array,
37
+ o.on('--certname NAME[,NAME]', Array,
38
38
  'One or more comma separated certnames') do |certs|
39
39
  parsed['certnames'] += certs
40
40
  end
41
- o.on('--config PUPPET.CONF', 'Custom path to puppet.conf') do |conf|
41
+ o.on('--config CONF', 'Custom path to puppet.conf') do |conf|
42
42
  parsed['config'] = conf
43
43
  end
44
44
  o.on('--help', 'Display this clean specific help output') do |help|
@@ -1,32 +1,31 @@
1
- require 'optparse'
2
- require 'puppetserver/ca/utils/file_system'
3
- require 'puppetserver/ca/local_certificate_authority'
4
1
  require 'puppetserver/ca/utils/cli_parsing'
5
- require 'puppetserver/ca/utils/signing_digest'
2
+ require 'puppetserver/ca/host'
3
+ require 'puppetserver/ca/certificate_authority'
6
4
  require 'puppetserver/ca/config/puppet'
5
+ require 'puppetserver/ca/utils/file_system'
6
+ require 'puppetserver/ca/utils/signing_digest'
7
7
 
8
8
  module Puppetserver
9
9
  module Ca
10
10
  module Action
11
11
  class Generate
12
+
12
13
  include Puppetserver::Ca::Utils
13
14
 
14
- SUMMARY = "Generate a root and intermediate signing CA for Puppet Server"
15
+ # Only allow printing ascii characters, excluding /
16
+ VALID_CERTNAME = /\A[ -.0-~]+\Z/
17
+ CERTNAME_BLACKLIST = %w{--all --config}
18
+
19
+ SUMMARY = "Generate a new certificate signed by the CA"
15
20
  BANNER = <<-BANNER
16
21
  Usage:
17
22
  puppetserver ca generate [--help]
18
- puppetserver ca generate [--config PATH] [--subject-alt-names ALTNAME1[,ALTNAME2...]]
19
- [--certname NAME] [--ca-name NAME]
23
+ puppetserver ca generate [--config PATH] [--certname NAME[,NAME]]
24
+ [--subject-alt-names NAME[,NAME]]
20
25
 
21
26
  Description:
22
- Generate a root and intermediate signing CA for Puppet Server
23
- and store generated CA keys, certs, and crls on disk.
24
-
25
- The `--subject-alt-names` flag can be used to add SANs to the
26
- certificate generated for the Puppet master. Multiple names can be
27
- listed as a comma separated string. These can be either DNS names or
28
- IP addresses, differentiated by prefixes: `DNS:foo.bar.com,IP:123.456.789`.
29
- Names with no prefix will be treated as DNS names.
27
+ Generates a new certificate signed by the intermediate CA
28
+ and stores generated keys and certs on disk.
30
29
 
31
30
  To determine the target location, the default puppet.conf
32
31
  is consulted for custom values. If using a custom puppet.conf
@@ -34,14 +33,69 @@ provide it with the --config flag
34
33
 
35
34
  Options:
36
35
  BANNER
37
-
38
36
  def initialize(logger)
39
37
  @logger = logger
40
38
  end
41
39
 
40
+ def self.parser(parsed = {})
41
+ parsed['certnames'] = []
42
+ parsed['subject-alt-names'] = ''
43
+ OptionParser.new do |opts|
44
+ opts.banner = BANNER
45
+ opts.on('--certname NAME[,NAME]', Array,
46
+ 'One or more comma separated certnames') do |certs|
47
+ parsed['certnames'] += certs
48
+ end
49
+ opts.on('--help', 'Display this generate specific help output') do |help|
50
+ parsed['help'] = true
51
+ end
52
+ opts.on('--config CONF', 'Path to puppet.conf') do |conf|
53
+ parsed['config'] = conf
54
+ end
55
+ opts.on('--subject-alt-names NAME[,NAME]',
56
+ 'Subject alternative names for the generated cert') do |sans|
57
+ parsed['subject-alt-names'] = sans
58
+ end
59
+ end
60
+ end
61
+
62
+ def parse(args)
63
+ results = {}
64
+ parser = self.class.parser(results)
65
+
66
+ errors = CliParsing.parse_with_errors(parser, args)
67
+
68
+ if results['certnames'].empty?
69
+ errors << ' At least one certname is required to generate'
70
+ else
71
+ results['certnames'].each do |certname|
72
+ if CERTNAME_BLACKLIST.include?(certname)
73
+ errors << " Cannot manage cert named `#{certname}` from " +
74
+ "the CLI, if needed use the HTTP API directly"
75
+ end
76
+
77
+ if certname.match(/\p{Upper}/)
78
+ errors << " Certificate names must be lower case"
79
+ end
80
+
81
+ unless certname =~ VALID_CERTNAME
82
+ errors << " Certname #{certname} must not contain unprintable or non-ASCII characters"
83
+ end
84
+ end
85
+ end
86
+
87
+ errors_were_handled = CliParsing.handle_errors(@logger, errors, parser.help)
88
+
89
+ exit_code = errors_were_handled ? 1 : nil
90
+
91
+ return results, exit_code
92
+ end
93
+
42
94
  def run(input)
43
- # Validate config_path provided
95
+ certnames = input['certnames']
44
96
  config_path = input['config']
97
+
98
+ # Validate config_path provided
45
99
  if config_path
46
100
  errors = FileSystem.validate_file_paths(config_path)
47
101
  return 1 if CliParsing.handle_errors(@logger, errors)
@@ -49,130 +103,81 @@ BANNER
49
103
 
50
104
  # Load, resolve, and validate puppet config settings
51
105
  settings_overrides = {}
52
- settings_overrides[:certname] = input['certname'] unless input['certname'].empty?
53
- settings_overrides[:ca_name] = input['ca-name'] unless input['ca-name'].empty?
54
106
  # Since puppet expects the key to be called 'dns_alt_names', we need to use that here
55
107
  # to ensure that the overriding works correctly.
56
108
  settings_overrides[:dns_alt_names] = input['subject-alt-names'] unless input['subject-alt-names'].empty?
57
-
58
109
  puppet = Config::Puppet.new(config_path)
59
110
  puppet.load(settings_overrides)
60
111
  return 1 if CliParsing.handle_errors(@logger, puppet.errors)
61
112
 
62
- # Load most secure signing digest we can for cers/crl/csr signing.
113
+ # Load most secure signing digest we can for csr signing.
63
114
  signer = SigningDigest.new
64
115
  return 1 if CliParsing.handle_errors(@logger, signer.errors)
65
116
 
66
- # Generate root and intermediate ca and put all the certificates, crls,
67
- # and keys where they should go.
68
- errors = generate_pki(puppet.settings, signer.digest)
69
- return 1 if CliParsing.handle_errors(@logger, errors)
70
-
71
- @logger.inform "Generation succeeded. Find your files in #{puppet.settings[:cadir]}"
72
- return 0
117
+ # Generate and save certs and associated keys
118
+ all_passed = generate_certs(certnames, puppet.settings, signer.digest)
119
+ return all_passed ? 0 : 1
73
120
  end
74
121
 
75
- def generate_pki(settings, signing_digest)
76
- ca = Puppetserver::Ca::LocalCertificateAuthority.new(signing_digest, settings)
77
-
78
- root_key, root_cert, root_crl = ca.create_root_cert
79
- int_key, int_cert, int_crl = ca.create_intermediate_cert(root_key, root_cert)
80
- master_key, master_cert = ca.create_master_cert(int_key, int_cert)
81
- return ca.errors if ca.errors.any?
82
-
122
+ # Generate csrs and keys, then submit them to CA, request for the CA to sign
123
+ # them, download the signed certificates from the CA, and finally save
124
+ # the signed certs and associated keys. Returns true if all certs were
125
+ # successfully created and saved.
126
+ def generate_certs(certnames, settings, digest)
127
+ # Make sure we have all the directories where we will be writing files
83
128
  FileSystem.ensure_dirs([settings[:ssldir],
84
- settings[:cadir],
85
129
  settings[:certdir],
86
130
  settings[:privatekeydir],
87
- settings[:publickeydir],
88
- settings[:signeddir]])
89
-
90
- public_files = [
91
- [settings[:cacert], [int_cert, root_cert]],
92
- [settings[:cacrl], [int_crl, root_crl]],
93
- [settings[:cadir] + '/infra_crl.pem', [int_crl, root_crl]],
94
- [settings[:hostcert], master_cert],
95
- [settings[:localcacert], [int_cert, root_cert]],
96
- [settings[:hostcrl], [int_crl, root_crl]],
97
- [settings[:hostpubkey], master_key.public_key],
98
- [settings[:capub], int_key.public_key],
99
- [settings[:cert_inventory], ca.inventory_entry(master_cert)],
100
- [settings[:cadir] + '/infra_inventory.txt', ''],
101
- [settings[:cadir] + '/infra_serials', ''],
102
- [settings[:serial], "002"],
103
- [File.join(settings[:signeddir], "#{settings[:certname]}.pem"), master_cert],
104
- ]
105
-
106
- private_files = [
107
- [settings[:hostprivkey], master_key],
108
- [settings[:rootkey], root_key],
109
- [settings[:cakey], int_key],
110
- ]
111
-
112
- files_to_check = public_files + private_files
113
- # We don't want to error if master's keys exist. Certain workflows
114
- # allow the agent to have already be installed with keys and then
115
- # upgraded to be a master. The host class will honor keys, if both
116
- # public and private exist, and error if only one exists - as is
117
- # previous behavior.
118
- files_to_check = files_to_check.map(&:first) - [settings[:hostpubkey], settings[:hostprivkey]]
119
- errors = FileSystem.check_for_existing_files(files_to_check)
120
-
121
- if !errors.empty?
122
- instructions = <<-ERR
123
- If you would really like to replace your CA, please delete the existing files first.
124
- Note that any certificates that were issued by this CA will become invalid if you
125
- replace it!
126
- ERR
127
- errors << instructions
128
- return errors
129
- end
130
-
131
- public_files.each do |location, content|
132
- FileSystem.write_file(location, content, 0644)
131
+ settings[:publickeydir]])
132
+
133
+ ca = Puppetserver::Ca::CertificateAuthority.new(@logger, settings)
134
+
135
+ 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])
140
+ if result = ca.get_certificate(certname)
141
+ save_file(result.body, certname, settings[:certdir], "Certificate")
142
+ save_keys(certname, settings, key)
143
+ true
144
+ else
145
+ false
146
+ end
133
147
  end
148
+ passed.all?
149
+ end
134
150
 
135
- private_files.each do |location, content|
136
- FileSystem.write_file(location, content, 0640)
151
+ def generate_key_csr(certname, settings, digest)
152
+ host = Puppetserver::Ca::Host.new(digest)
153
+ private_key = host.create_private_key(settings[:keylength])
154
+ extensions = []
155
+ if !settings[:subject_alt_names].empty?
156
+ ef = OpenSSL::X509::ExtensionFactory.new
157
+ extensions << ef.create_extension("subjectAltName",
158
+ settings[:subject_alt_names],
159
+ false)
137
160
  end
161
+ csr = host.create_csr(name: certname,
162
+ key: private_key,
163
+ cli_extensions: extensions,
164
+ csr_attributes_path: settings[:csr_attributes])
165
+ return if CliParsing.handle_errors(@logger, host.errors)
138
166
 
139
- return []
167
+ return private_key, csr
140
168
  end
141
169
 
142
- def parse(cli_args)
143
- results = {}
144
- parser = self.class.parser(results)
145
- errors = CliParsing.parse_with_errors(parser, cli_args)
146
- errors_were_handled = CliParsing.handle_errors(@logger, errors, parser.help)
147
- exit_code = errors_were_handled ? 1 : nil
148
- return results, exit_code
170
+ def save_keys(certname, settings, key)
171
+ public_key = key.public_key
172
+ save_file(key, certname, settings[:privatekeydir], "Private key")
173
+ save_file(public_key, certname, settings[:publickeydir], "Public key")
149
174
  end
150
175
 
151
- def self.parser(parsed = {})
152
- parsed['subject-alt-names'] = ''
153
- parsed['ca-name'] = ''
154
- parsed['certname'] = ''
155
- OptionParser.new do |opts|
156
- opts.banner = BANNER
157
- opts.on('--help', 'Display this generate specific help output') do |help|
158
- parsed['help'] = true
159
- end
160
- opts.on('--config CONF', 'Path to puppet.conf') do |conf|
161
- parsed['config'] = conf
162
- end
163
- opts.on('--subject-alt-names NAME1[,NAME2]',
164
- 'Subject alternative names for the master cert') do |sans|
165
- parsed['subject-alt-names'] = sans
166
- end
167
- opts.on('--ca-name NAME',
168
- 'Common name to use for the CA signing cert') do |name|
169
- parsed['ca-name'] = name
170
- end
171
- opts.on('--certname NAME',
172
- 'Common name to use for the master cert') do |name|
173
- parsed['certname'] = name
174
- end
175
- end
176
+ def save_file(content, certname, dir, type)
177
+ 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}"
176
181
  end
177
182
  end
178
183
  end
@@ -12,12 +12,12 @@ module Puppetserver
12
12
  class Import
13
13
  include Puppetserver::Ca::Utils
14
14
 
15
- SUMMARY = "Import the CA's key, certs, and crls"
15
+ SUMMARY = "Import an external CA chain and generate master PKI"
16
16
  BANNER = <<-BANNER
17
17
  Usage:
18
18
  puppetserver ca import [--help]
19
19
  puppetserver ca import [--config PATH] [--certname NAME]
20
- [--subject-alt-names ALTNAME1[,ALTNAME2...]]
20
+ [--subject-alt-names NAME[,NAME]]
21
21
  --private-key PATH --cert-bundle PATH --crl-chain PATH
22
22
 
23
23
  Description:
@@ -181,7 +181,7 @@ ERR
181
181
  'Common name to use for the master cert') do |name|
182
182
  parsed['certname'] = name
183
183
  end
184
- opts.on('--subject-alt-names NAME1[,NAME2]',
184
+ opts.on('--subject-alt-names NAME[,NAME]',
185
185
  'Subject alternative names for the master cert') do |sans|
186
186
  parsed['subject-alt-names'] = sans
187
187
  end
@@ -12,7 +12,7 @@ module Puppetserver
12
12
 
13
13
  include Puppetserver::Ca::Utils
14
14
 
15
- SUMMARY = 'List all certificate requests'
15
+ SUMMARY = 'List certificates and CSRs'
16
16
  BANNER = <<-BANNER
17
17
  Usage:
18
18
  puppetserver ca list [--help]
@@ -14,11 +14,11 @@ module Puppetserver
14
14
 
15
15
  CERTNAME_BLACKLIST = %w{--all --config}
16
16
 
17
- SUMMARY = 'Revoke a given certificate'
17
+ SUMMARY = 'Revoke certificate(s)'
18
18
  BANNER = <<-BANNER
19
19
  Usage:
20
20
  puppetserver ca revoke [--help]
21
- puppetserver ca revoke [--config] --certname CERTNAME[,ADDLCERTNAME]
21
+ puppetserver ca revoke [--config] --certname NAME[,NAME]
22
22
 
23
23
  Description:
24
24
  Given one or more valid certnames, instructs the CA to revoke them over
@@ -31,11 +31,11 @@ BANNER
31
31
  parsed['certnames'] = []
32
32
  OptionParser.new do |o|
33
33
  o.banner = BANNER
34
- o.on('--certname foo,bar', Array,
34
+ o.on('--certname NAME[,NAME]', Array,
35
35
  'One or more comma separated certnames') do |certs|
36
36
  parsed['certnames'] += certs
37
37
  end
38
- o.on('--config PUPPET.CONF', 'Custom path to puppet.conf') do |conf|
38
+ o.on('--config CONF', 'Custom path to puppet.conf') do |conf|
39
39
  parsed['config'] = conf
40
40
  end
41
41
  o.on('--help', 'Displays this revoke specific help output') do |help|
@@ -0,0 +1,181 @@
1
+ require 'optparse'
2
+ require 'puppetserver/ca/utils/file_system'
3
+ require 'puppetserver/ca/local_certificate_authority'
4
+ require 'puppetserver/ca/utils/cli_parsing'
5
+ require 'puppetserver/ca/utils/signing_digest'
6
+ require 'puppetserver/ca/config/puppet'
7
+
8
+ module Puppetserver
9
+ module Ca
10
+ module Action
11
+ class Setup
12
+ include Puppetserver::Ca::Utils
13
+
14
+ SUMMARY = "Setup a self-signed CA chain for Puppet Server"
15
+ BANNER = <<-BANNER
16
+ Usage:
17
+ puppetserver ca setup [--help]
18
+ puppetserver ca setup [--config PATH] [--subject-alt-names NAME[,NAME]]
19
+ [--certname NAME] [--ca-name NAME]
20
+
21
+ Description:
22
+ Setup a root and intermediate signing CA for Puppet Server
23
+ and store generated CA keys, certs, crls, and associated
24
+ master related files on disk.
25
+
26
+ The `--subject-alt-names` flag can be used to add SANs to the
27
+ certificate generated for the Puppet master. Multiple names can be
28
+ listed as a comma separated string. These can be either DNS names or
29
+ IP addresses, differentiated by prefixes: `DNS:foo.bar.com,IP:123.456.789`.
30
+ Names with no prefix will be treated as DNS names.
31
+
32
+ To determine the target location, the default puppet.conf
33
+ is consulted for custom values. If using a custom puppet.conf
34
+ provide it with the --config flag
35
+
36
+ Options:
37
+ BANNER
38
+
39
+ def initialize(logger)
40
+ @logger = logger
41
+ end
42
+
43
+ def run(input)
44
+ # Validate config_path provided
45
+ config_path = input['config']
46
+ if config_path
47
+ errors = FileSystem.validate_file_paths(config_path)
48
+ return 1 if CliParsing.handle_errors(@logger, errors)
49
+ end
50
+
51
+ # Load, resolve, and validate puppet config settings
52
+ settings_overrides = {}
53
+ settings_overrides[:certname] = input['certname'] unless input['certname'].empty?
54
+ settings_overrides[:ca_name] = input['ca-name'] unless input['ca-name'].empty?
55
+ # Since puppet expects the key to be called 'dns_alt_names', we need to use that here
56
+ # to ensure that the overriding works correctly.
57
+ settings_overrides[:dns_alt_names] = input['subject-alt-names'] unless input['subject-alt-names'].empty?
58
+
59
+ puppet = Config::Puppet.new(config_path)
60
+ puppet.load(settings_overrides)
61
+ return 1 if CliParsing.handle_errors(@logger, puppet.errors)
62
+
63
+ # Load most secure signing digest we can for cers/crl/csr signing.
64
+ signer = SigningDigest.new
65
+ return 1 if CliParsing.handle_errors(@logger, signer.errors)
66
+
67
+ # Generate root and intermediate ca and put all the certificates, crls,
68
+ # and keys where they should go.
69
+ errors = generate_pki(puppet.settings, signer.digest)
70
+ return 1 if CliParsing.handle_errors(@logger, errors)
71
+
72
+ @logger.inform "Generation succeeded. Find your files in #{puppet.settings[:cadir]}"
73
+ return 0
74
+ end
75
+
76
+ def generate_pki(settings, signing_digest)
77
+ ca = Puppetserver::Ca::LocalCertificateAuthority.new(signing_digest, settings)
78
+
79
+ root_key, root_cert, root_crl = ca.create_root_cert
80
+ int_key, int_cert, int_crl = ca.create_intermediate_cert(root_key, root_cert)
81
+ master_key, master_cert = ca.create_master_cert(int_key, int_cert)
82
+ return ca.errors if ca.errors.any?
83
+
84
+ FileSystem.ensure_dirs([settings[:ssldir],
85
+ settings[:cadir],
86
+ settings[:certdir],
87
+ settings[:privatekeydir],
88
+ settings[:publickeydir],
89
+ settings[:signeddir]])
90
+
91
+ public_files = [
92
+ [settings[:cacert], [int_cert, root_cert]],
93
+ [settings[:cacrl], [int_crl, root_crl]],
94
+ [settings[:cadir] + '/infra_crl.pem', [int_crl, root_crl]],
95
+ [settings[:hostcert], master_cert],
96
+ [settings[:localcacert], [int_cert, root_cert]],
97
+ [settings[:hostcrl], [int_crl, root_crl]],
98
+ [settings[:hostpubkey], master_key.public_key],
99
+ [settings[:capub], int_key.public_key],
100
+ [settings[:cert_inventory], ca.inventory_entry(master_cert)],
101
+ [settings[:cadir] + '/infra_inventory.txt', ''],
102
+ [settings[:cadir] + '/infra_serials', ''],
103
+ [settings[:serial], "002"],
104
+ [File.join(settings[:signeddir], "#{settings[:certname]}.pem"), master_cert],
105
+ ]
106
+
107
+ private_files = [
108
+ [settings[:hostprivkey], master_key],
109
+ [settings[:rootkey], root_key],
110
+ [settings[:cakey], int_key],
111
+ ]
112
+
113
+ files_to_check = public_files + private_files
114
+ # We don't want to error if master's keys exist. Certain workflows
115
+ # allow the agent to have already be installed with keys and then
116
+ # upgraded to be a master. The host class will honor keys, if both
117
+ # public and private exist, and error if only one exists - as is
118
+ # previous behavior.
119
+ files_to_check = files_to_check.map(&:first) - [settings[:hostpubkey], settings[:hostprivkey]]
120
+ errors = FileSystem.check_for_existing_files(files_to_check)
121
+
122
+ if !errors.empty?
123
+ instructions = <<-ERR
124
+ If you would really like to replace your CA, please delete the existing files first.
125
+ Note that any certificates that were issued by this CA will become invalid if you
126
+ replace it!
127
+ ERR
128
+ errors << instructions
129
+ return errors
130
+ end
131
+
132
+ public_files.each do |location, content|
133
+ FileSystem.write_file(location, content, 0644)
134
+ end
135
+
136
+ private_files.each do |location, content|
137
+ FileSystem.write_file(location, content, 0640)
138
+ end
139
+
140
+ return []
141
+ end
142
+
143
+ def parse(cli_args)
144
+ results = {}
145
+ parser = self.class.parser(results)
146
+ errors = CliParsing.parse_with_errors(parser, cli_args)
147
+ errors_were_handled = CliParsing.handle_errors(@logger, errors, parser.help)
148
+ exit_code = errors_were_handled ? 1 : nil
149
+ return results, exit_code
150
+ end
151
+
152
+ def self.parser(parsed = {})
153
+ parsed['subject-alt-names'] = ''
154
+ parsed['ca-name'] = ''
155
+ parsed['certname'] = ''
156
+ OptionParser.new do |opts|
157
+ opts.banner = BANNER
158
+ opts.on('--help', 'Display this setup specific help output') do |help|
159
+ parsed['help'] = true
160
+ end
161
+ opts.on('--config CONF', 'Path to puppet.conf') do |conf|
162
+ parsed['config'] = conf
163
+ end
164
+ opts.on('--subject-alt-names NAME[,NAME]',
165
+ 'Subject alternative names for the master cert') do |sans|
166
+ parsed['subject-alt-names'] = sans
167
+ end
168
+ opts.on('--ca-name NAME',
169
+ 'Common name to use for the CA signing cert') do |name|
170
+ parsed['ca-name'] = name
171
+ end
172
+ opts.on('--certname NAME',
173
+ 'Common name to use for the master cert') do |name|
174
+ parsed['certname'] = name
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -14,11 +14,11 @@ module Puppetserver
14
14
 
15
15
  include Puppetserver::Ca::Utils
16
16
 
17
- SUMMARY = 'Sign a given certificate'
17
+ SUMMARY = 'Sign certificate request(s)'
18
18
  BANNER = <<-BANNER
19
19
  Usage:
20
20
  puppetserver ca sign [--help]
21
- puppetserver ca sign [--config] --certname CERTNAME[,CERTNAME]
21
+ puppetserver ca sign [--config] --certname NAME[,NAME]
22
22
  puppetserver ca sign --all
23
23
 
24
24
  Description:
@@ -30,10 +30,10 @@ Options:
30
30
  def self.parser(parsed = {})
31
31
  OptionParser.new do |opts|
32
32
  opts.banner = BANNER
33
- opts.on('--certname x,y,z', Array, 'the name(s) of the cert(s) to be signed') do |cert|
33
+ opts.on('--certname NAME[,NAME]', Array, 'the name(s) of the cert(s) to be signed') do |cert|
34
34
  parsed['certname'] = cert
35
35
  end
36
- opts.on('--config PUPPET.CONF', 'Custom path to Puppet\'s config file') do |conf|
36
+ opts.on('--config CONF', 'Custom path to Puppet\'s config file') do |conf|
37
37
  parsed['config'] = conf
38
38
  end
39
39
  opts.on('--help', 'Display this command specific help output') do |help|
@@ -1,13 +1,13 @@
1
1
  require 'optparse'
2
- require 'puppetserver/ca/version'
3
- require 'puppetserver/ca/logger'
4
2
  require 'puppetserver/ca/action/clean'
5
- require 'puppetserver/ca/action/create'
6
- require 'puppetserver/ca/action/import'
7
3
  require 'puppetserver/ca/action/generate'
8
- require 'puppetserver/ca/action/revoke'
4
+ require 'puppetserver/ca/action/import'
9
5
  require 'puppetserver/ca/action/list'
6
+ require 'puppetserver/ca/action/revoke'
7
+ require 'puppetserver/ca/action/setup'
10
8
  require 'puppetserver/ca/action/sign'
9
+ require 'puppetserver/ca/logger'
10
+ require 'puppetserver/ca/version'
11
11
  require 'puppetserver/ca/utils/cli_parsing'
12
12
 
13
13
 
@@ -21,18 +21,28 @@ Manage the Private Key Infrastructure for
21
21
  Puppet Server's built-in Certificate Authority
22
22
  BANNER
23
23
 
24
- VALID_ACTIONS = {
24
+ INIT_ACTIONS = {
25
+ 'import' => Action::Import,
26
+ 'setup' => Action::Setup,
27
+ }
28
+
29
+ MAINT_ACTIONS = {
25
30
  'clean' => Action::Clean,
26
- 'create' => Action::Create,
27
31
  'generate' => Action::Generate,
28
- 'import' => Action::Import,
29
32
  'list' => Action::List,
30
33
  'revoke' => Action::Revoke,
31
34
  'sign' => Action::Sign
32
35
  }
33
36
 
34
- ACTION_LIST = "\nAvailable Actions:\n" +
35
- VALID_ACTIONS.map do |action, cls|
37
+ VALID_ACTIONS = INIT_ACTIONS.merge(MAINT_ACTIONS).sort.to_h
38
+
39
+ ACTION_LIST = "\nAvailable Actions:\n\n" +
40
+ " Certificate Actions (requires a running Puppet Server):\n\n" +
41
+ MAINT_ACTIONS.map do |action, cls|
42
+ " #{action}\t#{cls::SUMMARY}"
43
+ end.join("\n") + "\n\n" +
44
+ " Initialization Actions (requires Puppet Server to be stopped):\n\n" +
45
+ INIT_ACTIONS.map do |action, cls|
36
46
  " #{action}\t#{cls::SUMMARY}"
37
47
  end.join("\n")
38
48
 
@@ -1,5 +1,5 @@
1
1
  module Puppetserver
2
2
  module Ca
3
- VERSION = "0.6.0"
3
+ VERSION = "0.7.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: 0.6.0
4
+ version: 0.7.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-11 00:00:00.000000000 Z
11
+ date: 2018-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: facter
@@ -93,11 +93,11 @@ files:
93
93
  - exe/puppetserver-ca
94
94
  - lib/puppetserver/ca.rb
95
95
  - lib/puppetserver/ca/action/clean.rb
96
- - lib/puppetserver/ca/action/create.rb
97
96
  - lib/puppetserver/ca/action/generate.rb
98
97
  - lib/puppetserver/ca/action/import.rb
99
98
  - lib/puppetserver/ca/action/list.rb
100
99
  - lib/puppetserver/ca/action/revoke.rb
100
+ - lib/puppetserver/ca/action/setup.rb
101
101
  - lib/puppetserver/ca/action/sign.rb
102
102
  - lib/puppetserver/ca/certificate_authority.rb
103
103
  - lib/puppetserver/ca/cli.rb
@@ -1,185 +0,0 @@
1
- require 'puppetserver/ca/utils/cli_parsing'
2
- require 'puppetserver/ca/host'
3
- require 'puppetserver/ca/certificate_authority'
4
- require 'puppetserver/ca/config/puppet'
5
- require 'puppetserver/ca/utils/file_system'
6
- require 'puppetserver/ca/utils/signing_digest'
7
-
8
- module Puppetserver
9
- module Ca
10
- module Action
11
- class Create
12
-
13
- include Puppetserver::Ca::Utils
14
-
15
- # Only allow printing ascii characters, excluding /
16
- VALID_CERTNAME = /\A[ -.0-~]+\Z/
17
- CERTNAME_BLACKLIST = %w{--all --config}
18
-
19
- SUMMARY = "Create a new certificate signed by the CA"
20
- BANNER = <<-BANNER
21
- Usage:
22
- puppetserver ca create [--help]
23
- puppetserver ca create [--config PATH] [--certname CERTNAME[,ADDLCERTNAME]]
24
- [--subject-alt-names ALTNAME1[,ALTNAME2...]]
25
-
26
- Description:
27
- Creates a new certificate signed by the intermediate CA
28
- and stores generated keys and certs on disk.
29
-
30
- To determine the target location, the default puppet.conf
31
- is consulted for custom values. If using a custom puppet.conf
32
- provide it with the --config flag
33
-
34
- Options:
35
- BANNER
36
- def initialize(logger)
37
- @logger = logger
38
- end
39
-
40
- def self.parser(parsed = {})
41
- parsed['certnames'] = []
42
- parsed['subject-alt-names'] = ''
43
- OptionParser.new do |opts|
44
- opts.banner = BANNER
45
- opts.on('--certname FOO,BAR', Array,
46
- 'One or more comma separated certnames') do |certs|
47
- parsed['certnames'] += certs
48
- end
49
- opts.on('--help', 'Display this create specific help output') do |help|
50
- parsed['help'] = true
51
- end
52
- opts.on('--config CONF', 'Path to puppet.conf') do |conf|
53
- parsed['config'] = conf
54
- end
55
- opts.on('--subject-alt-names NAME1[,NAME2]',
56
- 'Subject alternative names for the generated cert') do |sans|
57
- parsed['subject-alt-names'] = sans
58
- end
59
- end
60
- end
61
-
62
- def parse(args)
63
- results = {}
64
- parser = self.class.parser(results)
65
-
66
- errors = CliParsing.parse_with_errors(parser, args)
67
-
68
- if results['certnames'].empty?
69
- errors << ' At least one certname is required to create'
70
- else
71
- results['certnames'].each do |certname|
72
- if CERTNAME_BLACKLIST.include?(certname)
73
- errors << " Cannot manage cert named `#{certname}` from " +
74
- "the CLI, if needed use the HTTP API directly"
75
- end
76
-
77
- if certname.match(/\p{Upper}/)
78
- errors << " Certificate names must be lower case"
79
- end
80
-
81
- unless certname =~ VALID_CERTNAME
82
- errors << " Certname #{certname} must not contain unprintable or non-ASCII characters"
83
- end
84
- end
85
- end
86
-
87
- errors_were_handled = CliParsing.handle_errors(@logger, errors, parser.help)
88
-
89
- exit_code = errors_were_handled ? 1 : nil
90
-
91
- return results, exit_code
92
- end
93
-
94
- def run(input)
95
- certnames = input['certnames']
96
- config_path = input['config']
97
-
98
- # Validate config_path provided
99
- if config_path
100
- errors = FileSystem.validate_file_paths(config_path)
101
- return 1 if CliParsing.handle_errors(@logger, errors)
102
- end
103
-
104
- # Load, resolve, and validate puppet config settings
105
- 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
- puppet = Config::Puppet.new(config_path)
110
- puppet.load(settings_overrides)
111
- return 1 if CliParsing.handle_errors(@logger, puppet.errors)
112
-
113
- # Load most secure signing digest we can for csr signing.
114
- signer = SigningDigest.new
115
- return 1 if CliParsing.handle_errors(@logger, signer.errors)
116
-
117
- # Generate and save certs and associated keys
118
- all_passed = generate_certs(certnames, puppet.settings, signer.digest)
119
- return all_passed ? 0 : 1
120
- end
121
-
122
- # Create csrs and keys, then submit them to CA, request for the CA to sign
123
- # them, download the signed certificates from the CA, and finally save
124
- # the signed certs and associated keys. Returns true if all certs were
125
- # successfully created and saved.
126
- def generate_certs(certnames, settings, digest)
127
- # Make sure we have all the directories where we will be writing files
128
- FileSystem.ensure_dirs([settings[:ssldir],
129
- settings[:certdir],
130
- settings[:privatekeydir],
131
- settings[:publickeydir]])
132
-
133
- ca = Puppetserver::Ca::CertificateAuthority.new(@logger, settings)
134
-
135
- 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])
140
- if result = ca.get_certificate(certname)
141
- save_file(result.body, certname, settings[:certdir], "Certificate")
142
- save_keys(certname, settings, key)
143
- true
144
- else
145
- false
146
- end
147
- end
148
- passed.all?
149
- end
150
-
151
- def generate_key_csr(certname, settings, digest)
152
- host = Puppetserver::Ca::Host.new(digest)
153
- private_key = host.create_private_key(settings[:keylength])
154
- extensions = []
155
- if !settings[:subject_alt_names].empty?
156
- ef = OpenSSL::X509::ExtensionFactory.new
157
- extensions << ef.create_extension("subjectAltName",
158
- settings[:subject_alt_names],
159
- false)
160
- end
161
- csr = host.create_csr(name: certname,
162
- key: private_key,
163
- cli_extensions: extensions,
164
- csr_attributes_path: settings[:csr_attributes])
165
- return if CliParsing.handle_errors(@logger, host.errors)
166
-
167
- return private_key, csr
168
- end
169
-
170
- def save_keys(certname, settings, key)
171
- public_key = key.public_key
172
- save_file(key, certname, settings[:privatekeydir], "Private key")
173
- save_file(public_key, certname, settings[:publickeydir], "Public key")
174
- end
175
-
176
- def save_file(content, certname, dir, type)
177
- 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}"
181
- end
182
- end
183
- end
184
- end
185
- end