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) ⇒ Host

Returns a new instance of Host.



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

def initialize(name = nil)
  @name = (name || Puppet[:certname]).downcase
  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



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

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. If we can't get it, quit.
    if !ensure_ca_certificate
      return nil
    end

    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.



183
184
185
186
187
188
189
190
191
192
# File 'lib/puppet/ssl/host.rb', line 183

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.



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

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.



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

def crl_usage=(value)
  @crl_usage = value
end

#keyObject



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

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

#nameObject (readonly)

Returns the value of attribute name.



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

def name
  @name
end

Class Method Details

.configure_indirection(terminus, cache = nil) ⇒ Object

Configure how our various classes interact with their various terminuses.



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

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



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

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

.localhostObject



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

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

.resetObject



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

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



541
542
543
544
545
546
547
548
549
550
# File 'lib/puppet/ssl/host.rb', line 541

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

#download_host_certificateObject



171
172
173
174
175
176
177
178
# File 'lib/puppet/ssl/host.rb', line 171

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

#ensure_ca_certificateBoolean

Ensures that the CA certificate is available for either generating or validating the host’s cert. It will first check on disk, then try to download it.

Returns:

  • (Boolean)

    true if the CA certificate was found, false otherwise

Raises:

  • (Puppet::Error)

    if text form of found certificate bundle is invalid and cannot be loaded into cert objects



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/puppet/ssl/host.rb', line 423

def ensure_ca_certificate
  file_path = certificate_location(CA_NAME)
  if Puppet::FileSystem.exist?(file_path)
    begin
      # This load ensures that the file contents is a valid cert bundle.
      # If the text is malformed, load_certificate_bundle will raise.
      load_certificate_bundle(Puppet::FileSystem.read(file_path))
    rescue Puppet::Error => e
      raise Puppet::Error, _("The CA certificate at %{file_path} is invalid: %{message}") % { file_path: file_path, message: e.message }
    end
  else
    bundle = download_ca_certificate_bundle
    if bundle
      save_bundle(bundle, certificate_location(CA_NAME))
      true
    else
      false
    end
  end
end

#generateObject

Generate all necessary parts of our ssl host.



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

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.



96
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
# File 'lib/puppet/ssl/host.rb', line 96

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.



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

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



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

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

#public_keyObject

Extract the public key from the private key.



283
284
285
# File 'lib/puppet/ssl/host.rb', line 283

def public_key
  key.content.public_key
end

#save_host_certificate(cert) ⇒ Object

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

Parameters:



340
341
342
343
344
345
# File 'lib/puppet/ssl/host.rb', line 340

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.



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

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.



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/puppet/ssl/host.rb', line 213

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)


287
288
289
# File 'lib/puppet/ssl/host.rb', line 287

def use_crl?
  !!@crl_usage
end

#use_crl_chain?Boolean

Returns:

  • (Boolean)


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

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:



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/puppet/ssl/host.rb', line 153

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], ssl_dir: Puppet[:ssldir], cert_dir: Puppet[:certdir].gsub('/', '\\') }
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:
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.



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/puppet/ssl/host.rb', line 305

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