Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save tobiasmcnulty/f1465b124e34a9dd6872a2f23e314a83 to your computer and use it in GitHub Desktop.
Save tobiasmcnulty/f1465b124e34a9dd6872a2f23e314a83 to your computer and use it in GitHub Desktop.
AWS, ELB, Let's Encrypt
Elastic Load Balancer, CloudFront and Let's Encrypt

This simple bash script will check if a ssl certificate expires within a defined threshold, perform a letsencrypt certificate renewal, upload the new certificate and set loadbalancers to use the new certificate. It can optionally update a cloudfront distribution to use the same certificate and delete any old certificates. It is perfect for those who want to use Let's Encrypt with their SSL-enabled ELB and or CloudFront. It uses the aws-cli and letsencrypt.

It is recommended to be run in as a daily cron-job. If the certificate does not need to be renewed, the script will print "The certificate is up to date, no need for renewal..." and do nothing. You can force a renewal with the --force option.

Requirements

  • awscli (pip install awscli)
  • letsencrypt (git clone https://github.com/letsencrypt/letsencrypt)
  • bc (apt-get install bc)

IAM User required Permissions

cloudfront:ListDistributions
cloudfront:GetDistributionConfig
cloudfront:UpdateDistribution
elasticloadbalancing:DescribeLoadBalancers
elasticloadbalancing:SetLoadBalancerListenerSSLCertificate
iam:ListServerCertificates
iam:UploadServerCertificate
iam:DeleteServerCertificate

Passing the Let's Encrypt Challenge

Handling of ACME challenge is not done through this script. The config file for LE should define a webroot-path where letsencrypt can write a hash. For example in the /web/root/path/.well-known/acme-challenge directory. That path should be accessible by let's encrypts servers so your cert renewal request can be authenticated. This script will need access to the acme-challenge directory to write a hash. If you are dockerizing the script, you can use the --volumes-from feature and attach this dockerized service to your nginx/apache/other webserver that hosts the acme challenge.

#!/bin/bash
#
# [1] "If you're using a CA other than AWS Certificate Manager and if you want to
# use the same certificate both for CloudFront and for other AWS services,
# you must upload the certificate twice: once for CloudFront and once for the
# other services." (http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/SecureConnections.html#CNAMEsAndHTTPS)
#
# If using your cert on cloudfront, make sure your cloudfront distribution has
# a behavior for .well-known/acme-challenge/* that lets requests through to
# the origin. You will probably need to forward the 'host' header in this behavior.
#
# References:
# http://marketing.intracto.com/renew-https-certificate-on-amazon-cloudfront
# https://vincent.composieux.fr/article/install-configure-and-automatically-renew-let-s-encrypt-ssl-certificate
# https://github.com/alex/letsencrypt-aws
CONFIG_FILE='letsencrypt.ini'
# Use RAM disk to avoid saving private key to disk:
LE_WORKDIR='/dev/shm/LE-work-dir'
LE_CONFIGDIR='/dev/shm/LE-config-dir'
LE_CMD="./certbot-auto --work-dir=$LE_WORKDIR --config-dir=$LE_CONFIGDIR --debug"
LOAD_BALANCER_NAME='...'
INSTANCE_PROTO=HTTP
INSTANCE_PORT=80
EXP_LIMIT=30 # renew if cert will expire in this many days or fewer
export AWS_DEFAULT_REGION=us-east-1
set -e
#set -o pipefail
if [ ! -f $CONFIG_FILE ]; then
echo "[ERROR] config file does not exist: $CONFIG_FILE"
exit 1;
fi
echo "Making sure dependencies are present..."
if [ ! -f certbot-auto ]; then
wget https://dl.eff.org/certbot-auto
chmod a+x certbot-auto
fi
sudo `which pip` install -U pip virtualenv
DOMAIN=`grep "^\s*domains" $CONFIG_FILE | sed "s/^\s*domains\s*=\s*//" | sed 's/(\s*)\|,.*$//'`
echo "Checking expiration date for $DOMAIN..."
DATE_NOW=$(date -d "now" +%s)
EXP_DATE=$(date -d "`echo | openssl s_client -connect $DOMAIN:443 2>/dev/null | openssl x509 -noout -enddate | cut -d"=" -f2-`" +%s)
EXP_DAYS=$(echo \( $EXP_DATE - $DATE_NOW \) / 86400 |bc)
ISSUER=$(echo | openssl s_client -connect $DOMAIN:443 2>/dev/null | openssl x509 -noout -issuer | cut -d"=" -f2-)
if [ "$EXP_DAYS" -gt "$EXP_LIMIT" ] && [[ $ISSUER == *"Let's Encrypt"* ]] && [ "$1" != "--force" ] ; then
echo "The certificate is up to date and uses Let's Encrypt, no need for renewal ($EXP_DAYS days left)."
exit 0;
else
echo "The certificate for $DOMAIN is about to expire soon or is not using Let's Encrypt:"
echo "Days remaining: $EXP_DAYS"
echo "Issuer: $ISSUER"
echo "Starting webroot renewal script..."
$LE_CMD certonly --agree-tos --renew-by-default --config $CONFIG_FILE
CERT_NAME="letsencrypt_cert_`date +%m-%d-%y_%H-%M-%S`"
echo "Uploading $CERT_NAME to IAM"
# path needs to be this to work for cloudfront (will work with elb too)
CERT_RES=$(sudo aws iam upload-server-certificate \
--server-certificate-name $CERT_NAME \
--certificate-body file://$LE_CONFIGDIR/live/$DOMAIN/cert.pem \
--private-key file://$LE_CONFIGDIR/live/$DOMAIN/privkey.pem \
--certificate-chain file://$LE_CONFIGDIR/live/$DOMAIN/chain.pem \
--output json
)
echo "CERT_RES: $CERT_RES"
NEW_CERT_ARN=$(echo $CERT_RES | python -c 'import json,sys;print(json.load(sys.stdin)["ServerCertificateMetadata"]["Arn"])')
echo "NEW_CERT_ARN: $NEW_CERT_ARN"
ELB_DESC=$(aws elb describe-load-balancers \
--load-balancer-name $LOAD_BALANCER_NAME \
--output json
)
echo "ELB_DESC: $ELB_DESC"
OLD_CERT_NAMES=$(echo $ELB_DESC | python -c 'import json,sys;print(" ".join(l["Listener"]["SSLCertificateId"].split("/")[-1] for l in json.load(sys.stdin)["LoadBalancerDescriptions"][0]["ListenerDescriptions"] if "SSLCertificateId" in l["Listener"] and l["Listener"]["LoadBalancerPort"] == 443))')
echo "OLD_CERT_NAMES: $OLD_CERT_NAMES"
echo "Waiting for cert to become active..."
sleep 20
if [ "x$OLD_CERT_NAMES" != "x" ]; then
echo "Updating ELB IAM cert to $NEW_CERT_ARN..."
aws elb set-load-balancer-listener-ssl-certificate \
--load-balancer-name $LOAD_BALANCER_NAME \
--load-balancer-port 443 \
--ssl-certificate-id $NEW_CERT_ARN
echo "Waiting 10 minutes, then deleting old certs: $OLD_CERT_NAMES"
sleep 600
echo $OLD_CERT_NAMES | xargs -n 1 aws iam delete-server-certificate --server-certificate-name
else
echo "No listener for port 443 found; creating new HTTPS listener..."
aws elb create-load-balancer-listeners \
--load-balancer-name $LOAD_BALANCER_NAME \
--listeners Protocol=HTTPS,LoadBalancerPort=443,InstanceProtocol=$INSTANCE_PROTO,InstancePort=$INSTANCE_PORT,SSLCertificateId=$NEW_CERT_ARN
fi
echo "Renewal process finished for domain $DOMAIN, cleaning up..."
sudo rm -rf "$LE_CONFIGDIR"
exit 0;
fi
@v1ct0rv
Copy link

v1ct0rv commented Nov 14, 2017

Hi, do you have a sample config file?

@starskrime
Copy link

Config file please

@rkorszun
Copy link

rkorszun commented Dec 19, 2017

the ini file looks like this:

authenticator = webroot
webroot-path = /var/www/
domains = www.mydomain.net
renew-by-default = true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment