From d459368dd5579ee94d109e57891687ea9f10c20b Mon Sep 17 00:00:00 2001 From: Michael Hashizume Date: Wed, 21 Jun 2023 16:38:35 -0700 Subject: [PATCH] (PUP-11856) State machine renews host cert This commit adds a new NeedRenewedCert class and related logic to the state machine to handle automatic host/client certificate renewal. --- lib/puppet/ssl/state_machine.rb | 44 +++++++++++++++++++++++++++-- spec/unit/ssl/state_machine_spec.rb | 19 +++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/lib/puppet/ssl/state_machine.rb b/lib/puppet/ssl/state_machine.rb index 7c1011f8805..3776ebc4e6f 100644 --- a/lib/puppet/ssl/state_machine.rb +++ b/lib/puppet/ssl/state_machine.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative '../../puppet/ssl' require_relative '../../puppet/util/pidlock' +require 'debug' # 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 @@ -262,7 +263,11 @@ def next_state 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) + if needs_refresh?(cert) + NeedRenewedCert.new(@machine, next_ctx, key) + else + return Done.new(@machine, next_ctx) + end end else if Puppet[:key_type] == 'ec' @@ -278,6 +283,15 @@ def next_state NeedSubmitCSR.new(@machine, @ssl_context, key) end + + private + + def needs_refresh?(cert) + cert_ttl = Puppet[:hostcert_renewal_interval] + return false unless cert_ttl + + Time.now.to_i >= (cert.not_after.to_i - cert_ttl) + end end # Base class for states with a private key. @@ -349,6 +363,32 @@ def next_state end end + # Class to renew a client/host certificate automatically. + # + class NeedRenewedCert < KeySSLState + def next_state + Puppet.debug(_("Renewing client certificate")) + + route = @machine.session.route_to(:ca, ssl_context: @ssl_context) + _, cert = route.post_certificate_renewal(@ssl_context) + + # 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) + + Done.new(@machine, next_ctx) if next_ctx + rescue Puppet::HTTP::ResponseError => e + if e.response.code == 404 + Puppet.info(_("Certificate autorenewal has not been enabled on the server.")) + else + Puppet.warning(_("Failed to automatically renew certificate: %{message}") % { message: e.response.message }, e) + Done.new(@machine, @ssl_context) + end + end + end + # We cannot make progress, so wait if allowed to do so, or exit. # class Wait < SSLState @@ -500,7 +540,7 @@ def ensure_ca_certificates final_state.ssl_context end - # Run the state machine for CA certs and CRLs. + # Run the state machine for a client/host certificate. # # @return [Puppet::SSL::SSLContext] initialized SSLContext # @raise [Puppet::Error] If we fail to generate an SSLContext diff --git a/spec/unit/ssl/state_machine_spec.rb b/spec/unit/ssl/state_machine_spec.rb index cfc4858eb62..717fa250d00 100644 --- a/spec/unit/ssl/state_machine_spec.rb +++ b/spec/unit/ssl/state_machine_spec.rb @@ -745,6 +745,25 @@ def expect_lockfile_to_contain(pid) state.next_state }.to raise_error(OpenSSL::PKey::RSAError) end + + it "transitions to Done if current time plus renewal interval is less than cert's \"NotAfter\" time" do + allow(cert_provider).to receive(:load_private_key).and_return(private_key) + allow(cert_provider).to receive(:load_client_cert).and_return(client_cert) + + st = state.next_state + expect(st).to be_instance_of(Puppet::SSL::StateMachine::Done) + end + + it "returns NeedRenewedCert if current time plus renewal interval is greater than cert's \"NotAfter\" time" do + client_cert.not_after=(Time.now + 300) + allow(cert_provider).to receive(:load_private_key).and_return(private_key) + allow(cert_provider).to receive(:load_client_cert).and_return(client_cert) + ssl_context = Puppet::SSL::SSLContext.new(cacerts: [cacert], client_cert: client_cert, crls: [crl]) + state = Puppet::SSL::StateMachine::NeedKey.new(machine, ssl_context) + + st = state.next_state + expect(st).to be_instance_of(Puppet::SSL::StateMachine::NeedRenewedCert) + end end context 'in state NeedSubmitCSR' do