Skip to content

Instantly share code, notes, and snippets.

@chadh
Created June 5, 2020 20:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chadh/66925963ef04fb482927d95a413cdea6 to your computer and use it in GitHub Desktop.
Save chadh/66925963ef04fb482927d95a413cdea6 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
#
# Puppet Server Intermediatet CA Certificate Generation
#
# USAGE:
# certgen.sh -C <ca arn> -R <CA AWS region> -n <host name> -r <instance AWS region> -s <stack> -o <output file>
#
# -C, --ca_arn: AWS CA arn
# -R, --ca_region: Region of AWS CA
# -n, --name: name of instance (e.g. "puppet-1")
# -r, --region: AWS region of instance
# -s, --stack: resource set that
# -f, --force: continue even if cert already exists
# -g, --generate: generate a new certificate and update {ca,crl}.pem
#
# EXAMPLE
# certgen.sh --generate --ca_arn arn:aws:acm-pca:us-west-1:XXXX:certificate-authority/XXXXXXXXX --ca_region <ca region> --name puppet-1 --region <region> --stack <stackname>
#
set -euo pipefail
scriptdir=$(cd "$(dirname "$0")"; pwd -P; cd - > /dev/null)
repodir="${scriptdir}/.."
datadir="${repodir}/data"
cafilesdir="${repodir}/site-modules/profile/files/puppet/certs"
cleanup() {
if [[ -n "${WORKDIR:-}" && -d "$WORKDIR" ]]; then
rm -rf "$WORKDIR"
fi
popd > /dev/null 2>&1
}
trap cleanup EXIT
usage() {
echo "ERROR ${1}" 1>&2
echo 1>&2
cat <<EOF 1>&2
Usage: certgen.sh -g -C <ca arn> -R <CA AWS region> -n <host name> -r <instance AWS region> -s <stack>
-C, --ca_arn: AWS CA arn (required)
-R, --ca_region: Region of AWS CA
-n, --name: name of instance (e.g. "puppet-1")
-r, --region: AWS region of instance
-s, --stack: resource set that
-f, --force: continue even if cert already exists
-g, --generate: create a new intermediate cert (-R, -n, -r, and -s required)
EOF
exit 1
}
#
# gencert function
#
# Issue an intermediate CA certificate from AWS Certificate Manager
#
# This function has side effects:
# 1. creates/updates private key in hiera
# 2. stores intermediate CA certificate and CRL in $cafilesdir
#
gencert() {
certname="${NAME}.${REGION}.${STACK}"
domain="my.domain"
ca_hash=$(cut -d':' -f6 <<<"$CA_ARN" | cut -d'/' -f2)
#
# Generate private key and CSR
#
cat <<EOF > certreq.cnf
[ req ]
prompt = no
distinguished_name = req_distinguished_name
req_extensions = v3_req
[ req_distinguished_name ]
commonName = "${certname}"
[ v3_req ]
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${certname}.${domain}
DNS.1 = ${role}.${REGION}.${STACK}.${domain}
DNS.2 = ${role}.${REGION}.${STACK}
DNS.3 = ${role}.${REGION}.${domain}
DNS.4 = ${role}.${REGION}
DNS.5 = ${role}.${STACK}.${domain}
DNS.6 = ${role}.${STACK}
DNS.7 = ${role}
EOF
openssl req -config certreq.cnf -nodes -new -newkey rsa:2048 -days 3650 -keyout key.pem -out csr.pem
#
# Request and retrieve an intermediate CA cert from the Root CA
#
cert_arn=$(aws acm-pca issue-certificate --certificate-authority-arn "$CA_ARN" --template-arn arn:aws:acm-pca:::template/SubordinateCACertificate_PathLen0/V1 --csr "fileb:///$(pwd)/csr.pem" --signing-algorithm "SHA256WITHRSA" --validity Value=3650,Type="DAYS" --idempotency-token "$RANDOM" --region "$CA_REGION" | jq -r .CertificateArn)
sleep 2
aws acm-pca get-certificate --certificate-authority-arn "$CA_ARN" --certificate-arn "$cert_arn" --region "$CA_REGION" --output text | tr '\t' '\n' > certchain.pem
#
# generate crl for intermediate CA cert
#
cat <<EOF > cacrl.cnf
[ ca ]
default_ca = CA_default
[ CA_default ]
database = ./index.txt
crlnumber = ./crlnumber
default_md = sha256
crl_extensions = crl_ext
default_crl_days = 5475
[ crl_ext ]
authorityKeyIdentifier=keyid:always
EOF
touch index.txt
echo -ne "00" > crlnumber
openssl ca -config cacrl.cnf -keyfile key.pem -cert certchain.pem -gencrl -out int_crl.pem
#
# stash the private key generated above in eyaml and delete it
#
if [[ $FORCE == "no" && -n $(yq r "${datadir}/puppet.yaml" profile::puppet::server::ca_private_keys["$certname"]) ]]; then
echo "ERROR: key for ${certname} already exists in hiera. Bailing" 1>&2
exit 1
fi
pushd "$repodir" > /dev/null 2>&1
mkdir -p .tmp
mv "${WORKDIR}/key.pem" .tmp/
enc=$(./script/docker_run.sh eyamlEncrypt "-o string -f .tmp/key.pem" | tr -d '\r')
rm -f .tmp/key.pem
yq w -i "${datadir}/puppet.yaml" profile::puppet::server::ca_private_keys["$certname"] "$enc"
popd > /dev/null 2>&1
#
# save intermediate CA cert to Puppet profile
#
if [[ $FORCE == "n" && -f "${cafilesdir}/cert-${certname}.pem" ]]; then
echo "ERROR: ${certname} already has a cert saved in profile module" 1>&2
exit 1
fi
mv certchain.pem "${cafilesdir}/cert-${certname}.pem"
mv int_crl.pem "${cafilesdir}/crl-${certname}.pem"
}
#
# main
#
CA_ARN=""
CA_REGION=""
NAME=""
REGION=""
STACK=""
FORCE="no"
GENERATE="no"
while (( $# > 0 )); do
case "$1" in
-C|--ca_arn)
shift
CA_ARN="$1"
shift
;;
-R|--ca_region)
shift
CA_REGION="$1"
shift
;;
-n|--name)
shift
NAME="$1"
shift
;;
-r|--region)
shift
REGION="$1"
shift
;;
-s|--stack)
shift
STACK="$1"
shift
;;
-f | --force)
shift
FORCE="yes"
;;
-g | --generate)
shift
GENERATE="yes"
;;
*)
usage "Unrecognized argument ${1}"
;;
esac
done
if [[ -z $CA_ARN ]]; then
usage "Must specify CA arn"
fi
if [[ $GENERATE == "yes" ]]; then
if [[ -z $CA_ARN || -z $CA_REGION || -z $NAME || -z $REGION || -z $STACK ]]; then
usage "Must specify CA arn and region, as well as name, region, and stack"
fi
fi
# Name may be of form `<role>-<num>` Cert will use just <role>
num=$(cut -d. -f1 <<<"$NAME" | awk -F- '{ print $NF; }' )
if ! [[ $num =~ [0-9]+ ]]; then
role=$(cut -d. -f1 <<<"$NAME")
else
role=$(cut -d. -f1 <<<"$NAME" | rev | cut -d- -f2- | rev)
fi
ca_hash=$(cut -d':' -f6 <<<"$CA_ARN" | cut -d'/' -f2)
WORKDIR=$(mktemp -d)
pushd "$WORKDIR" > /dev/null 2>&1
#
# In order for agents to talk to any puppet server, every intermediate cert
# should be in the ca.pem. Same for the crl.pem.
#
#
# If --generate is provided, then request a new intermediate CA certificate
# and set its root CA certificate as our reference root cert
#
if [[ $GENERATE == "yes" ]]; then
gencert
# extract root CA cert
mkdir rootcert && pushd rootcert > /dev/null 2>&1
csplit -s -f "rootcert-" "${cafilesdir}/cert-${certname}.pem" '/-----BEGIN CERTIFICATE-----/'
# root cert should be the last cert in the chain
mv "$(find . -type f | sort -n | tail -n 1)" ../root_cert.pem
popd > /dev/null 2>&1
fi
#
# retrieve the Root CA CRL and convert to PEM format
#
aws s3 cp "s3://mybucket/crl/${ca_hash}.crl" raw.crl
openssl crl -inform DER -in raw.crl -outform PEM -out root_crl.pem
#
# Now loop through all the certificates and generate the aggregate
# ca.pem file with the root cert last.
#
mkdir int_certs && pushd int_certs > /dev/null 2>&1
first="yes"
for cert in "${cafilesdir}"/cert-*.pem; do
h=$(basename "$cert" .pem | cut -d- -f2-)
mkdir "$h" && pushd "$h" > /dev/null 2>&1
csplit -s -f "${h}-" "$cert" '/-----BEGIN CERTIFICATE-----/'
# root ca cert should be the last one
r=$(find . -type f | sort -n | tail -n 1)
# normally we match root certs against new cert, but if we are just doing a regeneration
# just use the first root cert
if [[ $GENERATE == "no" && $first == "yes" ]]; then
mv "$r" ../../root_cert.pem
first="no"
else
# just to be safe, make sure all root certs match
if ! diff ../../root_cert.pem "$r" > /dev/null 1>&2; then
echo "ERROR: root CA cert from chain doesn't match"
exit 1
fi
fi
# the top cert in the chain should be this host's intermediate CA cert
mv -- "${h}-00" ../"intcert-${h}.pem"
popd > /dev/null 2>&1
done
cat -- intcert-* ../root_cert.pem > ../ca.pem
popd > /dev/null 2>&1
#
# Unlike the cert files above, the crl files only contain a single host's CRL
# So we can just concatenate them, with the updated root CRL last.
#
cat -- "$cafilesdir"/crl-*.pem root_crl.pem > crl.pem
#
# Now stash the aggregate cert and crl in puppet
#
mv ca.pem "$repodir"/site-modules/profile/files/puppet/certs/
mv crl.pem "$repodir"/site-modules/profile/files/puppet/certs/
echo "Complete"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment