puppetserver-ca 0.6.0 → 0.7.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: 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