Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
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.


  • awscli (pip install awscli)
  • letsencrypt (git clone
  • bc (apt-get install bc)

IAM User required Permissions


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.

# [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." (
# 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:
# Use RAM disk to avoid saving private key to disk:
LE_CMD="./certbot-auto --work-dir=$LE_WORKDIR --config-dir=$LE_CONFIGDIR --debug"
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;
echo "Making sure dependencies are present..."
if [ ! -f certbot-auto ]; then
chmod a+x certbot-auto
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;
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
NEW_CERT_ARN=$(echo $CERT_RES | python -c 'import json,sys;print(json.load(sys.stdin)["ServerCertificateMetadata"]["Arn"])')
ELB_DESC=$(aws elb describe-load-balancers \
--load-balancer-name $LOAD_BALANCER_NAME \
--output json
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 "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
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
echo "Renewal process finished for domain $DOMAIN, cleaning up..."
sudo rm -rf "$LE_CONFIGDIR"
exit 0;

This comment has been minimized.

Copy link

@v1ct0rv v1ct0rv commented Nov 14, 2017

Hi, do you have a sample config file?


This comment has been minimized.

Copy link

@starskrime starskrime commented Dec 8, 2017

Config file please


This comment has been minimized.

Copy link

@rkorszun rkorszun commented Dec 19, 2017

the ini file looks like this:

authenticator = webroot
webroot-path = /var/www/
domains =
renew-by-default = true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.