diff --git a/acceptance/lib/puppet_x/acceptance/external_cert_fixtures.rb b/acceptance/lib/puppet_x/acceptance/external_cert_fixtures.rb index 7eadc7426..5a8a0610a 100644 --- a/acceptance/lib/puppet_x/acceptance/external_cert_fixtures.rb +++ b/acceptance/lib/puppet_x/acceptance/external_cert_fixtures.rb @@ -1,324 +1,361 @@ module PuppetX module Acceptance class ExternalCertFixtures attr_reader :fixture_dir attr_reader :test_dir attr_reader :master_name attr_reader :agent_name ## # ExternalCerts provides a utility class to fill in fixture data and other # large blobs of text configuration for the acceptance testing of External CA # behavior. # # @param [String] fixture_dir The fixture directory to read from. # # @param [String] test_dir The directory on the remote system, used for # filling in templates. # # @param [String] master_name The common name the master should be reachable # at. This name should match up with the certificate files in the fixture # directory, e.g. master1.example.org. # # @param [String] agent_name The common name the agent is configured to use. # This name should match up with the certificate files in the fixture # directory, e.g. def initialize(fixture_dir, test_dir, master_name = "master1.example.org", agent_name = "agent1.example.org") @fixture_dir = fixture_dir @test_dir = test_dir @master_name = master_name @agent_name = agent_name end def master_short_name @master_short_name ||= master_name.gsub(/\..*/, '') end def host_entry - @host_entry ||= "127.0.0.3 #{master_name} #{master_short_name} puppet\n" + @host_entry ||= "127.0.0.3 #{master_name} #{master_short_name} puppet" end def root_ca_cert @root_ca_cert ||= File.read(File.join(fixture_dir, 'root', 'ca-root.crt')) end def agent_ca_cert @agent_ca_cert ||= File.read(File.join(fixture_dir, 'agent-ca', 'ca-agent-ca.crt')) end def master_ca_cert @master_ca_cert ||= File.read(File.join(fixture_dir, 'master-ca', 'ca-master-ca.crt')) end def master_ca_crl @master_ca_crl ||= File.read(File.join(fixture_dir, 'master-ca', 'ca-master-ca.crl')) end def agent_cert @agent_cert ||= File.read(File.join(fixture_dir, 'leaves', "#{agent_name}.issued_by.agent-ca.crt")) end def agent_key @agent_key ||= File.read(File.join(fixture_dir, 'leaves', "#{agent_name}.issued_by.agent-ca.key")) end def agent_email_cert @agent_email_cert ||= File.read(File.join(fixture_dir, 'leaves', "#{agent_name}.email.issued_by.agent-ca.crt")) end def agent_email_key @agent_email_cert ||= File.read(File.join(fixture_dir, 'leaves', "#{agent_name}.email.issued_by.agent-ca.key")) end def master_cert @master_cert ||= File.read(File.join(fixture_dir, 'leaves', "#{master_name}.issued_by.master-ca.crt")) end def master_key @master_key ||= File.read(File.join(fixture_dir, 'leaves', "#{master_name}.issued_by.master-ca.key")) end def master_cert_rogue @master_cert_rogue ||= File.read(File.join(fixture_dir, 'leaves', "#{master_name}.issued_by.agent-ca.crt")) end def master_key_rogue @master_key_rogue ||= File.read(File.join(fixture_dir, 'leaves', "#{master_name}.issued_by.agent-ca.key")) end ## Configuration files def agent_conf @agent_conf ||= <<-EO_AGENT_CONF [main] color = false certname = #{agent_name} server = #{master_name} certificate_revocation = false # localcacert must contain the Root CA certificate to complete the 2 level CA # chain when an intermediate CA certificate is being used. Either the HTTP # server must send the intermediate certificate during the handshake, or the # agent must use the `ssl_client_ca_auth` setting to provide the client # certificate. localcacert = #{test_dir}/ca_root.crt EO_AGENT_CONF end def agent_conf_email @agent_conf ||= <<-EO_AGENT_CONF [main] color = false certname = #{agent_name} server = #{master_name} certificate_revocation = false hostcert = #{test_dir}/agent_email.crt hostkey = #{test_dir}/agent_email.key localcacert = #{test_dir}/ca_root.crt EO_AGENT_CONF end def agent_conf_crl @agent_conf_crl ||= <<-EO_AGENT_CONF [main] certname = #{agent_name} server = #{master_name} # localcacert must contain the Root CA certificate to complete the 2 level CA # chain when an intermediate CA certificate is being used. Either the HTTP # server must send the intermediate certificate during the handshake, or the # agent must use the `ssl_client_ca_auth` setting to provide the client # certificate. localcacert = #{test_dir}/ca_root.crt EO_AGENT_CONF end def master_conf @master_conf ||= <<-EO_MASTER_CONF [master] ca = false certname = #{master_name} ssl_client_header = HTTP_X_CLIENT_DN ssl_client_verify_header = HTTP_X_CLIENT_VERIFY EO_MASTER_CONF end ## # Passenger Rack compliant config.ru which is responsible for starting the # Puppet master. def config_ru @config_ru ||= <<-EO_CONFIG_RU \$0 = "master" ARGV << "--rack" ARGV << "--confdir=#{test_dir}/etc/master" ARGV << "--vardir=#{test_dir}/etc/master/var" require 'puppet/util/command_line' run Puppet::Util::CommandLine.new.execute EO_CONFIG_RU end ## # auth_conf should return auth authorization file that allows *.example.org # access to to the full REST API. def auth_conf @auth_conf_content ||= File.read(File.join(fixture_dir, 'auth.conf')) end ## # Apache configuration with Passenger def httpd_conf @httpd_conf ||= <<-EO_HTTPD_CONF User apache Group apache ServerRoot "/etc/httpd" PidFile run/httpd.pid Timeout 60 KeepAlive Off MaxKeepAliveRequests 100 KeepAliveTimeout 15 StartServers 8 MinSpareServers 5 MaxSpareServers 20 ServerLimit 256 MaxClients 256 MaxRequestsPerChild 4000 StartServers 4 MaxClients 300 MinSpareThreads 25 MaxSpareThreads 75 ThreadsPerChild 25 MaxRequestsPerChild 0 LoadModule auth_basic_module modules/mod_auth_basic.so LoadModule auth_digest_module modules/mod_auth_digest.so LoadModule authn_file_module modules/mod_authn_file.so LoadModule authn_alias_module modules/mod_authn_alias.so LoadModule authn_anon_module modules/mod_authn_anon.so LoadModule authn_dbm_module modules/mod_authn_dbm.so LoadModule authn_default_module modules/mod_authn_default.so LoadModule authz_host_module modules/mod_authz_host.so LoadModule authz_user_module modules/mod_authz_user.so LoadModule authz_owner_module modules/mod_authz_owner.so LoadModule authz_groupfile_module modules/mod_authz_groupfile.so LoadModule authz_dbm_module modules/mod_authz_dbm.so LoadModule authz_default_module modules/mod_authz_default.so LoadModule ldap_module modules/mod_ldap.so LoadModule authnz_ldap_module modules/mod_authnz_ldap.so LoadModule include_module modules/mod_include.so LoadModule log_config_module modules/mod_log_config.so LoadModule logio_module modules/mod_logio.so LoadModule env_module modules/mod_env.so LoadModule ext_filter_module modules/mod_ext_filter.so LoadModule mime_magic_module modules/mod_mime_magic.so LoadModule expires_module modules/mod_expires.so LoadModule deflate_module modules/mod_deflate.so LoadModule headers_module modules/mod_headers.so LoadModule usertrack_module modules/mod_usertrack.so LoadModule setenvif_module modules/mod_setenvif.so LoadModule mime_module modules/mod_mime.so LoadModule dav_module modules/mod_dav.so LoadModule status_module modules/mod_status.so LoadModule autoindex_module modules/mod_autoindex.so LoadModule info_module modules/mod_info.so LoadModule dav_fs_module modules/mod_dav_fs.so LoadModule vhost_alias_module modules/mod_vhost_alias.so LoadModule negotiation_module modules/mod_negotiation.so LoadModule dir_module modules/mod_dir.so LoadModule actions_module modules/mod_actions.so LoadModule speling_module modules/mod_speling.so LoadModule userdir_module modules/mod_userdir.so LoadModule alias_module modules/mod_alias.so LoadModule substitute_module modules/mod_substitute.so LoadModule rewrite_module modules/mod_rewrite.so LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_balancer_module modules/mod_proxy_balancer.so LoadModule proxy_ftp_module modules/mod_proxy_ftp.so LoadModule proxy_http_module modules/mod_proxy_http.so LoadModule proxy_ajp_module modules/mod_proxy_ajp.so LoadModule proxy_connect_module modules/mod_proxy_connect.so LoadModule cache_module modules/mod_cache.so LoadModule suexec_module modules/mod_suexec.so LoadModule disk_cache_module modules/mod_disk_cache.so LoadModule cgi_module modules/mod_cgi.so LoadModule version_module modules/mod_version.so LoadModule ssl_module modules/mod_ssl.so LoadModule passenger_module modules/mod_passenger.so ServerName #{master_name} DocumentRoot "#{test_dir}/etc/master/public" DefaultType text/plain TypesConfig /etc/mime.types # Same thing, just using a certificate issued by the Agent CA, which should not # be trusted by the clients. Listen 8140 https Listen 8141 https SSLEngine on SSLProtocol ALL -SSLv2 SSLCipherSuite ALL:!ADH:RC4+RSA:+HIGH:+MEDIUM:-LOW:-SSLv2:-EXP SSLCertificateFile "#{test_dir}/master.crt" SSLCertificateKeyFile "#{test_dir}/master.key" # The chain file is sent to the client during handshake. SSLCertificateChainFile "#{test_dir}/ca_master_bundle.crt" # The CA cert file is used to authenticate clients SSLCACertificateFile "#{test_dir}/ca_agent_bundle.crt" SSLVerifyClient optional SSLVerifyDepth 2 SSLOptions +StdEnvVars RequestHeader set X-SSL-Subject %{SSL_CLIENT_S_DN}e RequestHeader set X-Client-DN %{SSL_CLIENT_S_DN}e RequestHeader set X-Client-Verify %{SSL_CLIENT_VERIFY}e DocumentRoot "#{test_dir}/etc/master/public" PassengerRoot /usr/share/gems/gems/passenger-3.0.17 PassengerRuby /usr/bin/ruby RackAutoDetect On RackBaseURI / SSLEngine on SSLProtocol ALL -SSLv2 SSLCipherSuite ALL:!ADH:RC4+RSA:+HIGH:+MEDIUM:-LOW:-SSLv2:-EXP SSLCertificateFile "#{test_dir}/master_rogue.crt" SSLCertificateKeyFile "#{test_dir}/master_rogue.key" SSLCertificateChainFile "#{test_dir}/ca_agent_bundle.crt" SSLCACertificateFile "#{test_dir}/ca_agent_bundle.crt" SSLVerifyClient optional SSLVerifyDepth 2 SSLOptions +StdEnvVars RequestHeader set X-SSL-Subject %{SSL_CLIENT_S_DN}e RequestHeader set X-Client-DN %{SSL_CLIENT_S_DN}e RequestHeader set X-Client-Verify %{SSL_CLIENT_VERIFY}e DocumentRoot "#{test_dir}/etc/master/public" PassengerRoot /usr/share/gems/gems/passenger-3.0.17 PassengerRuby /usr/bin/ruby RackAutoDetect On RackBaseURI / EO_HTTPD_CONF end + + ## + # webserver.conf for a trustworthy master for use with Jetty + def jetty_webserver_conf_for_trustworthy_master + @jetty_webserver_conf_for_trustworthy_master ||= <<-EO_WEBSERVER_CONF +webserver: { + client-auth: want + ssl-host: 0.0.0.0 + ssl-port: 8140 + + ssl-cert: "#{test_dir}/master.crt" + ssl-key: "#{test_dir}/master.key" + + ssl-cert-chain: "#{test_dir}/ca_master_bundle.crt" + ssl-ca-cert: "#{test_dir}/ca_agent_bundle.crt" +} + EO_WEBSERVER_CONF + end + + ## + # webserver.conf for a rogue master for use with Jetty + def jetty_webserver_conf_for_rogue_master + @jetty_webserver_conf_for_rogue_master ||= <<-EO_WEBSERVER_CONF +webserver: { + client-auth: want + ssl-host: 0.0.0.0 + ssl-port: 8140 + + ssl-cert: "#{test_dir}/master_rogue.crt" + ssl-key: "#{test_dir}/master_rogue.key" + + ssl-cert-chain: "#{test_dir}/ca_agent_bundle.crt" + ssl-ca-cert: "#{test_dir}/ca_agent_bundle.crt" +} + EO_WEBSERVER_CONF + end + end end end diff --git a/acceptance/tests/external_ca_support/jetty_external_root_ca.rb b/acceptance/tests/external_ca_support/jetty_external_root_ca.rb new file mode 100644 index 000000000..be768b891 --- /dev/null +++ b/acceptance/tests/external_ca_support/jetty_external_root_ca.rb @@ -0,0 +1,172 @@ +begin + require 'puppet_x/acceptance/external_cert_fixtures' +rescue LoadError + $LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__)) + require 'puppet_x/acceptance/external_cert_fixtures' +end + +confine :except, :type => 'pe' + +skip_test "Test only supported on Jetty" unless @options[:is_puppetserver] + +# Verify that a trivial manifest can be run to completion. +# Supported Setup: Single, Root CA +# - Agent and Master SSL cert issued by the Root CA +# - Revocation disabled on the agent `certificate_revocation = false` +# - CA disabled on the master `ca = false` +# +# SUPPORT NOTES +# +# * If the x509 alt names extension is used when issuing SSL server certificates +# for the Puppet master, then the client SSL certificate issued by an external +# CA must posses the DNS common name in the alternate name field. This is +# due to a bug in Ruby. If the CN is not duplicated in the Alt Names, then +# the following error will appear on the agent with MRI 1.8.7: +# +# Warning: Server hostname 'master1.example.org' did not match server +# certificate; expected one of master1.example.org, DNS:puppet, +# DNS:master-ca.example.org +# +# See: https://bugs.ruby-lang.org/issues/6493 +test_name "Puppet agent and master work when both configured with externally issued certificates from independent intermediate CAs" + +step "Copy certificates and configuration files to the master..." +fixture_dir = File.expand_path('../fixtures', __FILE__) +testdir = master.tmpdir('jetty_external_root_ca') +fixtures = PuppetX::Acceptance::ExternalCertFixtures.new(fixture_dir, testdir) + +jetty_confdir = master['puppetserver-confdir'] + +# Register our cleanup steps early in a teardown so that they will happen even +# if execution aborts part way. +teardown do + step "Restore /etc/hosts and webserver.conf" + on master, "cp -p '#{testdir}/hosts' /etc/hosts" + on master, "cp -p '#{testdir}/webserver.conf.orig' '#{jetty_confdir}/webserver.conf'" +end + +# Read all of the CA certificates. + +# Copy all of the x.509 fixture data over to the master. +create_remote_file master, "#{testdir}/ca_root.crt", fixtures.root_ca_cert +create_remote_file master, "#{testdir}/ca_agent.crt", fixtures.agent_ca_cert +create_remote_file master, "#{testdir}/ca_master.crt", fixtures.master_ca_cert +create_remote_file master, "#{testdir}/ca_master.crl", fixtures.master_ca_crl +create_remote_file master, "#{testdir}/ca_master_bundle.crt", "#{fixtures.master_ca_cert}\n#{fixtures.root_ca_cert}\n" +create_remote_file master, "#{testdir}/ca_agent_bundle.crt", "#{fixtures.agent_ca_cert}\n#{fixtures.root_ca_cert}\n" +create_remote_file master, "#{testdir}/agent.crt", fixtures.agent_cert +create_remote_file master, "#{testdir}/agent.key", fixtures.agent_key +create_remote_file master, "#{testdir}/agent_email.crt", fixtures.agent_email_cert +create_remote_file master, "#{testdir}/agent_email.key", fixtures.agent_email_key +create_remote_file master, "#{testdir}/master.crt", fixtures.master_cert +create_remote_file master, "#{testdir}/master.key", fixtures.master_key +create_remote_file master, "#{testdir}/master_rogue.crt", fixtures.master_cert_rogue +create_remote_file master, "#{testdir}/master_rogue.key", fixtures.master_key_rogue + +## +# Now create the master and agent puppet.conf +# +# We need to create the public directory for Passenger and the modules +# directory to avoid `Error: Could not evaluate: Could not retrieve information +# from environment production source(s) puppet://master1.example.org/plugins` +on master, "mkdir -p #{testdir}/etc/{master/{public,modules/empty/lib},agent}" +# Backup /etc/hosts +on master, "cp -p /etc/hosts '#{testdir}/hosts'" + +# Make master1.example.org resolve if it doesn't already. +on master, "grep -q -x '#{fixtures.host_entry}' /etc/hosts || echo '#{fixtures.host_entry}' >> /etc/hosts" + +create_remote_file master, "#{testdir}/etc/agent/puppet.conf", fixtures.agent_conf +create_remote_file master, "#{testdir}/etc/agent/puppet.conf.crl", fixtures.agent_conf_crl +create_remote_file master, "#{testdir}/etc/agent/puppet.conf.email", fixtures.agent_conf_email + +# auth.conf to allow *.example.com access to the rest API +create_remote_file master, "#{testdir}/etc/master/auth.conf", fixtures.auth_conf + +create_remote_file master, "#{testdir}/etc/master/config.ru", fixtures.config_ru + +step "Set filesystem permissions and ownership for the master" +# These permissions are required for the JVM to start Puppet as puppet +on master, "chown -R puppet:puppet #{testdir}/etc/master" +on master, "chown -R puppet:puppet #{testdir}/*.crt" +on master, "chown -R puppet:puppet #{testdir}/*.key" +on master, "chown -R puppet:puppet #{testdir}/*.crl" + +# These permissions are just for testing, end users should protect their +# private keys. +on master, "chmod -R a+rX #{testdir}" + +agent_cmd_prefix = "--confdir #{testdir}/etc/agent --vardir #{testdir}/etc/agent/var" + +# Move the agent SSL cert and key into place. +# The filename must match the configured certname, otherwise Puppet will try +# and generate a new certificate and key +step "Configure the agent with the externally issued certificates" +on master, "mkdir -p #{testdir}/etc/agent/ssl/{public_keys,certs,certificate_requests,private_keys,private}" +create_remote_file master, "#{testdir}/etc/agent/ssl/certs/#{fixtures.agent_name}.pem", fixtures.agent_cert +create_remote_file master, "#{testdir}/etc/agent/ssl/private_keys/#{fixtures.agent_name}.pem", fixtures.agent_key + +on master, "cp -p '#{jetty_confdir}/webserver.conf' '#{testdir}/webserver.conf.orig'" +create_remote_file master, "#{jetty_confdir}/webserver.conf", + fixtures.jetty_webserver_conf_for_trustworthy_master + +master_opts = { + 'master' => { + 'ca' => false, + 'certname' => fixtures.master_name, + 'ssl_client_header' => "HTTP_X_CLIENT_DN", + 'ssl_client_verify_header' => "HTTP_X_CLIENT_VERIFY" + } +} + +step "Start the Puppet master service..." +with_puppet_running_on(master, master_opts) do + # Now, try and run the agent on the master against itself. + step "Successfully run the puppet agent on the master" + on master, puppet_agent("#{agent_cmd_prefix} --test"), :acceptable_exit_codes => (0..255) do + assert_no_match /Creating a new SSL key/, stdout + assert_no_match /\Wfailed\W/i, stderr + assert_no_match /\Wfailed\W/i, stdout + assert_no_match /\Werror\W/i, stderr + assert_no_match /\Werror\W/i, stdout + # Assert the exit code so we get a "Failed test" instead of an "Errored test" + assert exit_code == 0 + end + + step "Master accepts client cert with email address in subject" + on master, "cp #{testdir}/etc/agent/puppet.conf{,.no_email}" + on master, "cp #{testdir}/etc/agent/puppet.conf{.email,}" + on master, puppet_agent("#{agent_cmd_prefix} --test"), :acceptable_exit_codes => (0..255) do + assert_no_match /\Wfailed\W/i, stdout + assert_no_match /\Wfailed\W/i, stderr + assert_no_match /\Werror\W/i, stdout + assert_no_match /\Werror\W/i, stderr + # Assert the exit code so we get a "Failed test" instead of an "Errored test" + assert exit_code == 0 + end + + step "Agent refuses to connect to revoked master" + on master, "cp #{testdir}/etc/agent/puppet.conf{,.no_crl}" + on master, "cp #{testdir}/etc/agent/puppet.conf{.crl,}" + + revoke_opts = "--hostcrl #{testdir}/ca_master.crl" + on master, puppet_agent("#{agent_cmd_prefix} #{revoke_opts} --test"), :acceptable_exit_codes => (0..255) do + assert_match /certificate revoked.*?example.org/, stderr + assert exit_code == 1 + end +end + +create_remote_file master, "#{jetty_confdir}/webserver.conf", + fixtures.jetty_webserver_conf_for_rogue_master + +with_puppet_running_on(master, master_opts) do + step "Agent refuses to connect to a rogue master" + on master, puppet_agent("#{agent_cmd_prefix} --ssl_client_ca_auth=#{testdir}/ca_master.crt --test"), :acceptable_exit_codes => (0..255) do + assert_no_match /Creating a new SSL key/, stdout + assert_match /certificate verify failed/i, stderr + assert_match /The server presented a SSL certificate chain which does not include a CA listed in the ssl_client_ca_auth file/i, stderr + assert exit_code == 1 + end +end + +step "Finished testing External Certificates"