Skip to content

Commit 6a77bf0

Browse files
author
cthorn42
authored
Merge pull request #8899 from joshcooper/reload_crls_11428
(PUP-11428) Reload ssl context for each agent run
2 parents 2c7462f + 2d6861e commit 6a77bf0

File tree

14 files changed

+283
-81
lines changed

14 files changed

+283
-81
lines changed

lib/puppet.rb

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -242,20 +242,7 @@ def self.base_context(settings)
242242
{
243243
:environments => Puppet::Environments::Cached.new(Puppet::Environments::Combined.new(*loaders)),
244244
:http_pool => proc { Puppet.runtime[:http].pool },
245-
:ssl_context => proc {
246-
begin
247-
cert = Puppet::X509::CertProvider.new
248-
password = cert.load_private_key_password
249-
ssl = Puppet::SSL::SSLProvider.new
250-
ssl.load_context(certname: Puppet[:certname], password: password)
251-
rescue => e
252-
# TRANSLATORS: `message` is an already translated string of why SSL failed to initialize
253-
Puppet.log_exception(e, _("Failed to initialize SSL: %{message}") % { message: e.message })
254-
# TRANSLATORS: `puppet agent -t` is a command and should not be translated
255-
Puppet.err(_("Run `puppet agent -t`"))
256-
raise e
257-
end
258-
},
245+
:ssl_context => proc { Puppet.runtime[:http].default_ssl_context },
259246
:ssl_host => proc { Puppet::SSL::Host.localhost(true) },
260247
:http_session => proc { Puppet.runtime[:http].create_session },
261248
:plugins => proc { Puppet::Plugins::Configuration.load_plugins },

lib/puppet/agent.rb

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,29 @@ def run(client_options = {})
4545
result = nil
4646
wait_for_lock_deadline = nil
4747
block_run = Puppet::Application.controlled_run do
48-
splay client_options.fetch :splay, Puppet[:splay]
48+
# splay may sleep for awhile!
49+
splay(client_options.fetch(:splay, Puppet[:splay]))
50+
51+
# waiting for certs may sleep for awhile depending on onetime, waitforcert and maxwaitforcert!
52+
# this needs to happen before forking so that if we fail to obtain certs and try to exit, then
53+
# we exit the main process and not the forked child.
54+
ssl_context = wait_for_certificates(client_options)
55+
4956
result = run_in_fork(should_fork) do
5057
with_client(client_options[:transaction_uuid], client_options[:job_id]) do |client|
5158
client_args = client_options.merge(:pluginsync => Puppet::Configurer.should_pluginsync?)
5259
begin
60+
# lock may sleep for awhile depending on waitforlock and maxwaitforlock!
5361
lock do
5462
# NOTE: Timeout is pretty heinous as the location in which it
5563
# throws an error is entirely unpredictable, which means that
5664
# it can interrupt code blocks that perform cleanup or enforce
5765
# sanity. The only thing a Puppet agent should do after this
5866
# error is thrown is die with as much dignity as possible.
5967
Timeout.timeout(Puppet[:runtimeout], RunTimeoutError) do
60-
client.run(client_args)
68+
Puppet.override(ssl_context: ssl_context) do
69+
client.run(client_args)
70+
end
6171
end
6272
end
6373
rescue Puppet::LockError
@@ -84,6 +94,8 @@ def run(client_options = {})
8494
rescue StandardError => detail
8595
Puppet.log_exception(detail, _("Could not run %{client_class}: %{detail}") % { client_class: client_class, detail: detail })
8696
nil
97+
ensure
98+
Puppet.runtime[:http].close
8799
end
88100
end
89101
end
@@ -137,4 +149,10 @@ def with_client(transaction_uuid, job_id = nil)
137149
ensure
138150
@client = nil
139151
end
152+
153+
def wait_for_certificates(options)
154+
waitforcert = options[:waitforcert] || (Puppet[:onetime] ? 0 : Puppet[:waitforcert])
155+
sm = Puppet::SSL::StateMachine.new(waitforcert: waitforcert, onetime: Puppet[:onetime])
156+
sm.ensure_client_certificate
157+
end
140158
end

lib/puppet/application/agent.rb

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -383,15 +383,11 @@ def run_command
383383

384384
log_config if Puppet[:daemonize]
385385

386-
# run ssl state machine, waiting if needed
387-
ssl_context = wait_for_certificates
388-
389386
# Each application is responsible for pushing loaders onto the context.
390387
# Use the current environment that has already been established, though
391388
# it may change later during the configurer run.
392389
env = Puppet.lookup(:current_environment)
393-
Puppet.override(ssl_context: ssl_context,
394-
current_environment: env,
390+
Puppet.override(current_environment: env,
395391
loaders: Puppet::Pops::Loaders.new(env, true)) do
396392
if Puppet[:onetime]
397393
onetime(daemon)
@@ -434,7 +430,7 @@ def fingerprint
434430

435431
def onetime(daemon)
436432
begin
437-
exitstatus = daemon.agent.run({:job_id => options[:job_id], :start_time => options[:start_time]})
433+
exitstatus = daemon.agent.run({:job_id => options[:job_id], :start_time => options[:start_time], :waitforcert => options[:waitforcert]})
438434
rescue => detail
439435
Puppet.log_exception(detail)
440436
end
@@ -524,10 +520,4 @@ def daemonize_process_when(should_daemonize)
524520

525521
daemon
526522
end
527-
528-
def wait_for_certificates
529-
waitforcert = options[:waitforcert] || (Puppet[:onetime] ? 0 : Puppet[:waitforcert])
530-
sm = Puppet::SSL::StateMachine.new(waitforcert: waitforcert)
531-
sm.ensure_client_certificate
532-
end
533523
end

lib/puppet/http/client.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class Puppet::HTTP::Client
2525
# used if :include_system_store is set to true
2626
# @param [Integer] redirect_limit default number of HTTP redirections to allow
2727
# in a given request. Can also be specified per-request.
28-
# @param [Integer] retry_limit number of HTTP reties allowed in a given
28+
# @param [Integer] retry_limit number of HTTP retries allowed in a given
2929
# request
3030
#
3131
def initialize(pool: Puppet::Network::HTTP::Pool.new(Puppet[:http_keepalive_timeout]), ssl_context: nil, system_ssl_context: nil, redirect_limit: 10, retry_limit: 100)
@@ -272,6 +272,24 @@ def delete(url, headers: {}, params: {}, options: {})
272272
#
273273
def close
274274
@pool.close
275+
@default_ssl_context = nil
276+
@default_system_ssl_context = nil
277+
end
278+
279+
def default_ssl_context
280+
cert = Puppet::X509::CertProvider.new
281+
password = cert.load_private_key_password
282+
283+
ssl = Puppet::SSL::SSLProvider.new
284+
ctx = ssl.load_context(certname: Puppet[:certname], password: password)
285+
ssl.print(ctx)
286+
ctx
287+
rescue => e
288+
# TRANSLATORS: `message` is an already translated string of why SSL failed to initialize
289+
Puppet.log_exception(e, _("Failed to initialize SSL: %{message}") % { message: e.message })
290+
# TRANSLATORS: `puppet agent -t` is a command and should not be translated
291+
Puppet.err(_("Run `puppet agent -t`"))
292+
raise e
275293
end
276294

277295
protected
@@ -409,6 +427,8 @@ def system_ssl_context
409427

410428
ssl = Puppet::SSL::SSLProvider.new
411429
@default_system_ssl_context = ssl.create_system_context(cacerts: cacerts)
430+
ssl.print(@default_system_ssl_context)
431+
@default_system_ssl_context
412432
end
413433

414434
def apply_auth(request, basic_auth)

lib/puppet/ssl/ssl_provider.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,27 @@ def verify_request(csr, public_key)
174174
csr
175175
end
176176

177+
def print(ssl_context, alg = 'SHA256')
178+
if Puppet::Util::Log.sendlevel?(:debug)
179+
chain = ssl_context.client_chain
180+
# print from root to client
181+
chain.reverse.each_with_index do |cert, i|
182+
digest = Puppet::SSL::Digest.new(alg, cert.to_der)
183+
if i == chain.length - 1
184+
Puppet.debug(_("Verified client certificate '%{subject}' fingerprint %{digest}") % {subject: cert.subject.to_utf8, digest: digest})
185+
else
186+
Puppet.debug(_("Verified CA certificate '%{subject}' fingerprint %{digest}") % {subject: cert.subject.to_utf8, digest: digest})
187+
end
188+
end
189+
ssl_context.crls.each do |crl|
190+
oid_values = Hash[crl.extensions.map { |ext| [ext.oid, ext.value] }]
191+
crlNumber = oid_values['crlNumber'] || 'unknown'
192+
authKeyId = (oid_values['authorityKeyIdentifier'] || 'unknown').chomp!
193+
Puppet.debug("Using CRL '#{crl.issuer.to_utf8}' authorityKeyIdentifier '#{authKeyId}' crlNumber '#{crlNumber }'")
194+
end
195+
end
196+
end
197+
177198
private
178199

179200
def default_flags

lib/puppet/ssl/state_machine.rb

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ def to_error(message, cause)
2727
detail.set_backtrace(cause.backtrace)
2828
Error.new(@machine, message, detail)
2929
end
30+
31+
def log_error(message)
32+
# When running daemonized we set stdout to /dev/null, so write to the log instead
33+
if Puppet[:daemonize]
34+
Puppet.err(message)
35+
else
36+
$stdout.puts(message)
37+
end
38+
end
3039
end
3140

3241
# Load existing CA certs or download them. Transition to NeedCRLs.
@@ -270,15 +279,15 @@ def initialize(machine)
270279
def next_state
271280
time = @machine.waitforcert
272281
if time < 1
273-
puts _("Exiting now because the waitforcert setting is set to 0.")
282+
log_error(_("Exiting now because the waitforcert setting is set to 0."))
274283
exit(1)
275284
elsif Time.now.to_i > @machine.wait_deadline
276-
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] }
285+
log_error(_("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] })
277286
exit(1)
278287
else
279288
Puppet.info(_("Will try again in %{time} seconds.") % {time: time})
280289

281-
# close persistent connections and session state before sleeping
290+
# close http/tls and session state before sleeping
282291
Puppet.runtime[:http].close
283292
@machine.session = Puppet.runtime[:http].create_session
284293

@@ -417,20 +426,7 @@ def ensure_ca_certificates
417426
def ensure_client_certificate
418427
final_state = run_machine(NeedLock.new(self), Done)
419428
ssl_context = final_state.ssl_context
420-
421-
if Puppet::Util::Log.sendlevel?(:debug)
422-
chain = ssl_context.client_chain
423-
# print from root to client
424-
chain.reverse.each_with_index do |cert, i|
425-
digest = Puppet::SSL::Digest.new(@digest, cert.to_der)
426-
if i == chain.length - 1
427-
Puppet.debug(_("Verified client certificate '%{subject}' fingerprint %{digest}") % {subject: cert.subject.to_utf8, digest: digest})
428-
else
429-
Puppet.debug(_("Verified CA certificate '%{subject}' fingerprint %{digest}") % {subject: cert.subject.to_utf8, digest: digest})
430-
end
431-
end
432-
end
433-
429+
@ssl_provider.print(ssl_context, @digest)
434430
ssl_context
435431
end
436432

spec/integration/application/agent_spec.rb

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'puppet_spec/puppetserver'
44
require 'puppet_spec/compiler'
55
require 'puppet_spec/https'
6+
require 'puppet/application/agent'
67

78
describe "puppet agent", unless: Puppet::Util::Platform.jruby? do
89
include PuppetSpec::Files
@@ -737,4 +738,111 @@ def with_another_agent_running(&block)
737738
end
738739
end
739740
end
741+
742+
context "ssl" do
743+
context "bootstrapping" do
744+
before :each do
745+
# reconfigure ssl to non-existent dir and files to force bootstrapping
746+
dir = tmpdir('ssl')
747+
Puppet[:ssldir] = dir
748+
Puppet[:localcacert] = File.join(dir, 'ca.pem')
749+
Puppet[:hostcrl] = File.join(dir, 'crl.pem')
750+
Puppet[:hostprivkey] = File.join(dir, 'cert.pem')
751+
Puppet[:hostcert] = File.join(dir, 'key.pem')
752+
753+
Puppet[:daemonize] = false
754+
Puppet[:logdest] = 'console'
755+
Puppet[:log_level] = 'info'
756+
end
757+
758+
it "exits if the agent is not allowed to wait" do
759+
Puppet[:waitforcert] = 0
760+
761+
server.start_server do |port|
762+
Puppet[:serverport] = port
763+
expect {
764+
agent.run
765+
}.to exit_with(1)
766+
.and output(%r{Exiting now because the waitforcert setting is set to 0}).to_stdout
767+
.and output(%r{Failed to submit the CSR, HTTP response was 404}).to_stderr
768+
end
769+
end
770+
771+
it "exits if the maxwaitforcert time is exceeded" do
772+
Puppet[:waitforcert] = 1
773+
Puppet[:maxwaitforcert] = 1
774+
775+
server.start_server do |port|
776+
Puppet[:serverport] = port
777+
expect {
778+
agent.run
779+
}.to exit_with(1)
780+
.and output(%r{Couldn't fetch certificate from CA server; you might still need to sign this agent's certificate \(127.0.0.1\). Exiting now because the maxwaitforcert timeout has been exceeded.}).to_stdout
781+
.and output(%r{Failed to submit the CSR, HTTP response was 404}).to_stderr
782+
end
783+
end
784+
end
785+
786+
def copy_fixtures(sources, dest)
787+
ssldir = File.join(PuppetSpec::FIXTURE_DIR, 'ssl')
788+
File.open(dest, 'w') do |f|
789+
sources.each do |s|
790+
f.write(File.read(File.join(ssldir, s)))
791+
end
792+
end
793+
end
794+
795+
it "reloads the CRL between runs" do
796+
Puppet[:localcacert] = ca = tmpfile('ca')
797+
Puppet[:hostcrl] = crl = tmpfile('crl')
798+
Puppet[:hostcert] = cert = tmpfile('cert')
799+
Puppet[:hostprivkey] = key = tmpfile('key')
800+
801+
copy_fixtures(%w[ca.pem intermediate.pem], ca)
802+
copy_fixtures(%w[crl.pem intermediate-crl.pem], crl)
803+
copy_fixtures(%w[127.0.0.1.pem], cert)
804+
copy_fixtures(%w[127.0.0.1-key.pem], key)
805+
806+
revoked = cert_fixture('revoked.pem')
807+
revoked_key = key_fixture('revoked-key.pem')
808+
809+
mounts = {}
810+
mounts[:catalog] = -> (req, res) {
811+
catalog = compile_to_catalog(<<~MANIFEST, node)
812+
file { '#{cert}':
813+
ensure => file,
814+
content => '#{revoked}'
815+
}
816+
file { '#{key}':
817+
ensure => file,
818+
content => '#{revoked_key}'
819+
}
820+
MANIFEST
821+
822+
res.body = formatter.render(catalog)
823+
res['Content-Type'] = formatter.mime
824+
}
825+
826+
server.start_server(mounts: mounts) do |port|
827+
Puppet[:serverport] = port
828+
Puppet[:daemonize] = false
829+
Puppet[:runinterval] = 1
830+
Puppet[:waitforcert] = 1
831+
Puppet[:maxwaitforcert] = 1
832+
833+
# simulate two runs of the agent, then return so we don't infinite loop
834+
allow_any_instance_of(Puppet::Daemon).to receive(:run_event_loop) do |instance|
835+
instance.agent.run(splay: false)
836+
instance.agent.run(splay: false)
837+
end
838+
839+
agent.command_line.args << '--verbose'
840+
expect {
841+
agent.run
842+
}.to exit_with(1)
843+
.and output(%r{Exiting now because the maxwaitforcert timeout has been exceeded}).to_stdout
844+
.and output(%r{Certificate 'CN=revoked' is revoked}).to_stderr
845+
end
846+
end
847+
end
740848
end

0 commit comments

Comments
 (0)