puppetserver-ca 0.3.1 → 0.4.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.
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