Skip to content

Instantly share code, notes, and snippets.

Embed
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.

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='/usr/local/etc/le-exampledomain-webroot.ini'
LE_PATH='/opt/letsencrypt'
LOAD_BALANCER_NAME='exampleloadbalancer'
export AWS_DEFAULT_PROFILE='ExampleProfile'
export AWS_DEFAULT_REGION='us-west-2'
EXP_LIMIT=30
UPDATE_CLOUDFRONT=true
# deletes the old certs
DELETE_OLD=false
if [ ! -f $CONFIG_FILE ]; then
echo "[ERROR] config file does not exist: $CONFIG_FILE"
exit 1;
fi
DOMAIN=`grep "^\s*domains" $CONFIG_FILE | sed "s/^\s*domains\s*=\s*//" | sed 's/(\s*)\|,.*$//'`
CERT_FILE="/etc/letsencrypt/live/$DOMAIN/fullchain.pem"
if [ ! -f $CERT_FILE ]; then
echo "[ERROR] certificate file not found for domain $DOMAIN."
fi
DATE_NOW=$(date -d "now" +%s)
EXP_DATE=$(date -d "`openssl x509 -in $CERT_FILE -text -noout | grep "Not After" | cut -c 25-`" +%s)
EXP_DAYS=$(echo \( $EXP_DATE - $DATE_NOW \) / 86400 |bc)
echo "Checking expiration date for $DOMAIN..."
if [ "$EXP_DAYS" -gt "$EXP_LIMIT" ] && [ "$1" != "--force" ] ; then
echo "The certificate is up to date, no need for renewal ($EXP_DAYS days left)."
exit 0;
else
echo "The certificate for $DOMAIN is about to expire soon. Starting webroot renewal script..."
$LE_PATH/letsencrypt-auto certonly --agree-tos --renew-by-default --config $CONFIG_FILE
CERT_NAME="auto_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=$(aws iam upload-server-certificate \
--server-certificate-name $CERT_NAME \
--certificate-body file:///etc/letsencrypt/live/$DOMAIN/cert.pem \
--private-key file:///etc/letsencrypt/live/$DOMAIN/privkey.pem \
--certificate-chain file:///etc/letsencrypt/live/$DOMAIN/chain.pem \
--path /cloudfront/production/ \
--output json
)
echo $CERT_RES
NEW_CERT_ARN=$(echo $CERT_RES | python -c 'import json,sys;obj=json.load(sys.stdin);print(obj["ServerCertificateMetadata"]["Arn"])')
echo $NEW_CERT_ARN
echo "Updating ELB IAM cert..."
sleep 20
aws elb set-load-balancer-listener-ssl-certificate \
--load-balancer-name $LOAD_BALANCER_NAME \
--load-balancer-port 443 \
--ssl-certificate-id $NEW_CERT_ARN
if [ UPDATE_CLOUDFRONT = true ] ; then
echo "Updating cloudfront distribution..."
aws configure set preview.cloudfront true
DISTRIBUTION=$(aws cloudfront list-distributions --query \
"DistributionList.Items[0].{DistributionId: Id}" --output text)
OLD_CERT_ARN=$(aws cloudfront list-distributions --query "DistributionList.Items[0].ViewerCertificate.Certificate" --output text)
aws cloudfront get-distribution-config --id $DISTRIBUTION --query 'DistributionConfig' --output json > /tmp/dist_config.json
sed -i "s/$OLD_CERT_ARN/$NEW_CERT_ARN/" /tmp/dist_config.json
aws cloudfront update-distribution \
--id $DISTRIBUTION \
--distribution-config file:///tmp/dist_config.json
echo "Done updating cloudfront"
fi
echo "Renewal process finished for domain $DOMAIN"
# TODO: test!
if [ DELETE_OLD = true ] ; then
echo "Deleting ALL old certs in 10 minutes..."
sleep 600
aws iam list-server-certificates --query \
"ServerCertificateMetadataList[?ServerCertificateName != '$CERT_NAME'].ServerCertificateName" \
--output text | xargs -n 1 aws iam \
delete-server-certificate \
--server-certificate-name
echo "Deleted old certificates"
fi
exit 0;
fi
@tarikul05

This comment has been minimized.

Copy link

tarikul05 commented Nov 3, 2017

Thanks for the nice script. what is the CONFIG_FILE file structure. will you please attach a sample CONFIG_FILE file.

@starskrime

This comment has been minimized.

Copy link

starskrime commented Dec 8, 2017

Thanks but config file please

@mak2014

This comment has been minimized.

Copy link

mak2014 commented Dec 29, 2017

Not entirely sure but this might help in regards to config file - https://gist.github.com/thisismitch/e1b603165523df66d5cc#file-le-renew-webroot-ini

@vacri

This comment has been minimized.

Copy link

vacri commented Mar 2, 2018

Warning - reading that last awscli | xargs command - if you use the 'DELETE_OLD' option here, it will delete ALL your IAM server certs, not just the 'old ones that match the domains of the newly-generated one'. This option should definitely never be used unless you definitely use only a single cert and will never use more than one.

You can check how many you have with
aws iam list-server-certificates --query "ServerCertificateMetadataList[].ServerCertificateName"

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.