puppetserver-ca 0.2.0 → 0.3.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: f41aeb772961d07711e18252a727800b7962e11a
4
- data.tar.gz: ebc595c85433ec3a4c14d2660e0e92237bee3690
3
+ metadata.gz: 652bd1357434ad6509c6c08678b57373a4bd6ef1
4
+ data.tar.gz: 0ad23edeca68b2813ff5d3b1683882f911e7a4c8
5
5
  SHA512:
6
- metadata.gz: 315814ed7b351eea57fc5620bd08e8e406d6c489a24cc4e0e2e6048844d2ed2ef85260a6fc5c1de8f923f999708aaf0679388b3a23af865f04538f14f6a617e2
7
- data.tar.gz: b482951edd70310b07d1aa2a8f988a34bc3a08d3a5ddf75e10799a2b86ad09e0dfd8eee0b683ee0fd665eb90e795c0c422517371dedb68124c49d6544283bec6
6
+ metadata.gz: 7445af71aaa5aeed30c9e69ac679cd88bffd60a126e9f0766196a69edb353b8473312dec8b34cb767a87f58837af307005fc11e13d368538404a8ab464415970
7
+ data.tar.gz: 3a3e1ca91518f68c6a6aedb682357c34d9ddf845f482586d9bc21f342c0a36f9f59f577401c73cfcf9ae12d1dd44a952b5c30c28c84f7fd1032f2ac4e0e8db6f
@@ -0,0 +1,159 @@
1
+ require 'puppetserver/ca/utils'
2
+ require 'puppetserver/utils/http_client'
3
+ require 'puppetserver/utils/file_utilities'
4
+ require 'puppetserver/ca/puppet_config'
5
+ require 'puppetserver/ca/revoke_action'
6
+
7
+ require 'optparse'
8
+ require 'json'
9
+
10
+ module Puppetserver
11
+ module Ca
12
+ class CleanAction
13
+
14
+ include Puppetserver::Utils
15
+
16
+ CERTNAME_BLACKLIST = %w{--all --config}
17
+
18
+ SUMMARY = 'Clean files from the CA for certificate(s)'
19
+ BANNER = <<-BANNER
20
+ Usage:
21
+ puppetserver ca clean [--help|--version]
22
+ puppetserver ca clean [--config] --certname CERTNAME[,ADDLCERTNAME]
23
+
24
+ Description:
25
+ Given one or more valid certnames, instructs the CA to revoke certificates
26
+ matching the given certnames if they exist, and then remove files pertaining
27
+ to them (keys, cert, and certificate request) over HTTPS using the local
28
+ agent's PKI
29
+
30
+ Options:
31
+ BANNER
32
+
33
+ def self.parser(parsed = {})
34
+ parsed['certnames'] = []
35
+ OptionParser.new do |o|
36
+ o.banner = BANNER
37
+ o.on('--certname foo,bar', Array,
38
+ 'One or more comma separated certnames') do |certs|
39
+ parsed['certnames'] += certs
40
+ end
41
+ o.on('--config PUPPET.CONF', 'Custom path to puppet.conf') do |conf|
42
+ parsed['config'] = conf
43
+ end
44
+ o.on('--help', 'Displays this clean specific help output') do |help|
45
+ parsed['help'] = help
46
+ end
47
+ end
48
+ end
49
+
50
+ def initialize(logger)
51
+ @logger = logger
52
+ end
53
+
54
+ def parse(args)
55
+ results = {}
56
+ parser = self.class.parser(results)
57
+
58
+ errors = Utils.parse_with_errors(parser, args)
59
+
60
+ results['certnames'].each do |certname|
61
+ if CERTNAME_BLACKLIST.include?(certname)
62
+ errors << " Cannot manage cert named `#{certname}` from " +
63
+ "the CLI, if needed use the HTTP API directly"
64
+ end
65
+ end
66
+
67
+ if results['certnames'].empty?
68
+ errors << ' At least one certname is required to clean'
69
+ end
70
+
71
+ errors_were_handled = Utils.handle_errors(@logger, errors, parser.help)
72
+
73
+ exit_code = errors_were_handled ? 1 : nil
74
+
75
+ return results, exit_code
76
+ end
77
+
78
+ def run(args)
79
+ certnames = args['certnames']
80
+ config = args['config']
81
+
82
+ if config
83
+ errors = FileUtilities.validate_file_paths(config)
84
+ return 1 if Utils.handle_errors(@logger, errors)
85
+ end
86
+
87
+ puppet = PuppetConfig.parse(config)
88
+ return 1 if Utils.handle_errors(@logger, puppet.errors)
89
+
90
+ passed = clean_certs(certnames, puppet.settings)
91
+
92
+ return passed ? 0 : 1
93
+ end
94
+
95
+ def clean_certs(certnames, settings)
96
+ client = HttpClient.new(settings[:localcacert],
97
+ settings[:certificate_revocation],
98
+ settings[:hostcrl])
99
+
100
+ url = client.make_ca_url(settings[:ca_server],
101
+ settings[:ca_port],
102
+ 'certificate_status')
103
+
104
+ results = client.with_connection(url) do |connection|
105
+ certnames.map do |certname|
106
+ url.resource_name = certname
107
+ revoke_result = connection.put(RevokeAction::REQUEST_BODY, url)
108
+ revoked = check_revocation(revoke_result, certname)
109
+
110
+ cleaned = nil
111
+ unless revoked == :error
112
+ clean_result = connection.delete(url)
113
+ cleaned = check_result(clean_result, certname)
114
+ end
115
+
116
+ cleaned == :success && [:success, :not_found].include?(revoked)
117
+ end
118
+ end
119
+
120
+ return results.all?
121
+ end
122
+
123
+ # possibly logs the action, always returns a status symbol 👑
124
+ def check_revocation(result, certname)
125
+ case result.code
126
+ when '200', '204'
127
+ @logger.inform "Revoked certificate for #{certname}"
128
+ return :success
129
+ when '404'
130
+ return :not_found
131
+ else
132
+ @logger.err 'Error:'
133
+ @logger.err " Failed revoking certificate for #{certname}"
134
+ @logger.err " Received code: #{result.code}, body: #{result.body}"
135
+ return :error
136
+ end
137
+ end
138
+
139
+ # logs the action and returns a status symbol 👑
140
+ def check_result(result, certname)
141
+ case result.code
142
+ when '200', '204'
143
+ @logger.inform "Cleaned files related to #{certname}"
144
+ return :success
145
+ when '404'
146
+ @logger.err 'Error:'
147
+ @logger.err " Could not find files for #{certname}"
148
+ return :not_found
149
+ else
150
+ @logger.err 'Error:'
151
+ @logger.err " When cleaning #{certname} received:"
152
+ @logger.err " code: #{result.code}"
153
+ @logger.err " body: #{result.body.to_s}" if result.body
154
+ return :error
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -1,7 +1,14 @@
1
1
  require 'optparse'
