Class: Puppet::SSL::Host

Inherits:
Object show all
Extended by:
Indirector
Defined in:
lib/puppet/ssl/host.rb

Overview

The class that manages all aspects of our SSL certificates – private keys, public keys, requests, etc.

Constant Summary collapse

Key =

Yay, ruby’s strange constant lookups.

Puppet::SSL::Key
CA_NAME =
Puppet::SSL::CA_NAME
Certificate =
Puppet::SSL::Certificate
CertificateRequest =
Puppet::SSL::CertificateRequest
CertificateRevocationList =
Puppet::SSL::CertificateRevocationList
CA_MODES =
{
  # Our ca is local, so we use it as the ultimate source of information
  # And we cache files locally.
  :local => [:ca, :file],
  # We're a remote CA client.
  :remote => [:rest, :file],
  # We are the CA, so we don't have read/write access to the normal certificates.
  :only => [:ca],
  # We have no CA, so we just look in the local file store.
  :none => [:disabled_ca]
}

Constants included from Indirector

Indirector::BadNameRegexp

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Indirector

configure_routes, indirects

Constructor Details

#initialize(name = nil) ⇒ Host

Returns a new instance of Host.



262
263
264
265
266
267
# File 'lib/puppet/ssl/host.rb', line 262

def initialize(name = nil)
  @name = (name || Puppet[:certname]).downcase
  Puppet::SSL::Base.validate_certname(@name)
  @key = @certificate = @certificate_request = nil
  @ca = (name == self.class.ca_name)
end

Class Attribute Details

.ca_locationObject

Returns the value of attribute ca_location.



52
53
54
# File 'lib/puppet/ssl/host.rb', line 52

def ca_location
  @ca_location
end

Instance Attribute Details

#caObject

Returns the value of attribute ca.



26
27
28
# File 'lib/puppet/ssl/host.rb', line 26

def ca
  @ca
end

#certificateObject



195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/puppet/ssl/host.rb', line 195

def certificate
  unless @certificate
    generate_key unless key

    # get the CA cert first, since it's required for the normal cert
    # to be of any use.
    return nil unless Certificate.indirection.find("ca", :fail_on_404 => true) unless ca?
    return nil unless @certificate = Certificate.indirection.find(name)

    validate_certificate_with_key
  end
  @certificate
end

#certificate_requestObject



159
160
161
# File 'lib/puppet/ssl/host.rb', line 159

def certificate_request
  @certificate_request ||= CertificateRequest.indirection.find(name)
end

#desired_stateObject

This accessor is used in instances for indirector requests to hold desired state



31
32
33
# File 'lib/puppet/ssl/host.rb', line 31

def desired_state
  @desired_state
end

#keyObject



141
142
143
# File 'lib/puppet/ssl/host.rb', line 141

def key
  @key ||= Key.indirection.find(name)
end

#nameObject (readonly)

Returns the value of attribute name.



25
26
27
# File 'lib/puppet/ssl/host.rb', line 25

def name
  @name
end

Class Method Details

.ca_nameObject

This is the constant that people will use to mark that a given host is a certificate authority.



47
48
49
# File 'lib/puppet/ssl/host.rb', line 47

def self.ca_name
  CA_NAME
end

.configure_indirection(terminus, cache = nil) ⇒ Object

Configure how our various classes interact with their various terminuses.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/puppet/ssl/host.rb', line 56

def self.configure_indirection(terminus, cache = nil)
  Certificate.indirection.terminus_class = terminus
  CertificateRequest.indirection.terminus_class = terminus
  CertificateRevocationList.indirection.terminus_class = terminus

  host_map = {:ca => :file, :disabled_ca => nil, :file => nil, :rest => :rest}
  if term = host_map[terminus]
    self.indirection.terminus_class = term
  else
    self.indirection.reset_terminus_class
  end

  if cache
    # This is weird; we don't actually cache our keys, we
    # use what would otherwise be the cache as our normal
    # terminus.
    Key.indirection.terminus_class = cache
  else
    Key.indirection.terminus_class = terminus
  end

  if cache
    Certificate.indirection.cache_class = cache
    CertificateRequest.indirection.cache_class = cache
    CertificateRevocationList.indirection.cache_class = cache
  else
    # Make sure we have no cache configured.  puppet master
    # switches the configurations around a bit, so it's important
    # that we specify the configs for absolutely everything, every
    # time.
    Certificate.indirection.cache_class = nil
    CertificateRequest.indirection.cache_class = nil
    CertificateRevocationList.indirection.cache_class = nil
  end
end

.destroy(name) ⇒ Object

Puppet::SSL::Host is actually indirected now so the original implementation has been moved into the certificate_status indirector. This method is in-use in ‘puppet cert -c <certname>`.



117
118
119
# File 'lib/puppet/ssl/host.rb', line 117

def self.destroy(name)
  indirection.destroy(name)
end

.from_data_hash(data) ⇒ Object



121
122
123
124
125
126
127
# File 'lib/puppet/ssl/host.rb', line 121

def self.from_data_hash(data)
  instance = new(data["name"])
  if data["desired_state"]
    instance.desired_state = data["desired_state"]
  end
  instance
end

.localhostObject



33
34
35
36
37
38
39
# File 'lib/puppet/ssl/host.rb', line 33

def self.localhost
  return @localhost if @localhost
  @localhost = new
  @localhost.generate unless @localhost.certificate
  @localhost.key
  @localhost
end

.resetObject



41
42
43
# File 'lib/puppet/ssl/host.rb', line 41

def self.reset
  @localhost = nil
end

.search(options = {}) ⇒ Object

Puppet::SSL::Host is actually indirected now so the original implementation has been moved into the certificate_status indirector. This method does not appear to be in use in ‘puppet cert -l`.



132
133
134
# File 'lib/puppet/ssl/host.rb', line 132

def self.search(options = {})
  indirection.search("*", options)
end

Instance Method Details

#ca?Boolean

Is this a ca host, meaning that all of its files go in the CA location?

Returns:

  • (Boolean)


137
138
139
# File 'lib/puppet/ssl/host.rb', line 137

def ca?
  ca
end

#generateObject

Generate all necessary parts of our ssl host.



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/puppet/ssl/host.rb', line 228

def generate
  generate_key unless key
  # ask indirector to find any existing requests and download them
  existing_request = certificate_request

  # if CSR downloaded from master, but the local keypair was just generated and
  # does not match the public key in the CSR, fail hard
  if !existing_request.nil? &&
    (key.content.public_key.to_s != existing_request.content.public_key.to_s)

    raise Puppet::Error, _(<<ERROR_STRING) % { fingerprint: existing_request.fingerprint, csr_public_key: existing_request.content.public_key.to_text, agent_public_key: key.content.public_key.to_text, cert_name: Puppet[:certname], ssl_dir: Puppet[:ssldir], cert_dir: Puppet[:certdir].gsub('/', '\\') }
The CSR retrieved from the master does not match the agent's public key.
CSR fingerprint: %{fingerprint}
CSR public key: %{csr_public_key}
Agent public key: %{agent_public_key}
To fix this, remove the CSR from both the master and the agent and then start a puppet run, which will automatically regenerate a CSR.
On the master:
puppet cert clean %{cert_name}
On the agent:
1a. On most platforms: find %{ssl_dir} -name %{cert_name}.pem -delete
1b. On Windows: del "%{cert_dir}\\%{cert_name}.pem" /f
2. puppet agent -t
ERROR_STRING
  end
  generate_certificate_request unless existing_request

  # If we can get a CA instance, then we're a valid CA, and we
  # should use it to sign our request; else, just try to read
  # the cert.
  if ! certificate and ca = Puppet::SSL::CertificateAuthority.instance
    ca.sign(self.name, {allow_dns_alt_names: true})
  end
end

#generate_certificate_request(options = {}) ⇒ Object

Our certificate request requires the key but that’s all.



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/puppet/ssl/host.rb', line 164

def generate_certificate_request(options = {})
  generate_key unless key

  # If this CSR is for the current machine...
  if name == Puppet[:certname].downcase
    # ...add our configured dns_alt_names
    if Puppet[:dns_alt_names] and Puppet[:dns_alt_names] != ''
      options[:dns_alt_names] ||= Puppet[:dns_alt_names]
    elsif Puppet::SSL::CertificateAuthority.ca? and fqdn = Facter.value(:fqdn) and domain = Facter.value(:domain)
      options[:dns_alt_names] = "puppet, #{fqdn}, puppet.#{domain}"
    end
  end

  csr_attributes = Puppet::SSL::CertificateRequestAttributes.new(Puppet[:csr_attributes])
  if csr_attributes.load
    options[:csr_attributes] = csr_attributes.custom_attributes
    options[:extension_requests] = csr_attributes.extension_requests
  end

  @certificate_request = CertificateRequest.new(name)
  @certificate_request.generate(key.content, options)
  begin
    CertificateRequest.indirection.save(@certificate_request)
  rescue
    @certificate_request = nil
    raise
  end

  true
end

#generate_keyObject

This is the private key; we can create it from scratch with no inputs.



147
148
149
150
151
152
153
154
155
156
157
# File 'lib/puppet/ssl/host.rb', line 147

def generate_key
  @key = Key.new(name)
  @key.generate
  begin
    Key.indirection.save(@key)
  rescue
    @key = nil
    raise
  end
  true
