Class: Puppet::SSL::Host

Inherits:
Object show all
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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name = nil, device = false) ⇒ Host

Returns a new instance of Host.



281
282
283
284
285
286
287
288
# File 'lib/puppet/ssl/host.rb', line 281

def initialize(name = nil, device = false)
  @name = (name || Puppet[:certname]).downcase
  @device = device
  Puppet::SSL::Base.validate_certname(@name)
  @key = @certificate = @certificate_request = nil
  @crl_usage = Puppet.settings[:certificate_revocation]
  @crl_path = Puppet.settings[:hostcrl]
end

Instance Attribute Details

#certificateObject



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/puppet/ssl/host.rb', line 131

def certificate
  unless @certificate
    generate_key unless key

    # get CA and optional CRL
    sm = Puppet::SSL::StateMachine.new
    sm.ensure_ca_certificates

    cert = get_host_certificate
    return nil unless cert

    validate_certificate_with_key(cert)
    @certificate = cert
  end
  @certificate
end

#certificate_requestPuppet::SSL::CertificateRequest?

Search for an existing CSR for this host either cached on disk or stored by the CA. Returns nil if no request exists.



192
193
194
195
196
197
198
199
200
201
# File 'lib/puppet/ssl/host.rb', line 192

def certificate_request
  unless @certificate_request
    if csr = load_certificate_request_from_file
      @certificate_request = csr
    elsif csr = download_csr_from_ca
      @certificate_request = csr
    end
  end
  @certificate_request
end

#crl_pathObject (readonly)

Returns the value of attribute crl_path.



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

def crl_path
  @crl_path
end

#crl_usage=(value) ⇒ Object (writeonly)

Sets the attribute crl_usage

Parameters:

  • value

    the value to set the attribute crl_usage to.



29
30
31
# File 'lib/puppet/ssl/host.rb', line 29

def crl_usage=(value)
  @crl_usage = value
end

#deviceObject (readonly)

Returns the value of attribute device.



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

def device
  @device
end

#keyObject



78
79
80
# File 'lib/puppet/ssl/host.rb', line 78

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

#nameObject (readonly)

Returns the value of attribute name.



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

def name
  @name
end

Class Method Details

.configure_indirection(terminus, cache = nil) ⇒ Object

Configure how our various classes interact with their various terminuses.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/puppet/ssl/host.rb', line 44

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

  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
  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
  end
end

.from_data_hash(data) ⇒ Object



70
71
72
73
74
75
76
# File 'lib/puppet/ssl/host.rb', line 70

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

.localhostObject



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

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

.resetObject



39
40
41
# File 'lib/puppet/ssl/host.rb', line 39

def self.reset
  @localhost = nil
end

Instance Method Details

#check_for_certificate_on_disk(cert_name) ⇒ Puppet::SSL::Certificate?

Checks for the requested certificate on disc, at a location determined by this host’s configuration.

Returns:

Raises:

  • (Puppet::Error)

    if contents of certificate file is invalid and could not be loaded



467
468
469
470
471
472
473
474
475
476
# File 'lib/puppet/ssl/host.rb', line 467

def check_for_certificate_on_disk(cert_name)
  file_path = certificate_location(cert_name)
  if Puppet::FileSystem.exist?(file_path)
    begin
      Puppet::SSL::Certificate.from_s(Puppet::FileSystem.read(file_path))
    rescue OpenSSL::X509::CertificateError
      raise Puppet::Error, _("The certificate at %{file_path} is invalid. Could not load.") % { file_path: file_path }
    end
  end
end

#clean_paramsObject

The puppet parameters for commands output by the validate_ methods depend upon whether this is an agent or a device.



151
152
153
# File 'lib/puppet/ssl/host.rb', line 151

def clean_params
  @device ? "--target #{Puppet[:certname]}" : ''
end

#download_certificate_from_ca(cert_name) ⇒ Puppet::SSL::Certificate?

Attempts to download this host’s certificate from the CA server. Returns nil if the CA does not yet have a signed cert for this host.

Parameters:

  • name (String)

    then name of the cert to fetch

Returns:

Raises:

  • (Puppet::Error)

    if response from the CA does not contain a valid certificate



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
# File 'lib/puppet/ssl/host.rb', line 485

def download_certificate_from_ca(cert_name)
  begin
    cert = Puppet::Rest::Routes.get_certificate(
      cert_name,
      Puppet::SSL::SSLContext.new(store: ssl_store)
    )
    begin
      Puppet::SSL::Certificate.from_s(cert)
    rescue OpenSSL::X509::CertificateError
      raise Puppet::Error, _("Response from the CA did not contain a valid certificate for %{cert_name}.") % { cert_name: cert_name }
    end
  rescue Puppet::Rest::ResponseError => e
    if e.response.code.to_i == 404
      Puppet.debug _("No certificate for %{cert_name} on CA") % { cert_name: cert_name }
      nil
    else
      raise Puppet::Rest::ResponseError, _("Could not download host certificate: %{message}") % { message: e.message }
    end
  end
