puppetserver-ca 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/puppetserver/ca/action/clean.rb +102 -0
  3. data/lib/puppetserver/ca/action/create.rb +161 -0
  4. data/lib/puppetserver/ca/action/generate.rb +313 -0
  5. data/lib/puppetserver/ca/action/import.rb +132 -0
  6. data/lib/puppetserver/ca/action/list.rb +132 -0
  7. data/lib/puppetserver/ca/action/revoke.rb +101 -0
  8. data/lib/puppetserver/ca/action/sign.rb +126 -0
  9. data/lib/puppetserver/ca/certificate_authority.rb +224 -0
  10. data/lib/puppetserver/ca/cli.rb +17 -16
  11. data/lib/puppetserver/ca/config/puppet.rb +242 -0
  12. data/lib/puppetserver/ca/config/puppetserver.rb +85 -0
  13. data/lib/puppetserver/ca/utils/cli_parsing.rb +82 -0
  14. data/lib/puppetserver/ca/utils/config.rb +13 -0
  15. data/lib/puppetserver/ca/utils/file_system.rb +90 -0
  16. data/lib/puppetserver/ca/utils/http_client.rb +129 -0
  17. data/lib/puppetserver/ca/utils/signing_digest.rb +27 -0
  18. data/lib/puppetserver/ca/version.rb +1 -1
  19. metadata +17 -17
  20. data/lib/puppetserver/ca/clean_action.rb +0 -157
  21. data/lib/puppetserver/ca/config_utils.rb +0 -11
  22. data/lib/puppetserver/ca/create_action.rb +0 -265
  23. data/lib/puppetserver/ca/generate_action.rb +0 -227
  24. data/lib/puppetserver/ca/import_action.rb +0 -153
  25. data/lib/puppetserver/ca/list_action.rb +0 -153
  26. data/lib/puppetserver/ca/puppet_config.rb +0 -197
  27. data/lib/puppetserver/ca/puppetserver_config.rb +0 -83
  28. data/lib/puppetserver/ca/revoke_action.rb +0 -136
  29. data/lib/puppetserver/ca/sign_action.rb +0 -190
  30. data/lib/puppetserver/ca/utils.rb +0 -80
  31. data/lib/puppetserver/settings/ttl_setting.rb +0 -48
  32. data/lib/puppetserver/utils/file_utilities.rb +0 -78
  33. data/lib/puppetserver/utils/http_client.rb +0 -129
  34. data/lib/puppetserver/utils/signing_digest.rb +0 -25
