require 'puppet/ssl' require 'puppet/util/pidlock' # This class implements a state machine for bootstrapping a host's CA and CRL # bundles, private key and signed client certificate. Each state has a frozen # SSLContext that it uses to make network connections. If a state makes progress # bootstrapping the host, then the state will generate a new frozen SSLContext # and pass that to the next state. For example, the NeedCACerts state will load # or download a CA bundle, and generate a new SSLContext containing those CA # certs. This way we're sure about which SSLContext is being used during any # phase of the bootstrapping process. # # @private class Puppet::SSL::StateMachine class SSLState attr_reader :ssl_context def initialize(machine, ssl_context) @machine = machine @ssl_context = ssl_context @cert_provider = machine.cert_provider @ssl_provider = machine.ssl_provider end def to_error(message, cause) detail = Puppet::Error.new(message) detail.set_backtrace(cause.backtrace) Error.new(@machine, message, detail) end end # Load existing CA certs or download them. Transition to NeedCRLs. # class NeedCACerts < SSLState def initialize(machine) super(machine, nil) @ssl_context = @ssl_provider.create_insecure_context end def next_state Puppet.debug("Loading CA certs") cacerts = @cert_provider.load_cacerts if cacerts next_ctx = @ssl_provider.create_root_context(cacerts: cacerts, revocation: false) else route = @machine.session.route_to(:ca, ssl_context: @ssl_context) _, pem = route.get_certificate(Puppet::SSL::CA_NAME, ssl_context: @ssl_context) if @machine.ca_fingerprint actual_digest = Puppet::SSL::Digest.new(@machine.digest, pem).to_hex expected_digest = @machine.ca_fingerprint.scan(/../).join(':').upcase if actual_digest == expected_digest Puppet.info(_("Verified CA bundle with digest (%{digest_type}) %{actual_digest}") % { digest_type: @machine.digest, actual_digest: actual_digest }) else e = Puppet::Error.new(_("CA bundle with digest (%{digest_type}) %{actual_digest} did not match expected digest %{expected_digest}") % { digest_type: @machine.digest, actual_digest: actual_digest, expected_digest: expected_digest }) return Error.new(@machine, e.message, e) end end cacerts = @cert_provider.load_cacerts_from_pem(pem) # verify cacerts before saving next_ctx = @ssl_provider.create_root_context(cacerts: cacerts, revocation: false) @cert_provider.save_cacerts(cacerts) end NeedCRLs.new(@machine, next_ctx) rescue OpenSSL::X509::CertificateError => e Error.new(@machine, e.message, e) rescue Puppet::HTTP::ResponseError => e if e.response.code == 404 to_error(_('CA certificate is missing from the server'), e) else to_error(_('Could not download CA certificate: %{message}') % { message: e.message }, e) end end end # If revocation is enabled, load CRLs or download them, using the CA bundle # from the previous state. Transition to NeedKey. Even if Puppet[:certificate_revocation] # is leaf or chain, disable revocation when downloading the CRL, since 1) we may # not have one yet or 2) the connection will fail if NeedCACerts downloaded a new CA # for which we don't have a CRL # class NeedCRLs < SSLState def next_state Puppet.debug("Loading CRLs") case Puppet[:certificate_revocation] when :chain, :leaf crls = @cert_provider.load_crls if crls next_ctx = @ssl_provider.create_root_context(cacerts: ssl_context[:cacerts], crls: crls) crl_ttl = Puppet[:crl_refresh_interval] if crl_ttl last_update = @cert_provider.crl_last_update now = Time.now if last_update.nil? || now.to_i > last_update.to_i + crl_ttl # set last updated time first, then make a best effort to refresh @cert_provider.crl_last_update = now next_ctx = refresh_crl(next_ctx, last_update) end end else next_ctx = download_crl(@ssl_context, nil) end else Puppet.info("Certificate revocation is disabled, skipping CRL download") next_ctx = @ssl_provider.create_root_context(cacerts: ssl_context[:cacerts], crls: []) end NeedKey.new(@machine, next_ctx) rescue OpenSSL::X509::CRLError => e Error.new(@machine, e.message, e) rescue Puppet::HTTP::ResponseError => e if e.response.code == 404 to_error(_('CRL is missing from the server'), e) else to_error(_('Could not download CRLs: %{message}') % { message: e.message }, e) end end private def refresh_crl(ssl_ctx, last_update) Puppet.info(_("Refreshing CRL")) # return the next_ctx containing the updated crl download_crl(ssl_ctx, last_update) rescue Puppet::HTTP::ResponseError => e if e.response.code == 304 Puppet.info(_("CRL is unmodified, using existing CRL")) else Puppet.info(_("Failed to refresh CRL, using existing CRL: %{message}") % {message: e.message}) end # return the original ssl_ctx ssl_ctx rescue Puppet::HTTP::HTTPError => e Puppet.warning(_("Failed to refresh CRL, using existing CRL: %{message}") % {message: e.message}) # return the original ssl_ctx ssl_ctx end def download_crl(ssl_ctx, last_update) route = @machine.session.route_to(:ca, ssl_context: ssl_ctx) _, pem = route.get_certificate_revocation_list(if_modified_since: last_update, ssl_context: ssl_ctx) crls = @cert_provider.load_crls_from_pem(pem) # verify crls before saving next_ctx = @ssl_provider.create_root_context(cacerts: ssl_ctx[:cacerts], crls: crls) @cert_provider.save_crls(crls) next_ctx end end # Load or generate a private key. If the key exists, try to load the client cert # and transition to Done. If the cert is mismatched or otherwise fails valiation, # raise an error. If the key doesn't exist yet, generate one, and save it. If the # cert doesn't exist yet, transition to NeedSubmitCSR. # class NeedKey < SSLState def next_state Puppet.debug(_("Loading/generating private key")) password = @cert_provider.load_private_key_password key = @cert_provider.load_private_key(Puppet[:certname], password: password) if key cert = @cert_provider.load_client_cert(Puppet[:certname]) if cert next_ctx = @ssl_provider.create_context( cacerts: @ssl_context.cacerts, crls: @ssl_context.crls, private_key: key, client_cert: cert ) return Done.new(@machine, next_ctx) end else if Puppet[:key_type] == 'ec' Puppet.info _("Creating a new EC SSL key for %{name} using curve %{curve}") % { name: Puppet[:certname], curve: Puppet[:named_curve] } key = OpenSSL::PKey::EC.generate(Puppet[:named_curve]) else Puppet.info _("Creating a new RSA SSL key for %{name}") % { name: Puppet[:certname] } key = OpenSSL::PKey::RSA.new(Puppet[:keylength].to_i) end @cert_provider.save_private_key(Puppet[:certname], key, password: password) end NeedSubmitCSR.new(@machine, @ssl_context, key) end end # Base class for states with a private key. # class KeySSLState < SSLState attr_reader :private_key def initialize(machine, ssl_context, private_key) super(machine, ssl_context) @private_key = private_key end end # Generate and submit a CSR using the CA cert bundle and optional CRL bundle # from earlier states. If the request is submitted, proceed to NeedCert, # otherwise Wait. This could be due to the server already having a CSR # for this host (either the same or different CSR content), having a # signed certificate, or a revoked certificate. # class NeedSubmitCSR < KeySSLState def next_state Puppet.debug(_("Generating and submitting a CSR")) csr = @cert_provider.create_request(Puppet[:certname], @private_key) route = @machine.session.route_to(:ca, ssl_context: @ssl_context) route.put_certificate_request(Puppet[:certname], csr, ssl_context: @ssl_context) @cert_provider.save_request(Puppet[:certname], csr) NeedCert.new(@machine, @ssl_context, @private_key) rescue Puppet::HTTP::ResponseError => e if e.response.code == 400 NeedCert.new(@machine, @ssl_context, @private_key) else to_error(_("Failed to submit the CSR, HTTP response was %{code}") % { code: e.response.code }, e) end end end # Attempt to load or retrieve our signed cert. # class NeedCert < KeySSLState def next_state Puppet.debug(_("Downloading client certificate")) route = @machine.session.route_to(:ca, ssl_context: @ssl_context) cert = OpenSSL::X509::Certificate.new( route.get_certificate(Puppet[:certname], ssl_context: @ssl_context)[1] ) Puppet.info _("Downloaded certificate for %{name} from %{url}") % { name: Puppet[:certname], url: route.url } # verify client cert before saving next_ctx = @ssl_provider.create_context( cacerts: @ssl_context.cacerts, crls: @ssl_context.crls, private_key: @private_key, client_cert: cert ) @cert_provider.save_client_cert(Puppet[:certname], cert) @cert_provider.delete_request(Puppet[:certname]) Done.new(@machine, next_ctx) rescue Puppet::SSL::SSLError => e Error.new(@machine, e.message, e) rescue OpenSSL::X509::CertificateError => e Error.new(@machine, _("Failed to parse certificate: %{message}") % {message: e.message}, e) rescue Puppet::HTTP::ResponseError => e if e.response.code == 404 Puppet.info(_("Certificate for %{certname} has not been signed yet") % {certname: Puppet[:certname]}) $stdout.puts _("Couldn't fetch certificate from CA server; you might still need to sign this agent's certificate (%{name}).") % { name: Puppet[:certname] } Wait.new(@machine) else to_error(_("Failed to retrieve certificate for %{certname}: %{message}") % {certname: Puppet[:certname], message: e.response.message}, e) end end end # We cannot make progress, so wait if allowed to do so, or exit. # class Wait < SSLState def initialize(machine) super(machine, nil) end def next_state time = @machine.waitforcert if time < 1 puts _("Exiting now because the waitforcert setting is set to 0.") exit(1) elsif Time.now.to_i > @machine.wait_deadline puts _("Couldn't fetch certificate from CA server; you might still need to sign this agent's certificate (%{name}). Exiting now because the maxwaitforcert timeout has been exceeded.") % {name: Puppet[:certname] } exit(1) else Puppet.info(_("Will try again in %{time} seconds.") % {time: time}) # close persistent connections and session state before sleeping Puppet.runtime[:http].close @machine.session = Puppet.runtime[:http].create_session @machine.unlock Kernel.sleep(time) NeedLock.new(@machine) end end end # Acquire the ssl lock or return LockFailure causing us to exit. # class NeedLock < SSLState def initialize(machine) super(machine, nil) end def next_state if @machine.lock # our ssl directory may have been cleaned while we were # sleeping, start over from the top NeedCACerts.new(@machine) elsif @machine.waitforlock < 1 LockFailure.new(@machine, _("Another puppet instance is already running and the waitforlock setting is set to 0; exiting")) elsif Time.now.to_i >= @machine.waitlock_deadline LockFailure.new(@machine, _("Another puppet instance is already running and the maxwaitforlock timeout has been exceeded; exiting")) else Puppet.info _("Another puppet instance is already running; waiting for it to finish") Puppet.info _("Will try again in %{time} seconds.") % {time: @machine.waitforlock} Kernel.sleep @machine.waitforlock # try again self end end end # We failed to acquire the lock, so exit # class LockFailure < SSLState attr_reader :message def initialize(machine, message) super(machine, nil) @message = message end end # We cannot make progress due to an error. # class Error < SSLState attr_reader :message, :error def initialize(machine, message, error) super(machine, nil) @message = message @error = error end def next_state Puppet.log_exception(@error, @message) Wait.new(@machine) end end # We have a CA bundle, optional CRL bundle, a private key and matching cert # that chains to one of the root certs in our bundle. # class Done < SSLState; end attr_reader :waitforcert, :wait_deadline, :waitforlock, :waitlock_deadline, :cert_provider, :ssl_provider, :ca_fingerprint, :digest attr_accessor :session # Construct a state machine to manage the SSL initialization process. By # default, if the state machine encounters an exception, it will log the # exception and wait for `waitforcert` seconds and retry, restarting from the # beginning of the state machine. # # However, if `onetime` is true, then the state machine will raise the first # error it encounters, instead of waiting. Otherwise, if `waitforcert` is 0, # then then state machine will exit instead of wait. # # @param waitforcert [Integer] how many seconds to wait between attempts # @param maxwaitforcert [Integer] maximum amount of seconds to wait for the # server to sign the certificate request # @param waitforlock [Integer] how many seconds to wait between attempts for # acquiring the ssl lock # @param maxwaitforlock [Integer] maximum amount of seconds to wait for an # already running process to release the ssl lock # @param onetime [Boolean] whether to run onetime # @param lockfile [Puppet::Util::Pidlock] lockfile to protect against # concurrent modification by multiple processes # @param cert_provider [Puppet::X509::CertProvider] cert provider to use # to load and save X509 objects. # @param ssl_provider [Puppet::SSL::SSLProvider] ssl provider to use # to construct ssl contexts. # @param digest [String] digest algorithm to use for certificate fingerprinting # @param ca_fingerprint [String] optional fingerprint to verify the # downloaded CA bundle def initialize(waitforcert: Puppet[:waitforcert], maxwaitforcert: Puppet[:maxwaitforcert], waitforlock: Puppet[:waitforlock], maxwaitforlock: Puppet[:maxwaitforlock], onetime: Puppet[:onetime], cert_provider: Puppet::X509::CertProvider.new, ssl_provider: Puppet::SSL::SSLProvider.new, lockfile: Puppet::Util::Pidlock.new(Puppet[:ssl_lockfile]), digest: 'SHA256', ca_fingerprint: Puppet[:ca_fingerprint]) @waitforcert = waitforcert @wait_deadline = Time.now.to_i + maxwaitforcert @waitforlock = waitforlock @waitlock_deadline = Time.now.to_i + maxwaitforlock @onetime = onetime @cert_provider = cert_provider @ssl_provider = ssl_provider @lockfile = lockfile @digest = digest @ca_fingerprint = ca_fingerprint @session = Puppet.runtime[:http].create_session end # Run the state machine for CA certs and CRLs. # # @return [Puppet::SSL::SSLContext] initialized SSLContext # @raise [Puppet::Error] If we fail to generate an SSLContext def ensure_ca_certificates final_state = run_machine(NeedLock.new(self), NeedKey) final_state.ssl_context end # Run the state machine for CA certs and CRLs. # # @return [Puppet::SSL::SSLContext] initialized SSLContext # @raise [Puppet::Error] If we fail to generate an SSLContext def ensure_client_certificate final_state = run_machine(NeedLock.new(self), Done) ssl_context = final_state.ssl_context if Puppet::Util::Log.sendlevel?(:debug) chain = ssl_context.client_chain # print from root to client chain.reverse.each_with_index do |cert, i| digest = Puppet::SSL::Digest.new(@digest, cert.to_der) if i == chain.length - 1 Puppet.debug(_("Verified client certificate '%{subject}' fingerprint %{digest}") % {subject: cert.subject.to_utf8, digest: digest}) else Puppet.debug(_("Verified CA certificate '%{subject}' fingerprint %{digest}") % {subject: cert.subject.to_utf8, digest: digest}) end end end ssl_context end def lock @lockfile.lock end def unlock @lockfile.unlock end private def run_machine(state, stop) loop do state = run_step(state) case state when stop break when LockFailure raise Puppet::Error, state.message when Error if @onetime Puppet.log_exception(state.error) raise state.error end else # fall through end end state ensure @lockfile.unlock if @lockfile.locked? end def run_step(state) state.next_state rescue => e state.to_error(e.message, e) end end