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"