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.
@@ -1,4 +1,7 @@
1
1
  require 'puppetserver/ca/config_utils'
2
+ require 'puppetserver/settings/ttl_setting'
3
+ require 'securerandom'
4
+ require 'facter'
2
5
 
3
6
  module Puppetserver
4
7
  module Ca
@@ -49,6 +52,8 @@ module Puppetserver
49
52
  results = parse_text(File.read(@config_path))
50
53
  end
51
54
 
55
+ @certname = default_certname
56
+
52
57
  results ||= {}
53
58
  results[:main] ||= {}
54
59
  results[:master] ||= {}
@@ -58,9 +63,19 @@ module Puppetserver
58
63
  @settings = resolve_settings(overrides).freeze
59
64
  end
60
65
 
61
- # Resolve the cacert, cakey, and cacrl settings from default values,
62
- # with any overrides for the specific settings or their dependent
63
- # settings (ssldir, cadir) taken into account.
66
+ def default_certname
67
+ hostname = Facter.value(:hostname)
68
+ domain = Facter.value(:domain)
69
+ if domain and domain != ''
70
+ fqdn = [hostname, domain].join('.')
71
+ else
72
+ fqdn = hostname
73
+ end
74
+ fqdn.chomp('.')
75
+ end
76
+
77
+ # Resolve settings from default values, with any overrides for the
78
+ # specific settings or their dependent settings (ssldir, cadir) taken into account.
64
79
  def resolve_settings(overrides = {})
65
80
  unresolved_setting = /\$[a-z_]+/
66
81
 
@@ -75,22 +90,54 @@ module Puppetserver
75
90
  ssldir = overrides.fetch(:ssldir, '$confdir/ssl')
76
91
  settings[:ssldir] = substitutions['$ssldir'] = ssldir.sub('$confdir', confdir)
77
92
 
93
+ certdir = overrides.fetch(:certdir, '$ssldir/certs')
94
+ settings[:certdir] = substitutions['$certdir'] = certdir.sub(unresolved_setting, substitutions)
95
+
78
96
  cadir = overrides.fetch(:cadir, '$ssldir/ca')
79
97
  settings[:cadir] = substitutions['$cadir'] = cadir.sub(unresolved_setting, substitutions)
80
98
 
81
- settings[:cacert] = overrides.fetch(:cacert, '$cadir/ca_crt.pem')
82
- settings[:cakey] = overrides.fetch(:cakey, '$cadir/ca_key.pem')
83
- settings[:cacrl] = overrides.fetch(:cacrl, '$cadir/ca_crl.pem')
84
- settings[:serial] = overrides.fetch(:serial, '$cadir/serial')
99
+ settings[:certname] = substitutions['$certname'] = overrides.fetch(:certname, @certname)
100
+
101
+ server = overrides.fetch(:server, '$certname')
102
+ settings[:server] = substitutions['$server'] = server.sub(unresolved_setting, substitutions)
103
+
104
+ settings[:masterport] = substitutions['$masterport'] = overrides.fetch(:masterport, '8140')
105
+
106
+ settings[:ca_name] = overrides.fetch(:ca_name, 'Puppet CA: $certname')
107
+ settings[:root_ca_name] = overrides.fetch(:root_ca_name, "Puppet Root CA: #{SecureRandom.hex(7)}")
108
+
109
+ unmunged_ca_ttl = overrides.fetch(:ca_ttl, '15y')
110
+ ttl_setting = Puppetserver::Settings::TTLSetting.new(:ca_ttl, unmunged_ca_ttl)
111
+ if ttl_setting.errors
112
+ ttl_setting.errors.each { |error| @errors << error }
113
+ end
114
+
115
+ settings[:ca_ttl] = ttl_setting.munged_value
116
+ settings[:keylength] = overrides.fetch(:keylength, 4096)
117
+ settings[:cacert] = overrides.fetch(:cacert, '$cadir/ca_crt.pem')
118
+ settings[:cakey] = overrides.fetch(:cakey, '$cadir/ca_key.pem')
119
+ settings[:rootkey] = overrides.fetch(:rootkey, '$cadir/root_key.pem')
120
+ settings[:cacrl] = overrides.fetch(:cacrl, '$cadir/ca_crl.pem')
121
+ settings[:serial] = overrides.fetch(:serial, '$cadir/serial')
85
122
  settings[:cert_inventory] = overrides.fetch(:cert_inventory, '$cadir/inventory.txt')
