Created
June 5, 2020 20:01
-
-
Save chadh/66925963ef04fb482927d95a413cdea6 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
#!/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