2
2
  require 'puppetserver/ca/version'
3
- require 'puppetserver/ca/import_action'
4
3
  require 'puppetserver/ca/logger'
4
+ require 'puppetserver/ca/clean_action'
5
+ require 'puppetserver/ca/import_action'
6
+ require 'puppetserver/ca/generate_action'
7
+ require 'puppetserver/ca/revoke_action'
8
+ require 'puppetserver/ca/list_action'
9
+ require 'puppetserver/ca/sign_action'
10
+ require 'puppetserver/ca/utils'
11
+ require 'puppetserver/ca/create_action'
5
12
 
6
13
  module Puppetserver
7
14
  module Ca
@@ -13,7 +20,15 @@ Manage the Private Key Infrastructure for
13
20
  Puppet Server's built-in Certificate Authority
14
21
  BANNER
15
22
 
16
- VALID_ACTIONS = {'import' => ImportAction}
23
+ VALID_ACTIONS = {
24
+ 'clean' => CleanAction,
25
+ 'create' => CreateAction,
26
+ 'generate' => GenerateAction,
27
+ 'import' => ImportAction,
28
+ 'list' => ListAction,
29
+ 'revoke' => RevokeAction,
30
+ 'sign' => SignAction
31
+ }
17
32
 
18
33
  ACTION_LIST = "\nAvailable Actions:\n" +
