diff --git a/acceptance/suites/tests/authorization/default_rules.rb b/acceptance/suites/tests/authorization/default_rules.rb index eb2366a4e..c851c1059 100644 --- a/acceptance/suites/tests/authorization/default_rules.rb +++ b/acceptance/suites/tests/authorization/default_rules.rb @@ -34,6 +34,22 @@ def curl_unauthenticated(path) on(master, curl) end +# Issue a request with a certificate other than the primary's own. The +# primary's certificate carries the pp_cli_auth extension so that its CLI +# tooling can reach administrative endpoints, which makes it unsuitable for +# testing rules that gate on that extension. An ordinary agent certificate +# does not carry it. +def curl_with_cert(certname, path, &block) + curl = 'curl ' + curl += "--cert $(puppet config print ssldir)/certs/#{certname}.pem " + curl += "--key $(puppet config print ssldir)/private_keys/#{certname}.pem " + curl += '--cacert $(puppet config print localcacert) ' + curl += "--write-out '\\nSTATUSCODE=%{http_code}\\n' " + curl += "https://#{master}:8140#{path}" + result = on(master, curl) + block.call(result.stdout) +end + def assert_allowed(stdout, expected_statuscode = 200) refute_match(/Forbidden request/, stdout) assert_match(/STATUSCODE=#{expected_statuscode}/, stdout) @@ -52,6 +68,27 @@ def report_query(node) curl += '\",\"metrics\":{},\"logs\":[],\"resource_statuses\":{}}"' end +# An ordinary agent certificate, used to test rules that are gated on the +# pp_cli_auth extension. Generated once the server is up, and cleaned up in +# the teardown below. +agent_certname = 'filebucket-read-test-agent' + +# 'puppetserver ca generate' refuses to run while any key file for the certname +# is still on disk, so clear them before generating as well as afterwards. That +# keeps the test repeatable if an earlier run was interrupted before teardown. +def revoke_and_remove_cert(certname) + on(master, "puppetserver ca clean --certname #{certname}", + :accept_all_exit_codes => true) + on(master, "rm -f $(puppet config print ssldir)/certs/#{certname}.pem " \ + "$(puppet config print ssldir)/private_keys/#{certname}.pem " \ + "$(puppet config print ssldir)/public_keys/#{certname}.pem", + :accept_all_exit_codes => true) +end + +teardown do + revoke_and_remove_cert(agent_certname) +end + with_puppet_running_on(master, {}) do masterfqdn = on(master, '/opt/puppetlabs/bin/facter fqdn').stdout.chomp @@ -98,15 +135,14 @@ def report_query(node) assert_allowed(stdout) end - # In PE, the master (specifically the orchestrator) - # is allowed to make report submissions on behalf of - # other nodes + # Nodes may submit only their own reports (allow: "$1"); submitting on + # behalf of other nodes was a PE orchestrator capability that OpenVox + # does not have. Note: this must not be guarded on master.is_pe? -- + # this suite's beaker options do not set a host type, and beaker's + # default type is 'pe', which made is_pe? return true on FOSS hosts + # and select the wrong assertion. curl_authenticated(report_query('notme')) do |stdout| - if master.is_pe? - assert_allowed(stdout) - else - assert_denied(stdout, /\/puppet\/v3\/report\/notme \(method :put\)/) - end + assert_denied(stdout, /\/puppet\/v3\/report\/notme \(method :put\)/) end curl_unauthenticated(report_query(masterfqdn)) do |stdout| @@ -143,6 +179,9 @@ def report_query(node) sum = 'a' * 64 bucket_path = "/puppet/v3/file_bucket_file/sha256/#{sum}?environment=production" + revoke_and_remove_cert(agent_certname) + on(master, "puppetserver ca generate --certname=#{agent_certname}") + # Agents are allowed 'head' so they can test whether content is already # stored before uploading it. We'd actually need to store a file in the # filebucket in order to get back a 200, but we know that a 404 means we got @@ -151,9 +190,20 @@ def report_query(node) assert_allowed(stdout, 404) end + curl_with_cert(agent_certname, "#{bucket_path} --head") do |stdout| + assert_allowed(stdout, 404) + end + # Reading content back out is restricted to certificates carrying the - # pp_cli_auth extension, which an ordinary agent certificate does not have. + # pp_cli_auth extension. The primary's own certificate carries it, so the + # request reaches the endpoint and is answered with a 404. curl_authenticated(bucket_path) do |stdout| + assert_allowed(stdout, 404) + end + + # An ordinary agent certificate does not carry pp_cli_auth, so reads are + # refused even though the same certificate may store content. + curl_with_cert(agent_certname, bucket_path) do |stdout| assert_denied(stdout, /\/puppet\/v3\/file_bucket_file\/sha256\/#{sum} \(method :get\)/) end diff --git a/spec/puppet-server-lib/puppet/jvm/config_spec.rb b/spec/puppet-server-lib/puppet/jvm/config_spec.rb index 9fd05e842..a03a2844d 100644 --- a/spec/puppet-server-lib/puppet/jvm/config_spec.rb +++ b/spec/puppet-server-lib/puppet/jvm/config_spec.rb @@ -42,11 +42,23 @@ end end + it "falls back to the vendored PEM CA bundle when the keystore is absent" do + stub_const('Puppet::Server::Config::PUPPET_KEYSTORE_LOCATION', + 'spec/fixtures/does-not-exist') + stub_const('Puppet::Server::Config::PUPPET_CA_BUNDLE_LOCATION', + 'spec/fixtures/ca-cert.pem') + + expect(Puppet).not_to receive(:warning) + + ssl_context = Puppet::Server::Config.puppet_and_system_ssl_context + expect(ssl_context).to be_a_kind_of(Java::JavaxNetSsl::SSLContext) + end + it "warns if :ssl_trust_store is set but not readable" do Puppet[:ssl_trust_store] = "spec/fixtures/foo.pem" allow(File).to receive(:exist?).and_return(false) - expect(Puppet).to receive(:warning).with(/Could not find OpenVox-vendored keystore/) + expect(Puppet).to receive(:warning).with(/Could not find an OpenVox-vendored trust store/) expect(Puppet).to receive(:warning).with(/The 'ssl_trust_store' setting does not refer to a file/) Puppet::Server::Config.puppet_and_system_ssl_context diff --git a/src/ruby/puppetserver-lib/puppet/server/config.rb b/src/ruby/puppetserver-lib/puppet/server/config.rb index a9c714178..2a1711f6b 100644 --- a/src/ruby/puppetserver-lib/puppet/server/config.rb +++ b/src/ruby/puppetserver-lib/puppet/server/config.rb @@ -19,6 +19,7 @@ class Puppet::Server::Config PUPPET_KEYSTORE_LOCATION = '/opt/puppetlabs/puppet/ssl/puppet-cacerts' + PUPPET_CA_BUNDLE_LOCATION = '/opt/puppetlabs/puppet/ssl/cert.pem' CERT_REGEX = /.*-----BEGIN CERTIFICATE-----.*/ def self.initialize_puppet_server(puppet_server_config) @@ -82,8 +83,16 @@ def self.load_puppet_and_system_ssl_context truststore = stores['truststore'] if File.exist?(PUPPET_KEYSTORE_LOCATION) associate_entries(truststore, PUPPET_KEYSTORE_LOCATION) + elsif File.exist?(PUPPET_CA_BUNDLE_LOCATION) + # puppet-runtime stopped shipping the Java keystore form of the + # vendored CA bundle (puppet-runtime@066fd48); load the PEM bundle + # it still ships instead. + SSLUtils.associateCertsFromReader( + truststore, + 'openvox_vendored_ca_bundle', + FileReader.new(PUPPET_CA_BUNDLE_LOCATION)) else - Puppet.warning("Could not find OpenVox-vendored keystore at '#{PUPPET_KEYSTORE_LOCATION}'") + Puppet.warning("Could not find an OpenVox-vendored trust store at '#{PUPPET_KEYSTORE_LOCATION}' or '#{PUPPET_CA_BUNDLE_LOCATION}'") end if additional_store_location = Puppet[:ssl_trust_store]