-
-
Save jeffmccune/5267272 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /bin/bash | |
## NOTE: | |
## This script requires the follwing in /etc/hosts: | |
## 127.0.0.2 puppet master1.example.org | |
# This will fail with a stock puppet 3.1.1, but will succeed if all of the | |
# certificate subjects contain only the "CN" portion, and no O, OU, or | |
# emailAddress. | |
# basic config to describe the environment | |
B="/tmp/certchain" | |
HTTPS_PORT=8443 | |
OPENSSL=$(which openssl) | |
PUPPET=~/code/puppet/t/puppet/bin/puppet | |
# utility method to dedent a heredoc | |
dedent() { | |
python -c 'import sys, textwrap; print textwrap.dedent(sys.stdin.read())' | |
} | |
# invoke openssl | |
openssl() { | |
echo "----" | |
echo "running" ${OPENSSL} ${@} | |
echo " in $PWD" | |
${OPENSSL} "${@}" | |
} | |
show_cert() { | |
local cert="$1" | |
openssl x509 -in "${cert}" -noout -text | |
} | |
hash_cert() { | |
local cert="$1" | |
local certdir="${B}/certdir" | |
local h=$(${OPENSSL} x509 -hash -noout -in ${cert}) | |
mkdir -p "${certdir}" | |
ln -s "$cert" "${certdir}/${h}.0" | |
} | |
show_crl() { | |
local crl="$1" | |
openssl crl -in "${crl}" -noout -text | |
} | |
hash_crl() { | |
local crl="$1" | |
local certdir="${B}/certdir" | |
local h=$(${OPENSSL} crl -hash -noout -in ${crl}) | |
mkdir -p "${certdir}" | |
ln -s "$crl" "${certdir}/${h}.r0" | |
} | |
# clean out any messes ths script has made | |
clean_up() { | |
stop_apache | |
rm -rf "$B" | |
} | |
stop_apache() { | |
local pid pidfile="${B}/apache/httpd.pid" | |
while true; do | |
pid=$(cat "${pidfile}" 2>/dev/null || true) | |
[ -z "$pid" ] && break # break if the pid is gone | |
kill "$pid" || break # break if the kill fails (process is gone) | |
sleep 0.1 | |
done | |
} | |
# perform basic setup: make directories, etc. | |
set_up() { | |
mkdir -p "$B" | |
} | |
# create CA certificates: | |
# | |
# * $B/root_ca | |
# * $B/master{1..2}_ca | |
# | |
# with each containing: | |
# | |
# * openssl.conf -- suitable for signing certificates | |
# * $name-ca.key -- PEM format certificate key, with no password | |
# * $name-ca.crt -- PEM format certificate | |
create_ca_certs() { | |
local name cn dir subj ca_config | |
for name in root master{1..2}; do | |
dir="${B}/${name}" | |
mkdir -p "${dir}" | |
( cd "${dir}" | |
# if this is the root cert, make a self-signed cert | |
if [ "$name" = "root" ]; then | |
subj="/CN=Root CA/OU=Server Operations/O=Example Org, LLC" | |
openssl req -new -newkey rsa -days 365 -nodes -x509 -subj "${subj}" -keyout "${name}-ca.key" -out "${name}-ca.crt" | |
else | |
# make a new key for the CA | |
openssl genrsa -out "${name}-ca.key" | |
# build a CSR out of it | |
dedent > openssl.tmp << OPENSSL_TMP | |
[req] | |
prompt = no | |
distinguished_name = dn_config | |
[dn_config] | |
commonName = CA on ${name}.example.org | |
emailAddress = test@example.org | |
organizationalUnitName = Server Operations | |
organizationName = Example Org, LLC | |
OPENSSL_TMP | |
openssl req -config openssl.tmp -new -key "${name}-ca.key" -out "${name}-ca.csr" | |
rm openssl.tmp | |
# sign it with the root CA | |
openssl ca -config ../root/openssl.conf -in "${name}-ca.csr" -notext -out "${name}-ca.crt" -batch | |
# clean up the now-redundant csr | |
rm "${name}-ca.csr" | |
fi | |
# set up the CA config; this uses the same file for all, but with different options | |
# for the root and master CAs | |
[ "$name" = "root" ] && ca_config=root_ca_config || ca_config=master_ca_config | |
dedent > openssl.conf << OPENSSL_CONF | |
[ca] | |
default_ca = ${ca_config} | |
# Root CA | |
[root_ca_config] | |
certificate = ${dir}/${name}-ca.crt | |
private_key = ${dir}/${name}-ca.key | |
database = ${dir}/inventory.txt | |
new_certs_dir = ${dir}/certs | |
serial = ${dir}/serial | |
default_crl_days = 3650 | |
default_days = 1825 | |
default_md = sha1 | |
policy = root_ca_policy | |
x509_extensions = root_ca_exts | |
[root_ca_policy] | |
commonName = supplied | |
emailAddress = supplied | |
organizationName = supplied | |
organizationalUnitName = supplied | |
[root_ca_exts] | |
authorityKeyIdentifier = keyid,issuer:always | |
basicConstraints = critical,CA:true | |
keyUsage = keyCertSign, cRLSign | |
# Master CA | |
[master_ca_config] | |
certificate = ${dir}/${name}-ca.crt | |
private_key = ${dir}/${name}-ca.key | |
database = ${dir}/inventory.txt | |
new_certs_dir = ${dir}/certs | |
serial = ${dir}/serial | |
default_crl_days = 7 | |
default_days = 1825 | |
default_md = sha1 | |
policy = master_ca_policy | |
x509_extensions = master_ca_exts | |
[master_ca_policy] | |
commonName = supplied | |
# default extensions for clients | |
[master_ca_exts] | |
authorityKeyIdentifier = keyid,issuer:always | |
basicConstraints = critical,CA:false | |
keyUsage = keyEncipherment, digitalSignature | |
extendedKeyUsage = serverAuth, clientAuth | |
# extensions for the master certificate (specifically adding subjectAltName) | |
[master_self_ca_exts] | |
authorityKeyIdentifier = keyid,issuer:always | |
basicConstraints = critical,CA:false | |
keyUsage = keyEncipherment, digitalSignature | |
extendedKeyUsage = serverAuth, clientAuth | |
# include the master's fqdn here, as well as in the CN, to work | |
# around https://bugs.ruby-lang.org/issues/6493 | |
subjectAltName = DNS:puppet,DNS:${name}.example.org | |
OPENSSL_CONF | |
touch inventory.txt | |
mkdir certs | |
echo 01 > serial | |
show_cert "${dir}/${name}-ca.crt" | |
hash_cert "${dir}/${name}-ca.crt" | |
# generate an empty CRL for this CA | |
openssl ca -config "${dir}/openssl.conf" -gencrl -out "${dir}/${name}-ca.crl" | |
show_crl "${dir}/${name}-ca.crl" | |
hash_crl "${dir}/${name}-ca.crl" | |
) | |
done | |
} | |
# revoke leaf cert for $1 issued by master CA $2 | |
revoke_leaf_cert() { | |
local fqdn="$1" | |
local master="$2" | |
local dir="${B}/${master}" | |
# revoke the cert and regenerate the crl | |
openssl ca -config "${dir}/openssl.conf" -revoke "${B}/leaves/${fqdn}.crt" | |
openssl ca -config "${dir}/openssl.conf" -gencrl -out "${dir}/${master}-ca.crl" | |
show_crl "${dir}/${master}-ca.crl" | |
kill -HUP $(< "${B}/apache/httpd.pid") | |
} | |
# revoke CA cert for $1 | |
revoke_ca_cert() { | |
local master="$1" | |
local dir="${B}/root" | |
# revoke the cert and regenerate the crl | |
openssl ca -config "${dir}/openssl.conf" -revoke "${B}/${master}/${master}-ca.crt" | |
openssl ca -config "${dir}/openssl.conf" -gencrl -out "${dir}/root-ca.crl" | |
show_crl "${dir}/root-ca.crl" | |
kill -HUP $(< "${B}/apache/httpd.pid") | |
} | |
# create a "leaf" certificate for the given fqdn, signed by the given master. | |
# $fqdn.{key,crt} will be placed in "${B}/leaves" | |
create_leaf_cert() { | |
local fqdn="$1" master="$2" | |
local masterdir="${B}/${master}" | |
local dir="${B}/leaves" | |
local exts | |
mkdir -p "${dir}" | |
( cd "${dir}" | |
openssl genrsa -out "${fqdn}.key" | |
openssl req -subj "/CN=${fqdn}" -new -key "${fqdn}.key" -out "${fqdn}.csr" | |
# if we are generating a cert for the master itself, use master_self_ca_certs | |
[ "${fqdn}" = "${master}.example.org" ] && exts="-extensions master_self_ca_exts" | |
openssl ca -config "${B}/${master}/openssl.conf" -in "${fqdn}.csr" -notext -out "${fqdn}.crt" -batch $exts | |
) | |
show_cert "${dir}/${fqdn}.crt" | |
} | |
create_leaf_certs() { | |
# most of these aren't required for our tests | |
create_leaf_cert master1.example.org master1 | |
#create_leaf_cert client1a.example.org master1 | |
#create_leaf_cert client1b.example.org master1 | |
#create_leaf_cert master2.example.org master2 | |
create_leaf_cert client2a.example.org master2 | |
create_leaf_cert client2b.example.org master2 | |
} | |
set_up_apache() { | |
local apachedir="${B}/apache" | |
mkdir -p "${apachedir}/puppetmaster/public" | |
echo 'passed'> "${apachedir}/puppetmaster/public/test.txt" | |
dedent > "${apachedir}/httpd.conf" <<HTTPD_CONF | |
LoadModule mpm_prefork_module modules/mod_mpm_prefork.so | |
LoadModule unixd_module modules/mod_unixd.so | |
LoadModule authn_core_module modules/mod_authn_core.so | |
LoadModule authz_core_module modules/mod_authz_core.so | |
LoadModule ssl_module modules/mod_ssl.so | |
LoadModule headers_module modules/mod_headers.so | |
LoadModule passenger_module modules/mod_passenger.so | |
# NOTE: these may be "fun" to make portable.. | |
PassengerRoot /usr/share/gems/gems/passenger-3.0.17 | |
PassengerRuby /usr/bin/ruby | |
PidFile "${apachedir}/httpd.pid" | |
ErrorLog "${apachedir}/error_log" | |
LogLevel debug | |
Listen ${HTTPS_PORT} https | |
SSLRandomSeed startup file:/dev/urandom 256 | |
SSLRandomSeed connect builtin | |
SSLEngine on | |
SSLProtocol all -SSLv2 | |
SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5 | |
# puppet-relevant SSL config: | |
SSLCertificateFile "${B}/leaves/master1.example.org.crt" | |
SSLCertificateKeyFile "${B}/leaves/master1.example.org.key" | |
# chain in the intermediate cert for this master | |
SSLCertificateChainFile "${B}/master1/master1-ca.crt" | |
SSLCACertificatePath "${B}/certdir" | |
SSLCARevocationPath "${B}/certdir" | |
SSLCARevocationCheck chain | |
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 | |
ServerName master1.example.org | |
DocumentRoot "${apachedir}/puppetmaster/public" | |
# NOTE: this is httpd-2.4 syntax | |
<Directory "${apachedir}/puppetmaster/public"> | |
Require all granted | |
</Directory> | |
RackAutoDetect On | |
RackBaseURI / | |
HTTPD_CONF | |
} | |
set_up_puppetmaster() { | |
local apachedir="${B}/apache" | |
local masterdir="${B}/puppetmaster" | |
mkdir -p "${masterdir}/conf" "${masterdir}/var" "${masterdir}/manifests" | |
dedent > "${apachedir}/puppetmaster/config.ru" <<CONFIG_RU | |
\$0 = "master" | |
ARGV << "--rack" | |
ARGV << "--debug" | |
ARGV << "--confdir=${masterdir}/conf" | |
ARGV << "--vardir=${masterdir}/var" | |
require 'puppet/application/master' | |
run Puppet::Application[:master].run | |
CONFIG_RU | |
dedent > "${masterdir}/conf/puppet.conf" <<PUPPET_CONF | |
[main] | |
node_name = cert | |
strict_hostname_checking = true | |
[master] | |
ca = false | |
ssl_client_header = SSL_CLIENT_S_DN | |
ssl_client_verify_header = SSL_CLIENT_VERIFY | |
manifestdir = ${masterdir}/manifests | |
PUPPET_CONF | |
dedent > "${masterdir}/manifests/site.pp" <<SITE_PP | |
node /client.*.example.org/ { | |
file { "${B}/i_was_here": | |
content => "yes I was" | |
} | |
} | |
SITE_PP | |
} | |
start_apache() { | |
local apachedir="${B}/apache" | |
if ! httpd -f "${apachedir}/httpd.conf"; then | |
[ -f "${apachedir}/error_log" ] && tail "${apachedir}/error_log" | |
false | |
fi | |
} | |
check_apache() { | |
# verify the SSL config with openssl. Note that s_client exits with 0 | |
# no matter what, so this greps the output for an OK status. Also note | |
# that this only checks that the validation of the server certs is OK, since | |
# client validation is optional in the httpd config. | |
echo $'GET /test.txt HTTP/1.0\n' | \ | |
openssl s_client -connect "127.0.0.1:${HTTPS_PORT}" -verify 2 \ | |
-cert "${B}/leaves/client2a.example.org.crt" \ | |
-key "${B}/leaves/client2a.example.org.key" \ | |
-CAfile "${B}/root/root-ca.crt" \ | |
| tee "${B}/verify.out" | |
cat "${B}/apache/error_log" | |
grep -q "Verify return code: 0 (ok)" "${B}/verify.out" | |
} | |
check_puppetmaster() { | |
# this is insecure, because otherwise curl will check that 127.0.0.1 == | |
# master1.example.org and fail; validation of the server certs is done | |
# above in check_apache, so this is fine. | |
curl -vks --fail \ | |
--header 'Accept: yaml' \ | |
--cert "${B}/leaves/client2a.example.org.crt" \ | |
--key "${B}/leaves/client2a.example.org.key" \ | |
"https://127.0.0.1:${HTTPS_PORT}/production/catalog/client2a.example.org" >/dev/null | |
echo | |
} | |
# set up the agent with the given fqdn | |
set_up_agent() { | |
local fqdn="$1" | |
local agentdir="${B}/agent" | |
mkdir -p "${agentdir}/conf" "${agentdir}/var" | |
mkdir -p "${agentdir}/conf/ssl/private_keys" "${agentdir}/conf/ssl/certs" | |
dedent > "${agentdir}/conf/puppet.conf" <<PUPPET_CONF | |
[agent] | |
server = master1.example.org | |
# agent can't verify CRLs for a chain | |
certificate_revocation = false | |
masterport = ${HTTPS_PORT} | |
ca_port = ${HTTPS_PORT} | |
report = false | |
PUPPET_CONF | |
# the client needs its own leaf cert/key and the root CA cert | |
cp "${B}/leaves/${fqdn}.key" "${agentdir}/conf/ssl/private_keys/${fqdn}.pem" | |
cp "${B}/leaves/${fqdn}.crt" "${agentdir}/conf/ssl/certs/${fqdn}.pem" | |
cp "${B}/root/root-ca.crt" "${agentdir}/conf/ssl/certs/ca.pem" | |
} | |
# run the agent with the given fqdn; with -f, expect it to fail | |
run_agent() { | |
local fqdn="$1" | |
local expfail=false | |
local agentdir="${B}/agent" | |
if [ "$2" = "-f" ]; then | |
expfail=true | |
fi | |
# the manifest will create this file | |
rm -f "${B}/i_was_here" | |
if puppet agent --test --debug \ | |
--confdir=/tmp/certchain/agent/conf/ --vardir=/tmp/certchain/agent/var/ \ | |
--fqdn "${fqdn}"; then | |
if ${expfail}; then | |
false | |
fi | |
# This appears not to work in 3.1.x | |
#test -f "${B}/i_was_here" | |
else | |
echo "expected failure" | |
if ! ${expfail}; then | |
false | |
fi | |
# This appears not to work in 3.1.x | |
#test ! -f "${B}/i_was_here" | |
fi | |
} | |
call() { | |
echo "==== $1 ====" | |
"${@}" | |
} | |
main() { | |
call clean_up | |
call set_up | |
call create_ca_certs | |
call create_leaf_certs | |
call set_up_apache | |
call set_up_puppetmaster | |
call start_apache | |
call check_apache | |
call check_puppetmaster | |
# set up the client to run normally, then revoke the client's cert and see it fail | |
call set_up_agent client2a.example.org | |
call run_agent client2a.example.org | |
call revoke_leaf_cert client2a.example.org master2 | |
call run_agent client2a.example.org -f | |
# set up the client to run another host, then revoke master2's CA cert | |
call set_up_agent client2b.example.org | |
call run_agent client2b.example.org | |
call revoke_ca_cert master2 | |
call run_agent client2b.example.org -f | |
call clean_up | |
} | |
set -e | |
main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment