Skip to content

Commit 7aec0c7

Browse files
committed
(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.
1 parent 96528b4 commit 7aec0c7

File tree

2 files changed

+110
-1
lines changed

2 files changed

+110
-1
lines changed

lib/puppet/ssl/state_machine.rb

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,11 @@ def next_state
262262
next_ctx = @ssl_provider.create_context(
263263
cacerts: @ssl_context.cacerts, crls: @ssl_context.crls, private_key: key, client_cert: cert
264264
)
265-
return Done.new(@machine, next_ctx)
265+
if needs_refresh?(cert)
266+
return NeedRenewedCert.new(@machine, next_ctx, key)
267+
else
268+
return Done.new(@machine, next_ctx)
269+
end
266270
end
267271
else
268272
if Puppet[:key_type] == 'ec'
@@ -278,6 +282,15 @@ def next_state
278282

279283
NeedSubmitCSR.new(@machine, @ssl_context, key)
280284
end
285+
286+
private
287+
288+
def needs_refresh?(cert)
289+
cert_ttl = Puppet[:hostcert_renewal_interval]
290+
return false unless cert_ttl
291+
292+
Time.now.to_i >= (cert.not_after.to_i - cert_ttl)
293+
end
281294
end
282295

283296
# Base class for states with a private key.
@@ -349,6 +362,39 @@ def next_state
349362
end
350363
end
351364

365+
# Class to renew a client/host certificate automatically.
366+
#
367+
class NeedRenewedCert < KeySSLState
368+
def next_state
369+
Puppet.debug(_("Renewing client certificate"))
370+
371+
route = @machine.session.route_to(:ca, ssl_context: @ssl_context)
372+
cert = OpenSSL::X509::Certificate.new(
373+
route.post_certificate_renewal(@ssl_context)[1]
374+
)
375+
376+
# verify client cert before saving
377+
next_ctx = @ssl_provider.create_context(
378+
cacerts: @ssl_context.cacerts, crls: @ssl_context.crls, private_key: @private_key, client_cert: cert
379+
)
380+
@cert_provider.save_client_cert(Puppet[:certname], cert)
381+
382+
Puppet.info(_("Refreshed client certificate: %{cert_digest}, not before '%{not_before}', not after '%{not_after}'") % { cert_digest: @machine.digest_as_hex(cert.to_pem), not_before: cert.not_before, not_after: cert.not_after })
383+
384+
Done.new(@machine, next_ctx)
385+
rescue Puppet::HTTP::ResponseError => e
386+
if e.response.code == 404
387+
Puppet.info(_("Certificate autorenewal has not been enabled on the server."))
388+
else
389+
Puppet.warning(_("Failed to automatically renew certificate: %{code} %{reason}") % { code: e.response.code, reason: e.response.reason })
390+
end
391+
Done.new(@machine, @ssl_context)
392+
rescue => e
393+
Puppet.warning(_("Unable to automatically renew certificate: %{message}") % { message: e })
394+
Done.new(@machine, @ssl_context)
395+
end
396+
end
397+
352398
# We cannot make progress, so wait if allowed to do so, or exit.
353399
#
354400
class Wait < SSLState

spec/unit/ssl/state_machine_spec.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,26 @@ def expect_lockfile_to_contain(pid)
745745
state.next_state
746746
}.to raise_error(OpenSSL::PKey::RSAError)
747747
end
748+
749+
it "transitions to Done if current time plus renewal interval is less than cert's \"NotAfter\" time" do
750+
allow(cert_provider).to receive(:load_private_key).and_return(private_key)
751+
allow(cert_provider).to receive(:load_client_cert).and_return(client_cert)
752+
753+
st = state.next_state
754+
expect(st).to be_instance_of(Puppet::SSL::StateMachine::Done)
755+
end
756+
757+
it "returns NeedRenewedCert if current time plus renewal interval is greater than cert's \"NotAfter\" time" do
758+
client_cert.not_after=(Time.now + 300)
759+
allow(cert_provider).to receive(:load_private_key).and_return(private_key)
760+
allow(cert_provider).to receive(:load_client_cert).and_return(client_cert)
761+
762+
ssl_context = Puppet::SSL::SSLContext.new(cacerts: [cacert], client_cert: client_cert, crls: [crl])
763+
state = Puppet::SSL::StateMachine::NeedKey.new(machine, ssl_context)
764+
765+
st = state.next_state
766+
expect(st).to be_instance_of(Puppet::SSL::StateMachine::NeedRenewedCert)
767+
end
748768
end
749769

750770
context 'in state NeedSubmitCSR' do
@@ -1049,5 +1069,48 @@ def write_csr_attributes(data)
10491069
expect(state.next_state).to be_an_instance_of(Puppet::SSL::StateMachine::LockFailure)
10501070
end
10511071
end
1072+
1073+
context 'in state NeedRenewedCert' do
1074+
before :each do
1075+
client_cert.not_after=(Time.now + 300)
1076+
end
1077+
1078+
let(:ssl_context) { Puppet::SSL::SSLContext.new(cacerts: cacerts, client_cert: client_cert, crls: crls,)}
1079+
let(:state) { Puppet::SSL::StateMachine::NeedRenewedCert.new(machine, ssl_context, private_key) }
1080+
let(:renewed_cert) { cert_fixture('renewed.pem') }
1081+
1082+
it 'returns Done with renewed cert when successful' do
1083+
allow(cert_provider).to receive(:save_client_cert)
1084+
stub_request(:post, %r{puppet-ca/v1/certificate_renewal}).to_return(status: 200, body: renewed_cert.to_pem)
1085+
1086+
st = state.next_state
1087+
expect(st).to be_an_instance_of(Puppet::SSL::StateMachine::Done)
1088+
expect(st.ssl_context[:client_cert]).to eq(renewed_cert)
1089+
end
1090+
1091+
it 'logs a warning message when failing with a non-404 status' do
1092+
stub_request(:post, %r{puppet-ca/v1/certificate_renewal}).to_return(status: 400, body: 'Failed to automatically renew certificate: 400 Bad request')
1093+
1094+
expect(Puppet).to receive(:warning).with(/Failed to automatically renew certificate/)
1095+
st = state.next_state
1096+
expect(st).to be_an_instance_of(Puppet::SSL::StateMachine::Done)
1097+
end
1098+
1099+
it 'logs an info message when failing with 404' do
1100+
stub_request(:post, %r{puppet-ca/v1/certificate_renewal}).to_return(status: 404, body: 'Certificate autorenewal has not been enabled on the server.')
1101+
1102+
expect(Puppet).to receive(:info).with('Certificate autorenewal has not been enabled on the server.')
1103+
st = state.next_state
1104+
expect(st).to be_an_instance_of(Puppet::SSL::StateMachine::Done)
1105+
end
1106+
1107+
it 'logs a warning message when failing with no HTTP status' do
1108+
stub_request(:post, %r{puppet-ca/v1/certificate_renewal}).to_raise(Errno::ECONNREFUSED)
1109+
1110+
expect(Puppet).to receive(:warning).with(/Unable to automatically renew certificate:/)
1111+
st = state.next_state
1112+
expect(st).to be_an_instance_of(Puppet::SSL::StateMachine::Done)
1113+
end
1114+
end
10521115
end
10531116
end

0 commit comments

Comments
 (0)