19
34
  VALID_ACTIONS.map do |action, cls|
@@ -22,10 +37,12 @@ BANNER
22
37
 
23
38
  ACTION_OPTIONS = "\nAction Options:\n" +
24
39
  VALID_ACTIONS.map do |action, cls|
25
- " #{action}:\n" +
26
- cls.parser.summarize.
27
- select{|line| line =~ /^\s*--/ }.
28
- reject{|line| line =~ /--help|--version/ }.join('')
40
+ action_summary = cls.parser.summarize.
41
+ select{|line| line =~ /^\s*--/ }.
42
+ reject{|line| line =~ /--help|--version/ }
43
+ summary = action_summary.empty? ? ' N/A' : action_summary.join('')
44
+
45
+ " #{action}:\n" + summary
29
46
  end.join("\n")
30
47
 
31
48
 
@@ -86,19 +103,9 @@ BANNER
86
103
 
87
104
  end
88
105
 
89
- unparsed, nonopts = [], []
90
-
91
- begin
92
- general_parser.order!(inputs) do |nonopt|
93
- nonopts << nonopt
94
- end
95
- rescue OptionParser::InvalidOption => e
96
- unparsed += e.args
97
- unparsed << inputs.shift unless inputs.first =~ /^-{1,2}/
98
- retry
99
- end
106
+ all,_,_,_ = Utils.parse_without_raising(general_parser, inputs)
100
107
 
101
- return general_parser, parsed, nonopts + unparsed
108
+ return general_parser, parsed, all
102
109
  end
103
110
  end
104
111
  end