123
+ settings[:ca_server] = overrides.fetch(:ca_server, '$server')
124
+ settings[:ca_port] = overrides.fetch(:ca_port, '$masterport')
125
+ settings[:localcacert] = overrides.fetch(:localcacert, '$certdir/ca.pem')
126
+ settings[:hostcert] = overrides.fetch(:hostcert, '$certdir/$certname.pem')
127
+ settings[:hostcrl] = overrides.fetch(:hostcrl, '$ssldir/crl.pem')
128
+ settings[:privatekeydir] = overrides.fetch(:privatekeydir, '$ssldir/private_keys')
129
+ settings[:publickeydir] = overrides.fetch(:publickeydir, '$ssldir/public_keys')
130
+ settings[:certificate_revocation] = parse_crl_usage(overrides.fetch(:certificate_revocation, 'true'))
86
131
 
87
132
  settings.each_pair do |key, value|
88
- settings[key] = value.sub(unresolved_setting, substitutions)
133
+ next unless value.is_a? String
134
+
135
+ settings[key] = value.gsub(unresolved_setting, substitutions)
89
136
 
90
137
  if match = settings[key].match(unresolved_setting)
91
138
  @errors << "Could not parse #{match[0]} in #{value}, " +
92
139
  'valid settings to be interpolated are ' +
93
- '$ssldir or $cadir'
140
+ '$ssldir, $cadir, or $certname'
94
141
  end
95
142
  end
96
143
 
@@ -127,6 +174,21 @@ module Puppetserver
127
174
  def explicitly_given_config_file_or_default_config_exists?
128
175
  !@using_default_location || File.exist?(@config_path)
129
176
  end
