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 +4 -4
- data/lib/puppetserver/ca/clean_action.rb +159 -0
- data/lib/puppetserver/ca/cli.rb +25 -18
- data/lib/puppetserver/ca/create_action.rb +267 -0
- data/lib/puppetserver/ca/generate_action.rb +227 -0
- data/lib/puppetserver/ca/host.rb +26 -0
- data/lib/puppetserver/ca/import_action.rb +11 -69
- data/lib/puppetserver/ca/list_action.rb +155 -0
- data/lib/puppetserver/ca/puppet_config.rb +71 -9
- data/lib/puppetserver/ca/revoke_action.rb +138 -0
- data/lib/puppetserver/ca/sign_action.rb +192 -0
- data/lib/puppetserver/ca/utils.rb +80 -0
- data/lib/puppetserver/ca/version.rb +1 -1
- data/lib/puppetserver/settings/ttl_setting.rb +48 -0
- data/lib/puppetserver/utils/file_utilities.rb +78 -0
- data/lib/puppetserver/utils/http_client.rb +116 -0
- data/lib/puppetserver/utils/signing_digest.rb +25 -0
- data/puppetserver-ca.gemspec +2 -0
- metadata +34 -2
@@ -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
|
-
|
62
|
-
|
63
|
-
|
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[:
|
82
|
-
|
83
|
-
|
84
|
-
settings[:
|
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
|
-
|
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 $
|
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
|