puppetserver-ca 0.2.0 → 0.3.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: 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