177
+
178
+ def run(command)
179
+ %x( #{command} )
180
+ end
181
+
182
+ def parse_crl_usage(setting)
183
+ case setting.to_s
184
+ when 'true', 'chain'
185
+ :chain
186
+ when 'leaf'
187
+ :leaf
188
+ when 'false'
189
+ :ignore
190
+ end
191
+ end
130
192
  end
131
193
  end
132
194
  end
@@ -0,0 +1,138 @@
1
+ require 'puppetserver/ca/utils'
2
+ require 'puppetserver/utils/http_client'
3
+ require 'puppetserver/utils/file_utilities'
4
+ require 'puppetserver/ca/puppet_config'
5
+
6
+ require 'optparse'
7
+ require 'json'
8
+
9
+ module Puppetserver
10
+ module Ca
11
+ class RevokeAction
12
+
13
+ include Puppetserver::Utils
14
+
15
+ REQUEST_BODY = JSON.dump({ desired_state: 'revoked' })
16
+ CERTNAME_BLACKLIST = %w{--all --config}
17
+
18
+ SUMMARY = 'Revoke a given certificate'
19
+ BANNER = <<-BANNER
20
+ Usage:
21
+ puppetserver ca revoke [--help|--version]
22
+ puppetserver ca revoke [--config] --certname CERTNAME[,ADDLCERTNAME]
23
+
24
+ Description:
25
+ Given one or more valid certnames, instructs the CA to revoke them over
26
+ HTTPS using the local agent's PKI
27
+
28
+ Options:
29
+ BANNER
30
+
31
+ def self.parser(parsed = {})
32
+ parsed['certnames'] = []
33
+ OptionParser.new do |o|
34
+ o.banner = BANNER
35
+ o.on('--certname foo,bar', Array,
36
+ 'One or more comma separated certnames') do |certs|
37
+ parsed['certnames'] += certs
38
+ end
39
+ o.on('--config PUPPET.CONF', 'Custom path to puppet.conf') do |conf|
40
+ parsed['config'] = conf
41
+ end
42
+ o.on('--help', 'Displays this revoke specific help output') do |help|
43
+ parsed['help'] = help
44
+ end
45
+ end
46
+ end
47
+
48
+ def initialize(logger)
49
+ @logger = logger
50
+ end
51
+
52
+ def parse(args)
53
+ results = {}
54
+ parser = self.class.parser(results)
55
+
56
+ errors = Utils.parse_with_errors(parser, args)
57
+
58
+ results['certnames'].each do |certname|
59
+ if CERTNAME_BLACKLIST.include?(certname)
60
+ errors << " Cannot manage cert named `#{certname}` from " +
61
+ "the CLI, if needed use the HTTP API directly"
62
+ end
63
+ end
64
+
65
+ if results['certnames'].empty?
66
+ errors << ' At least one certname is required to revoke'
67
+ end
68
+
69
+ errors_were_handled = Utils.handle_errors(@logger, errors, parser.help)
70
+
71
+ # if there is an exit_code then Cli will return it early, so we only
72
+ # return an exit_code if there's an error
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 = revoke_certs(certnames, puppet.settings)
91
+
92
+ return passed ? 0 : 1
93
+ end
94
+
95
+ def revoke_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 will be a list of trues & falses based on the success
105
+ # of revocations
106
+ results = client.with_connection(url) do |connection|
107
+ certnames.map do |certname|
108
+ url.resource_name = certname
109
+ result = connection.put(REQUEST_BODY, url)
110
+
111
+ check_result(result, certname)
112
+ end
113
+ end
114
+
115
+ return results.all?
116
+ end
117
+
118
+ # logs the action and returns a boolean for success/failure
119
+ def check_result(result, certname)
120
+ case result.code
121
+ when '200', '204'
122
+ @logger.inform "Revoked certificate for #{certname}"
123
+ return true
124
+ when '404'
125
+ @logger.err 'Error:'
126
+ @logger.err " Could not find certificate for #{certname}"
127
+ return false
128
+ else
129
+ @logger.err 'Error:'
130
+ @logger.err " When revoking #{certname} received:"
131
+ @logger.err " code: #{result.code}"
132
+ @logger.err " body: #{result.body.to_s}" if result.body
133
+ return false
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,192 @@
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 'optparse'
6
+ require 'openssl'
7
+ require 'net/https'
8
+ require 'json'
9
+
10
+ module Puppetserver
11
+ module Ca
12
+ class SignAction
13
+
14
+ include Puppetserver::Utils
15
+
16
+ SUMMARY = 'Sign a given certificate'
17
+ BANNER = <<-BANNER
18
+ Usage:
19
+ puppetserver ca sign [--help|--version]
20
+ puppetserver ca sign [--config] --certname CERTNAME[,CERTNAME]
21
+ puppetserver ca sign --all
22
+
23
+ Description:
24
+ Given a comma-separated list of valid certnames, instructs the CA to sign each cert.
25
+
26
+ Options:
27
+ BANNER
28
+ BODY = JSON.dump({desired_state: 'signed'})
29
+
30
+ def self.parser(parsed = {})
31
+ OptionParser.new do |opts|
32
+ opts.banner = BANNER
33
+ opts.on('--certname x,y,z', Array, 'the name(s) of the cert(s) to be signed') do |cert|
34
+ parsed['certname'] = cert
35
+ end
36
+ opts.on('--config PUPPET.CONF', 'Custom path to Puppet\'s config file') do |conf|
37
+ parsed['config'] = conf
38
+ end
39
+ opts.on('--help', 'Display this command specific help output') do |help|
40
+ parsed['help'] = true
41
+ end
42
+ opts.on('--version', 'Output the version') do |v|
43
+ parsed['version'] = true
44
+ end
45
+ opts.on('--all', 'Operate on all certnames') do |a|
46
+ parsed['all'] = true
47
+ end
48
+ end
49
+ end
50
+
51
+ def initialize(logger)
52
+ @logger = logger
53
+ end
54
+
55
+ def run(input)
56
+ config = input['config']
57
+
58
+ if config
59
+ errors = FileUtilities.validate_file_paths(config)
60
+ return 1 if Utils.handle_errors(@logger, errors)
61
+ end
62
+
63
+ puppet = PuppetConfig.parse(config)
64
+ return 1 if Utils.handle_errors(@logger, puppet.errors)
65
+
66
+ if input['all']
67
+ requested_certnames = get_all_pending_certs(puppet.settings)
68
+ if requested_certnames.nil?
69
+ return 1
70
+ end
71
+ else
72
+ requested_certnames = input['certname']
73
+ end
74
+
75
+ success = sign_requested_certs(requested_certnames, puppet.settings)
76
+ return success ? 0 : 1
77
+ end
78
+
79
+ def http_client(settings)
80
+ @client ||= HttpClient.new(settings[:localcacert],
81
+ settings[:certificate_revocation],
82
+ settings[:hostcrl])
83
+ end
84
+
85
+ def get_certificate_statuses(settings)
86
+ client = http_client(settings)
87
+ url = client.make_ca_url(settings[:ca_server],
88
+ settings[:ca_port],
89
+ 'certificate_statuses',
90
+ 'any_key')
91
+ client.with_connection(url) do |connection|
92
+ connection.get(url)
93
+ end
94
+ end
95
+
96
+ def sign_certs(certnames,settings)
97
+ results = {}
98
+ client = http_client(settings)
99
+ url = client.make_ca_url(settings[:ca_server],
100
+ settings[:ca_port],
101
+ 'certificate_status')
102
+ client.with_connection(url) do |connection|
103
+ certnames.each do |certname|
104
+ url.resource_name = certname
105
+ results[certname] = connection.put(BODY, url)
106
+ end
107
+ end
108
+ return results
109
+ end
110
+
111
+ def get_all_certs(settings)
112
+ result = get_certificate_statuses(settings)
113
+
114
+ unless result.code == 200
115
+ @logger.err 'Error:'
116
+ @logger.err " #{result.inspect}"
117
+ return nil
118
+ end
119
+ return result
120
+ end
121
+
122
+ def select_pending_certs(get_result)
123
+ requested_certnames = JSON.parse(get_result).select{|e| e["state"] == "requested"}.map{|e| e["name"]}
124
+
125
+ if requested_certnames.empty?
126
+ @logger.err 'Error:'
127
+ @logger.err " No waiting certificate requests to sign"
128
+ return nil
129
+ end
130
+
131
+ return requested_certnames
132
+ end
133
+
134
+ def get_all_pending_certs(settings)
135
+ result = get_all_certs(settings)
136
+ if result
137
+ select_pending_certs(result.body)
138
+ end
139
+ end
140
+
141
+ def sign_requested_certs(certnames,settings)
142
+ success = true
143
+ results = sign_certs(certnames, settings)
144
+ results.each do |certname, result|
145
+ case result.code
146
+ when '204'
147
+ @logger.inform "Signed certificate for #{certname}"
148
+ when '404'
149
+ @logger.err 'Error:'
150
+ @logger.err " Could not find certificate for #{certname}"
151
+ success = false
152
+ else
153
+ @logger.err 'Error:'
154
+ @logger.err " When download requested for #{result.inspect}"
155
+ @logger.err " code: #{result.code}"
156
+ @logger.err " body: #{result.body.to_s}" if result.body
157
+ success = false
158
+ end
159
+ end
160
+ return success
161
+ end
162
+
163
+ def check_flag_usage(results)
164
+ if results['certname'] && results['all']
165
+ '--all and --certname cannot be used together'
166
+ elsif !results['certname'] && !results['all']
167
+ 'No arguments given'
168
+ elsif results['certname'] && results['certname'].include?('--all')
169
+ 'Cannot use --all with --certname. If you actually have a certificate request ' +
170
+ 'for a certifcate named --all, you need to use the HTTP API.'
171
+ end
172
+ end
173
+
174
+ def parse(args)
175
+ results = {}
176
+ parser = self.class.parser(results)
177
+
178
+ errors = Utils.parse_with_errors(parser, args)
179
+
180
+ if check_flag_usage(results)
181
+ errors << check_flag_usage(results)
182
+ end
183
+
184
+ errors_were_handled = Utils.handle_errors(@logger, errors, parser.help)
185
+
186
+ exit_code = errors_were_handled ? 1 : nil
187
+
188
+ return results, exit_code
189
+ end
190
+ end
191
+ end
192
+ end