end

#download_host_certificateObject



180
181
182
183
184
185
186
187
# File 'lib/puppet/ssl/host.rb', line 180

def download_host_certificate
  cert = download_certificate_from_ca(name)
  return nil unless cert

  validate_certificate_with_key(cert)
  save_host_certificate(cert)
  cert
end

#generateObject

Generate all necessary parts of our ssl host.



204
205
206
207
208
209
210
211
212
213
214
# File 'lib/puppet/ssl/host.rb', line 204

def generate
  generate_key unless key

  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
  validate_csr_with_key(existing_request, key) if existing_request

  generate_certificate_request unless existing_request
end

#generate_certificate_request(options = {}) ⇒ Object

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



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/puppet/ssl/host.rb', line 97

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]
    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
    submit_certificate_request(@certificate_request)
    save_certificate_request(@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.



84
85
86
87
88
89
90
91
92
93
94
# File 'lib/puppet/ssl/host.rb', line 84

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

#http_client(ssl_context) ⇒ Object



127
128
129
# File 'lib/puppet/ssl/host.rb', line 127

def http_client(ssl_context)
  Puppet::Rest::Client.new(ssl_context: ssl_context)
end

#public_keyObject

Extract the public key from the private key.



291
292
293
# File 'lib/puppet/ssl/host.rb', line 291

def public_key
  key.content.public_key
end

#puppet_paramsObject



155
156
157
# File 'lib/puppet/ssl/host.rb', line 155

def puppet_params
  @device ? "device -v --target #{Puppet[:certname]}" : 'agent -t'
end

#save_host_certificate(cert) ⇒ Object

Saves the given certificate to disc, at a location determined by this host’s configuration.

Parameters:



348
349
350
351
352
353
# File 'lib/puppet/ssl/host.rb', line 348

def save_host_certificate(cert)
  file_path = certificate_location(name)
  Puppet::Util.replace_file(file_path, 0644) do |f|
    f.write(cert.to_s)
  end
end

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

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



305
306
307
308
309
310
# File 'lib/puppet/ssl/host.rb', line 305

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

#submit_requestPuppet::SSL::CertificateRequest?

Generate a keypair, generate a CSR, and submit it. If a local key pair already exists it will be used to generate the CSR. If a local CSR already exists and matches the key then the existing CSR will be submitted. If the CSR and key do not match an exception will be raised.



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/puppet/ssl/host.rb', line 222

def submit_request
  generate_key unless key

  csr = load_certificate_request_from_file
  if csr
    if key.content.public_key.to_s != csr.content.public_key.to_s
      Puppet.warning("The local CSR does not match the agent's public key. Generating a new CSR.")

      request_path = certificate_request_location(name)
      Puppet::FileSystem.unlink(request_path)
      csr = nil
    end
  end

  if csr
    validate_csr_with_key(csr, key)
    submit_certificate_request(csr)
    @certificate_request = csr
  else
    generate_certificate_request
  end

  @certificate_request
end

#use_crl?Boolean

Returns:

  • (Boolean)


295
296
297
# File 'lib/puppet/ssl/host.rb', line 295

def use_crl?
  !!@crl_usage
end

#use_crl_chain?Boolean

Returns:

  • (Boolean)


299
300
301
# File 'lib/puppet/ssl/host.rb', line 299

def use_crl_chain?
  @crl_usage == true || @crl_usage == :chain
end

#validate_certificate_with_key(cert) ⇒ Object

Validate that our private key matches the specified certificate.

Parameters:

Raises:



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/puppet/ssl/host.rb', line 163

def validate_certificate_with_key(cert)
  raise Puppet::Error, _("No certificate to validate.") unless cert
  raise Puppet::Error, _("No private key with which to validate certificate with fingerprint: %{fingerprint}") % { fingerprint: cert.fingerprint } unless key
  unless cert.content.check_private_key(key.content)
    raise Puppet::Error, _(<<ERROR_STRING) % { fingerprint: cert.fingerprint, cert_name: Puppet[:certname], clean_params: clean_params, puppet_params: puppet_params }
The certificate retrieved from the master does not match the agent's private key. Did you forget to run as root?
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:
puppetserver ca clean --certname %{cert_name}
On the agent:
1. puppet ssl clean %{clean_params}
2. puppet %{puppet_params}
ERROR_STRING
  end
end

#wait_for_cert(time) ⇒ Object

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



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/puppet/ssl/host.rb', line 313

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