@@ -0,0 +1,224 @@
1
+ require 'puppetserver/ca/utils/http_client'
2
+
3
+ require 'json'
4
+
5
+ module Puppetserver
6
+ module Ca
7
+ class CertificateAuthority
8
+
9
+ include Puppetserver::Ca::Utils
10
+
11
+ REVOKE_BODY = JSON.dump({ desired_state: 'revoked' })
12
+ SIGN_BODY = JSON.dump({ desired_state: 'signed' })
13
+
14
+ def initialize(logger, settings)
15
+ @logger = logger
16
+ @client = HttpClient.new(settings)
17
+ @ca_server = settings[:ca_server]
18
+ @ca_port = settings[:ca_port]
19
+ end
20
+
21
+ # Returns a URI-like wrapper around CA specific urls
22
+ def make_ca_url(resource_type = nil, certname = nil)
23
+ HttpClient::URL.new('https', @ca_server, @ca_port, 'puppet-ca', 'v1', resource_type, certname)
24
+ end
25
+
26
+ def sign_certs(certnames)
27
+ put(certnames,
28
+ resource_type: 'certificate_status',
29
+ body: SIGN_BODY,
30
+ type: :sign)
31
+ end
32
+
33
+ def revoke_certs(certnames)
34
+ put(certnames,
35
+ resource_type: 'certificate_status',
36
+ body: REVOKE_BODY,
37
+ type: :revoke)
38
+ end
39
+
40
+ def submit_certificate_request(certname, csr)
41
+ put([certname],
42
+ resource_type: 'certificate_request',
43
+ body: csr.to_pem,
44
+ headers: {'Content-Type' => 'text/plain'},
45
+ type: :submit)
46
+ end
47
+
48
+ # Make an HTTP PUT request to CA
49
+ # @param resource_type [String] the resource type of url
50
+ # @param certnames [Array] array of certnames
51
+ # @param body [JSON/String] body of the put request
52
+ # @param type [Symbol] type of error processing to perform on result
53
+ # @return [Boolean] whether all requests were successful
54
+ def put(certnames, resource_type:, body:, type:, headers: {})
55
+ url = make_ca_url(resource_type)
56
+ results = @client.with_connection(url) do |connection|
57
+ certnames.map do |certname|
58
+ url.resource_name = certname
59
+ result = connection.put(body, url, headers)
60
+ process_results(type, certname, result)
61
+ end
62
+ end
63
+
64
+ results.all?
65
+ end
66
+
67
+ # logs the action and returns true/false for success
68
+ def process_results(type, certname, result)
69
+ case type
70
+ when :sign
71
+ case result.code
72
+ when '204'
73
+ @logger.inform "Successfully signed certificate request for #{certname}"
74
+ return true
75
+ when '404'
76
+ @logger.err 'Error:'
77
+ @logger.err " Could not find certificate request for #{certname}"
78
+ return false
79
+ else
80
+ @logger.err 'Error:'
81
+ @logger.err " When attempting to sign certificate request '#{certname}', received"
82
+ @logger.err " code: #{result.code}"
83
+ @logger.err " body: #{result.body.to_s}" if result.body
84
+ return false
85
+ end
86
+ when :revoke
87
+ case result.code
88
+ when '200', '204'
89
+ @logger.inform "Revoked certificate for #{certname}"
90
+ return true
91
+ when '404'
92
+ @logger.err 'Error:'
93
+ @logger.err " Could not find certificate for #{certname}"
94
+ return false
95
+ else
96
+ @logger.err 'Error:'
97
+ @logger.err " When attempting to revoke certificate '#{certname}', received:"
98
+ @logger.err " code: #{result.code}"
99
+ @logger.err " body: #{result.body.to_s}" if result.body
100
+ return false
101
+ end
102
+ when :submit
103
+ case result.code
104
+ when '200', '204'
105
+ @logger.inform "Successfully submitted certificate request for #{certname}"
106
+ return true
107
+ else
108
+ @logger.err 'Error:'
109
+ @logger.err " When attempting to submit certificate request for '#{certname}', received:"
110
+ @logger.err " code: #{result.code}"
111
+ @logger.err " body: #{result.body.to_s}" if result.body
112
+ return false
113
+ end
114
+ end
115
+ end
116
+
117
+ # Make an HTTP request to CA to clean the named certificates
118
+ # @param certnames [Array] the name of the certificate(s) to have cleaned
119
+ # @return [Boolean] whether all certificate cleaning and revocation was successful
120
+ def clean_certs(certnames)
121
+ url = make_ca_url('certificate_status')
122
+
123
+ results = @client.with_connection(url) do |connection|
124
+ certnames.map do |certname|
125
+ url.resource_name = certname
126
+ revoke_result = connection.put(REVOKE_BODY, url)
127
+ revoked = check_revocation(certname, revoke_result)
128
+
129
+ cleaned = nil
130
+ unless revoked == :error
131
+ clean_result = connection.delete(url)
132
+ cleaned = check_clean(certname, clean_result)
133
+ end
134
+
135
+ cleaned == :success && [:success, :not_found].include?(revoked)
136
+ end
137
+ end
138
+
139
+ return results.all?
140
+ end
141
+
142
+ # possibly logs the action, always returns a status symbol 👑
143
+ def check_revocation(certname, result)
144
+ case result.code
145
+ when '200', '204'
146
+ @logger.inform "Revoked certificate for #{certname}"
147
+ return :success
148
+ when '404'
149
+ return :not_found
150
+ else
151
+ @logger.err 'Error:'
152
+ @logger.err " When attempting to revoke certificate '#{certname}', received:"
153
+ @logger.err " code: #{result.code}"
154
+ @logger.err " body: #{result.body.to_s}" if result.body
155
+ return :error
156
+ end
157
+ end
158
+
159
+ # logs the action and returns a status symbol 👑
160
+ def check_clean(certname, result)
161
+ case result.code
162
+ when '200', '204'
163
+ @logger.inform "Cleaned files related to #{certname}"
164
+ return :success
165
+ when '404'
166
+ @logger.err 'Error:'
167
+ @logger.err " Could not find files to clean for #{certname}"
168
+ return :not_found
169
+ else
170
+ @logger.err 'Error:'
171
+ @logger.err " When attempting to clean certificate '#{certname}', received:"
172
+ @logger.err " code: #{result.code}"
173
+ @logger.err " body: #{result.body.to_s}" if result.body
174
+ return :error
175
+ end
176
+ end
177
+
178
+ # Returns nil for errors, else the result of the GET request
179
+ def get_certificate_statuses
180
+ result = get('certificate_statuses', 'any_key')
181
+
182
+ unless result.code == '200'
183
+ @logger.err 'Error:'
184
+ @logger.err " code: #{result.code}"
185
+ @logger.err " body: #{result.body}" if result.body
186
+ return nil
187
+ end
188
+
189
+ result
190
+ end
191
+
192
+ # Returns nil for errors, else the result of the GET request
193
+ def get_certificate(certname)
194
+ result = get('certificate', certname)
195
+
196
+ case result.code
197
+ when '200'
198
+ return result
199
+ when '404'
200
+ @logger.err 'Error:'
201
+ @logger.err " Signed certificate #{certname} could not be found on the CA"
202
+ return nil
203
+ else
204
+ @logger.err 'Error:'
205
+ @logger.err " When attempting to download certificate '#{certname}', received:"
206
+ @logger.err " code: #{result.code}"
207
+ @logger.err " body: #{result.body.to_s}" if result.body
208
+ return nil
209
+ end
210
+ end
211
+
212
+ # Make an HTTP GET request to CA
213
+ # @param resource_type [String] the resource type of url
214
+ # @param resource_name [String] the resource name of url
215
+ # @return [Struct] an instance of the Result struct with :code, :body
216
+ def get(resource_type, resource_name)
217
+ url = make_ca_url(resource_type, resource_name)
218
+ @client.with_connection(url) do |connection|
219
+ connection.get(url)
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -1,14 +1,15 @@
1
1
  require 'optparse'