end

#public_keyObject

Extract the public key from the private key.



270
271
272
# File 'lib/puppet/ssl/host.rb', line 270

def public_key
  key.content.public_key
end

#ssl_store(purpose = OpenSSL::X509::PURPOSE_ANY) ⇒ Object

Create/return a store that uses our SSL info to validate connections.



276
277
278
279
280
281
# File 'lib/puppet/ssl/host.rb', line 276

def ssl_store(purpose = OpenSSL::X509::PURPOSE_ANY)
  if @ssl_store.nil?
    @ssl_store = build_ssl_store(purpose)
  end
  @ssl_store
end

#stateObject



359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/puppet/ssl/host.rb', line 359

def state
  if certificate_request
    return 'requested'
  end

  begin
    Puppet::SSL::CertificateAuthority.new.verify(name)
    return 'signed'
  rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError
    return 'revoked'
  end
end

#suitable_message_digest_algorithmsObject

eventually we’ll probably want to move this somewhere else or make it configurable –jeffweiss 29 aug 2012



322
323
324
# File 'lib/puppet/ssl/host.rb', line 322

def suitable_message_digest_algorithms
  [:SHA1, :SHA256, :SHA512]
end

#to_data_hashObject



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/puppet/ssl/host.rb', line 283

def to_data_hash
  my_cert = Puppet::SSL::Certificate.indirection.find(name)
  result = { 'name'  => name }

  my_state = state

  result['state'] = my_state
  result['desired_state'] = desired_state if desired_state

  thing_to_use = (my_state == 'requested') ? certificate_request : my_cert

  # this is for backwards-compatibility
  # we should deprecate it and transition people to using
  # json[:fingerprints][:default]
  # It appears that we have no internal consumers of this api
  # --jeffweiss 30 aug 2012
  result['fingerprint'] = thing_to_use.fingerprint

  # The above fingerprint doesn't tell us what message digest algorithm was used
  # No problem, except that the default is changing between 2.7 and 3.0. Also, as
  # we move to FIPS 140-2 compliance, MD5 is no longer allowed (and, gasp, will
  # segfault in rubies older than 1.9.3)
  # So, when we add the newer fingerprints, we're explicit about the hashing
  # algorithm used.
  # --jeffweiss 31 july 2012
  result['fingerprints'] = {}
  result['fingerprints']['default'] = thing_to_use.fingerprint

  suitable_message_digest_algorithms.each do |md|
    result['fingerprints'][md.to_s] = thing_to_use.fingerprint md
  end
  result['dns_alt_names'] = thing_to_use.subject_alt_names

  result
end

#validate_certificate_with_keyObject

Raises:



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/puppet/ssl/host.rb', line 209

def validate_certificate_with_key
  raise Puppet::Error, _("No certificate to validate.") unless certificate
  raise Puppet::Error, _("No private key with which to validate certificate with fingerprint: %{fingerprint}") % { fingerprint: certificate.fingerprint } unless key
  unless certificate.content.check_private_key(key.content)
    raise Puppet::Error, _(<<ERROR_STRING) % { fingerprint: certificate.fingerprint, cert_name: Puppet[:certname], ssl_dir: Puppet[:ssldir], cert_dir: Puppet[:certdir].gsub('/', '\\') }
The certificate retrieved from the master does not match the agent's private key.
Certificate fingerprint: %{fingerprint}
To fix this, remove the certificate from both the master and the agent and then start a puppet run, which will automatically regenerate a certificate.
On the master:
puppet cert clean %{cert_name}
On the agent:
1a. On most platforms: find %{ssl_dir} -name %{cert_name}.pem -delete
1b. On Windows: del "%{cert_dir}\\%{cert_name}.pem" /f
2. puppet agent -t
ERROR_STRING
  end
end

#wait_for_cert(time) ⇒ Object

Attempt to retrieve a cert, if we don’t already have one.



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/puppet/ssl/host.rb', line 327

def wait_for_cert(time)
  begin
    return if certificate
    generate
    return if certificate
  rescue StandardError => detail
    Puppet.log_exception(detail, _("Could not request certificate: %{message}") % { message: detail.message })
    if time < 1
      puts _("Exiting; failed to retrieve certificate and waitforcert is disabled")
      exit(1)
    else
      sleep(time)
    end
    retry
  end

  if time < 1
    puts _("Exiting; no certificate found and waitforcert is disabled")
    exit(1)
  end

  while true
    sleep time
    begin
      break if certificate
      Puppet.notice _("Did not receive certificate")
    rescue StandardError => detail
      Puppet.log_exception(detail, _("Could not request certificate: %{message}") % { message: detail.message })
    end
  end
end