Skip to content

Instantly share code, notes, and snippets.

@mikob
Last active February 15, 2024 13:47
Show Gist options
  • Star 68 You must be signed in to star a gist
  • Fork 18 You must be signed in to fork a gist
  • Save mikob/a89fd8c5f85e0a00d557 to your computer and use it in GitHub Desktop.
Save mikob/a89fd8c5f85e0a00d557 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='/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
@mak2014
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
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