2
2
  require 'puppetserver/ca/version'
3
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'
4
+ require 'puppetserver/ca/action/clean'
5
+ require 'puppetserver/ca/action/create'
6
+ require 'puppetserver/ca/action/import'
7
+ require 'puppetserver/ca/action/generate'
8
+ require 'puppetserver/ca/action/revoke'
9
+ require 'puppetserver/ca/action/list'
10
+ require 'puppetserver/ca/action/sign'
11
+ require 'puppetserver/ca/utils/cli_parsing'
12
+
12
13
 
13
14
  module Puppetserver
14
15
  module Ca
@@ -21,13 +22,13 @@ Puppet Server's built-in Certificate Authority
21
22
  BANNER
22
23
 
23
24
  VALID_ACTIONS = {
24
- 'clean' => CleanAction,
25
- 'create' => CreateAction,
26
- 'generate' => GenerateAction,
27
- 'import' => ImportAction,
28
- 'list' => ListAction,
29
- 'revoke' => RevokeAction,
30
- 'sign' => SignAction
25
+ 'clean' => Action::Clean,
26
+ 'create' => Action::Create,
27
+ 'generate' => Action::Generate,
28
+ 'import' => Action::Import,
29
+ 'list' => Action::List,
30
+ 'revoke' => Action::Revoke,
31
+ 'sign' => Action::Sign
31
32
  }
32
33
 
33
34
  ACTION_LIST = "\nAvailable Actions:\n" +
@@ -103,7 +104,7 @@ BANNER
103
104
 
104
105
  end
105
106
 
106
- all,_,_,_ = Utils.parse_without_raising(general_parser, inputs)
107
+ all,_,_,_ = Utils::CliParsing.parse_without_raising(general_parser, inputs)
107
108
 
108
109
  return general_parser, parsed, all
109
110
  end