@@ -0,0 +1,267 @@
1
+ require 'puppetserver/ca/utils'
2
+ require 'puppetserver/ca/host'
3
+ require 'puppetserver/ca/puppet_config'
4
+ require 'puppetserver/utils/file_utilities'
5
+ require 'puppetserver/utils/http_client'
6
+ require 'puppetserver/utils/signing_digest'
7
+ require 'json'
8
+
9
+ module Puppetserver
10
+ module Ca
11
+ class CreateAction
12
+
13
+ include Puppetserver::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] --certname CERTNAME[,ADDLCERTNAME]
24
+
25
+ Description:
26
+ Creates a new certificate signed by the intermediate CA
27
+ and stores generated keys and certs on disk.
28
+
29
+ To determine the target location, the default puppet.conf
30
+ is consulted for custom values. If using a custom puppet.conf
31
+ provide it with the --config flag
32
+
33
+ Options:
34
+ BANNER
35
+ def initialize(logger)
36
+ @logger = logger
37
+ end
38
+
39
+ def self.parser(parsed = {})
40
+ parsed['certnames'] = []
41
+ OptionParser.new do |opts|
42
+ opts.banner = BANNER
43
+ opts.on('--certname FOO,BAR', Array,
44
+ 'One or more comma separated certnames') do |certs|
45
+ parsed['certnames'] += certs
46
+ end
47
+ opts.on('--help', 'Display this create specific help output') do |help|
48
+ parsed['help'] = true
49
+ end
50
+ opts.on('--config CONF', 'Path to puppet.conf') do |conf|
51
+ parsed['config'] = conf
52
+ end
53
+ end
54
+ end
55
+
56
+ def parse(args)
57
+ results = {}
58
+ parser = self.class.parser(results)
59
+
60
+ errors = Utils.parse_with_errors(parser, args)
61
+
62
+ if results['certnames'].empty?
63
+ errors << ' At least one certname is required to create'
64
+ else
65
+ results['certnames'].each do |certname|
66
+ if CERTNAME_BLACKLIST.include?(certname)
67
+ errors << " Cannot manage cert named `#{certname}` from " +
68
+ "the CLI, if needed use the HTTP API directly"
69
+ end
70
+
71
+ if certname.match(/\p{Upper}/)
72
+ errors << " Certificate names must be lower case"
73
+ end
74
+
75
+ unless certname =~ VALID_CERTNAME
76
+ errors << " Certname #{certname} must not contain unprintable or non-ASCII characters"
77
+ end
78
+ end
79
+ end
80
+
81
+ errors_were_handled = Utils.handle_errors(@logger, errors, parser.help)
82
+
83
+ exit_code = errors_were_handled ? 1 : nil
84
+
85
+ return results, exit_code
86
+ end
87
+
88
+ def run(input)
89
+ certnames = input['certnames']
90
+ config_path = input['config']
91
+
92
+ # Validate config_path provided
93
+ if config_path
94
+ errors = FileUtilities.validate_file_paths(config_path)
95
+ return 1 if Utils.handle_errors(@logger, errors)
96
+ end
97
+
98
+ # Load, resolve, and validate puppet config settings
99
+ puppet = PuppetConfig.parse(config_path)
100
+ return 1 if Utils.handle_errors(@logger, puppet.errors)
101
+
102
+ # Load most secure signing digest we can for csr signing.
103
+ signer = SigningDigest.new
104
+ return 1 if Utils.handle_errors(@logger, signer.errors)
105
+
106
+ # Make sure we have all the directories where we will be writing files
107
+ FileUtilities.ensure_dir(puppet.settings[:certdir])
108
+ FileUtilities.ensure_dir(puppet.settings[:privatekeydir])
109
+ FileUtilities.ensure_dir(puppet.settings[:publickeydir])
110
+
111
+ # Generate and save certs and associated keys
112
+ all_passed = generate_certs(certnames, puppet.settings, signer.digest)
113
+ return all_passed ? 0 : 1
114
+ end
115
+
116
+ # Create csrs and keys, then submit them to CA, request for the CA to sign
117
+ # them, download the signed certificates from the CA, and finally save
118
+ # the signed certs and associated keys. Returns true if all certs were
119
+ # successfully created and saved.
120
+ def generate_certs(certnames, settings, digest)
121
+ passed = certnames.map do |certname|
122
+ key, csr = generate_key_csr(certname, settings, digest)
123
+ return false unless submit_certificate_request(certname, csr.to_s, settings)
124
+ return false unless sign_cert(certname, settings)
125
+ if download_cert(certname, settings)
126
+ save_keys(key, certname, settings)
127
+ true
128
+ else
129
+ false
130
+ end
131
+ end
132
+ passed.all?
133
+ end
134
+
135
+ def generate_key_csr(certname, settings, digest)
136
+ host = Puppetserver::Ca::Host.new(digest)
137
+ private_key = host.create_private_key(settings[:keylength])
138
+ csr = host.create_csr(certname, private_key)
139
+
140
+ return private_key, csr
141
+ end
142
+
143
+ def http_client(settings)
144
+ @client ||= HttpClient.new(settings[:localcacert],
145
+ settings[:certificate_revocation],
146
+ settings[:hostcrl])
147
+ end
148
+
149
+ # Make an HTTP request to submit certificate requests to CA
150
+ # @param certname [String] the name of the certificate to fetch
151
+ # @param csr [String] string version of a OpenSSL::X509::Request
152
+ # @param settings [Hash] a hash of config settings
153
+ # @return [Boolean] success of all csrs being submitted to CA
154
+ def submit_certificate_request(certname, csr, settings)
155
+ client = http_client(settings)
156
+ url = client.make_ca_url(settings[:ca_server],
157
+ settings[:ca_port],
158
+ 'certificate_request',
159
+ certname)
160
+
161
+ client.with_connection(url) do |connection|
162
+ result = connection.put(csr, url)
163
+ check_submit_result(result, certname)
164
+ end
165
+ end
166
+
167
+ def check_submit_result(result, certname)
168
+ case result.code
169
+ when '200', '204'
170
+ @logger.inform "Successfully submitted certificate request for #{certname}"
171
+ return true
172
+ else
173
+ @logger.err 'Error:'
174
+ @logger.err " When certificate request submitted for #{certname}:"
175
+ @logger.err " code: #{result.code}"
176
+ @logger.err " body: #{result.body.to_s}" if result.body
177
+ return false
178
+ end
179
+ end
180
+
181
+ # Make an HTTP request to CA to sign the named certificates
182
+ # @param certname [String] the name of the certificate to have signed
183
+ # @param settings [Hash] a hash of config settings
184
+ # @return [Boolean] the success of certificates being signed
185
+ def sign_cert(certname, settings)
186
+ client = http_client(settings)
187
+
188
+ url = client.make_ca_url(settings[:ca_server],
189
+ settings[:ca_port],
190
+ 'certificate_status',
191
+ certname)
192
+
193
+ client.with_connection(url) do |connection|
194
+ body = JSON.dump({desired_state: 'signed'})
195
+ result = connection.put(body, url)
196
+ check_sign_result(result, certname)
197
+ end
198
+ end
199
+
200
+ def check_sign_result(result, certname)
201
+ case result.code
202
+ when '204'
203
+ @logger.inform "Successfully signed certificate request for #{certname}"
204
+ return true
205
+ else
206
+ @logger.err 'Error:'
207
+ @logger.err " When signing request submitted for #{certname}:"
208
+ @logger.err " code: #{result.code}"
209
+ @logger.err " body: #{result.body.to_s}" if result.body
210
+ return false
211
+ end
212
+ end
213
+
214
+ # Make an HTTP request to fetch the named certificates from CA
215
+ # @param certname [String] the name of the certificate to fetch
216
+ # @param settings [Hash] a hash of config settings
217
+ # @return [Boolean] the success of certificate being downloaded
218
+ def download_cert(certname, settings)
219
+ client = http_client(settings)
220
+ url = client.make_ca_url(settings[:ca_server],
221
+ settings[:ca_port],
222
+ 'certificate',
223
+ certname)
224
+ client.with_connection(url) do |connection|
225
+ result = connection.get(url)
226
+ if downloaded = check_download_result(result, certname)
227
+ save_file(result.body, certname, settings[:certdir], "Certificate")
228
+ @logger.inform "Successfully downloaded and saved certificate #{certname} to #{settings[:certdir]}/#{certname}.pem"
229
+ end
230
+ downloaded
231
+ end
232
+ end
233
+
234
+ def check_download_result(result, certname)
235
+ case result.code
236
+ when '200'
237
+ return true
238
+ when '404'
239
+ @logger.err 'Error:'
240
+ @logger.err " Signed certificate #{certname} could not be found on the CA"
241
+ return false
242
+ else
243
+ @logger.err 'Error:'
244
+ @logger.err " When download requested for certificate #{certname}:"
245
+ @logger.err " code: #{result.code}"
246
+ @logger.err " body: #{result.body.to_s}" if result.body
247
+ return false
248
+ end
249
+ end
250
+
251
+ def save_keys(key, certname, settings)
252
+ public_key = key.public_key
253
+ save_file(key, certname, settings[:privatekeydir], "Private key")
254
+ save_file(public_key, certname, settings[:publickeydir], "Public key")
255
+ @logger.inform "Successfully saved private key for #{certname} to #{settings[:privatekeydir]}/#{certname}.pem"
256
+ @logger.inform "Successfully saved public key for #{certname} to #{settings[:publickeydir]}/#{certname}.pem"
257
+ end
258
+
259
+
260
+ def save_file(content, certname, dir, type)
261
+ location = File.join(dir, "#{certname}.pem")
262
+ @logger.warn "#{type} #{certname}.pem already exists, overwriting" if File.exist?(location)
263
+ FileUtilities.write_file(location, content, 0640)
264
+ end
265
+ end
266
+ end
267
+ end