@@ -0,0 +1,242 @@
1
+ require 'puppetserver/ca/utils/config'
2
+ require 'securerandom'
3
+ require 'facter'
4
+
5
+ module Puppetserver
6
+ module Ca
7
+ module Config
8
+ # Provides an interface for asking for Puppet settings w/o loading
9
+ # Puppet. Includes a simple ini parser that will ignore Puppet's
10
+ # more complicated conventions.
11
+ class Puppet
12
+ # How we convert from various units to seconds.
13
+ TTL_UNITMAP = {
14
+ # 365 days isn't technically a year, but is sufficient for most purposes
15
+ "y" => 365 * 24 * 60 * 60,
16
+ "d" => 24 * 60 * 60,
17
+ "h" => 60 * 60,
18
+ "m" => 60,
19
+ "s" => 1
20
+ }
21
+
22
+ # A regex describing valid formats with groups for capturing the value and units
23
+ TTL_FORMAT = /^(\d+)(y|d|h|m|s)?$/
24
+
25
+ include Puppetserver::Ca::Utils::Config
26
+
27
+ def self.parse(config_path = nil)
28
+ instance = new(config_path)
29
+ instance.load
30
+
31
+ return instance
32
+ end
33
+
34
+ attr_reader :errors, :settings
35
+
36
+ def initialize(supplied_config_path = nil)
37
+ @using_default_location = !supplied_config_path
38
+ @config_path = supplied_config_path || user_specific_conf_file
39
+
40
+ @settings = nil
41
+ @errors = []
42
+ end
43
+
44
+ # Return the correct confdir. We check for being root on *nix,
45
+ # else the user path. We do not include a check for running
46
+ # as Adminstrator since non-development scenarios for Puppet Server
47
+ # on Windows are unsupported.
48
+ # Note that Puppet Server runs as the [pe-]puppet user but to
49
+ # start/stop it you must be root.
50
+ def user_specific_conf_dir
51
+ @user_specific_conf_dir ||=
52
+ if running_as_root?
53
+ '/etc/puppetlabs/puppet'
54
+ else
55
+ "#{ENV['HOME']}/.puppetlabs/etc/puppet"
56
+ end
57
+ end
58
+
59
+ def user_specific_conf_file
60
+ user_specific_conf_dir + '/puppet.conf'
61
+ end
62
+
63
+ def load
64
+ if explicitly_given_config_file_or_default_config_exists?
65
+ results = parse_text(File.read(@config_path))
66
+ end
67
+
68
+ results ||= {}
69
+ results[:main] ||= {}
70
+ results[:master] ||= {}
71
+
72
+ overrides = results[:main].merge(results[:master])
73
+
74
+ @settings = resolve_settings(overrides).freeze
75
+ end
76
+
77
+ def default_certname
78
+ @certname ||=
79
+ hostname = Facter.value(:hostname)
80
+ domain = Facter.value(:domain)
81
+ if domain and domain != ''
82
+ fqdn = [hostname, domain].join('.')
83
+ else
84
+ fqdn = hostname
85
+ end
86
+ fqdn.chomp('.')
87
+ end
88
+
89
+ # Resolve settings from default values, with any overrides for the
90
+ # specific settings or their dependent settings (ssldir, cadir) taken into account.
91
+ def resolve_settings(overrides = {})
92
+ unresolved_setting = /\$[a-z_]+/
93
+
94
+ # Returning the key for unknown keys (rather than nil) is required to
95
+ # keep unknown settings in the string for later verification.
96
+ substitutions = Hash.new {|h, k| k }
97
+ settings = {}
98
+
99
+ # Order for base settings here matters!
100
+ # These need to be evaluated before we can construct their dependent
101
+ # defaults below
102
+ base_defaults = [
103
+ [:confdir, user_specific_conf_dir],
104
+ [:ssldir,'$confdir/ssl'],
105
+ [:cadir, '$ssldir/ca'],
106
+ [:certdir, '$ssldir/certs'],
107
+ [:certname, default_certname],
108
+ [:server, '$certname'],
109
+ [:masterport, '8140'],
110
+ [:privatekeydir, '$ssldir/private_keys'],
111
+ [:publickeydir, '$ssldir/public_keys'],
112
+ ]
113
+
114
+ dependent_defaults = {
115
+ :ca_name => 'Puppet CA: $certname',
116
+ :root_ca_name => "Puppet Root CA: #{SecureRandom.hex(7)}",
117
+ :keylength => 4096,
118
+ :cacert => '$cadir/ca_crt.pem',
119
+ :cakey => '$cadir/ca_key.pem',
120
+ :capub => '$cadir/ca_pub.pem',
121
+ :rootkey => '$cadir/root_key.pem',
122
+ :cacrl => '$cadir/ca_crl.pem',
123
+ :serial => '$cadir/serial',
124
+ :cert_inventory => '$cadir/inventory.txt',
125
+ :ca_server => '$server',
126
+ :ca_port => '$masterport',
127
+ :localcacert => '$certdir/ca.pem',
128
+ :localcacrl => '$ssldir/crl.pem',
129
+ :hostcert => '$certdir/$certname.pem',
130
+ :hostcrl => '$ssldir/crl.pem',
131
+ :hostprivkey => '$privatekeydir/$certname.pem',
132
+ :hostpubkey => '$publickeydir/$certname.pem',
133
+ :publickeydir => '$ssldir/public_keys',
134
+ :ca_ttl => '15y',
135
+ :certificate_revocation => 'true',
136
+ }
137
+
138
+ # This loops through the base defaults and gives each setting a
139
+ # default if the value isn't specified in the config file. Default
140
+ # values given may depend upon the value of a previous base setting,
141
+ # thus the creation of the substitution hash.
142
+ base_defaults.each do |setting_name, default_value|
143
+ substitution_name = '$' + setting_name.to_s
144
+ setting_value = overrides.fetch(setting_name, default_value)
145
+ subbed_value = setting_value.sub(unresolved_setting, substitutions)
146
+ settings[setting_name] = substitutions[substitution_name] = subbed_value
147
+ end
148
+
149
+ dependent_defaults.each do |setting_name, default_value|
150
+ setting_value = overrides.fetch(setting_name, default_value)
151
+ settings[setting_name] = setting_value
152
+ end
153
+
154
+ # Some special cases where we need to manipulate config settings:
155
+ settings[:ca_ttl] = munge_ttl_setting(settings[:ca_ttl])
156
+ settings[:certificate_revocation] = parse_crl_usage(settings[:certificate_revocation])
157
+
158
+ # rename dns_alt_names to subject_alt_names now that we support IP alt names
159
+ settings[:subject_alt_names] = overrides.fetch(:dns_alt_names, "puppet,$certname")
160
+
161
+ settings.each do |key, value|
162
+ next unless value.is_a? String
163
+ settings[key] = value.gsub(unresolved_setting, substitutions)
164
+ if match = settings[key].match(unresolved_setting)
165
+ @errors << "Could not parse #{match[0]} in #{value}, " +
166
+ 'valid settings to be interpolated are ' +
167
+ '$ssldir, $certdir, $cadir, $certname, $server, or $masterport'
168
+ end
169
+ end
170
+
171
+ return settings
172
+ end
173
+
174
+ # Parse an inifile formatted String. Only captures \word character
175
+ # class keys/section names but nearly any character values (excluding
176
+ # leading whitespace) up to one of whitespace, opening curly brace, or
177
+ # hash sign (Our concern being to capture filesystem path values).
178
+ # Put values without a section into :main.
179
+ #
180
+ # Return Hash of Symbol section names with Symbol setting keys and
181
+ # String values.
182
+ def parse_text(text)
183
+ res = {}
184
+ current_section = :main
185
+ text.each_line do |line|
186
+ case line
187
+ when /^\s*\[(\w+)\].*/
188
+ current_section = $1.to_sym
189
+ when /^\s*(\w+)\s*=\s*([^\s{#]+).*$/
190
+ # Using a Hash with a default key breaks RSpec expectations.
191
+ res[current_section] ||= {}
192
+ res[current_section][$1.to_sym] = $2
193
+ end
194
+ end
195
+
196
+ res
197
+ end
198
+
199
+ private
200
+
201
+ def explicitly_given_config_file_or_default_config_exists?
202
+ !@using_default_location || File.exist?(@config_path)
203
+ end
204
+
205
+ def run(command)
206
+ %x( #{command} )
207
+ end
208
+
209
+ # Convert the value to Numeric, parsing numeric string with units if necessary.
210
+ def munge_ttl_setting(ca_ttl_setting)
211
+ case
212
+ when ca_ttl_setting.is_a?(Numeric)
213
+ if ca_ttl_setting < 0
214
+ @errors << "Invalid negative 'time to live' #{ca_ttl_setting.inspect} - did you mean 'unlimited'?"
215
+ end
216
+ ca_ttl_setting
217
+
218
+ when ca_ttl_setting == 'unlimited'
219
+ Float::INFINITY
220
+
221
+ when (ca_ttl_setting.is_a?(String) and ca_ttl_setting =~ TTL_FORMAT)
222
+ $1.to_i * TTL_UNITMAP[$2 || 's']
223
+ else
224
+ @errors << "Invalid 'time to live' format '#{ca_ttl_setting.inspect}' for parameter: :ca_ttl"
225
+ end
226
+ end
227
+
228
+
229
+ def parse_crl_usage(setting)
230
+ case setting.to_s
231
+ when 'true', 'chain'
232
+ :chain
233
+ when 'leaf'
234
+ :leaf
235
+ when 'false'
236
+ :ignore
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end