This Lambda function will check if a Let's Encrypt certificate is older than 60 days. If it is older than 60 days, then an EC2 instance will be launched that will update the certificate and then terminate itself.
Below is a list of known issues and limitations with this implementation.
The following permissions are needed
Example Lambda role policy with minimal permissions.
The following permissions are needed
Example EC2 role policy with minimal permissions.
The CloudWatch Event can be scheduled to run once a week and must invoke the Lambda function with input as described below. For each line below the “LEFT”:
is the key / variable name used in the Lambda function and the : “RIGHT”
is the value that applies to your environment. The keys “IMPORT_ACM”, “DNS_SERVICE”, and “FORCE_RUN” are optional, all other keys are required. The key “DNS_SERVICE” can be either Route53 or cloudflare.
{ "EC2_ROLE_NAME": "certgen-role-e2", "DOMAIN_NAME": "DOMAIN.TLD", "CERTGEN_BUCKET": "certgenbucket", "SNS_TOPIC_ARN": "arn:aws:sns:us-east-1:123456789012:certgen", "IMPORT_ACM": "Yes", "DNS_SERVICE": "Route53", "FORCE_RUN": "No" }
The values show above match up with the values used in the following example Lambda and EC2 role policies.
When creating the Lambda function placement inside a VPC is not required, but assignment of a Lambda IAM role is required.
import boto3 from datetime import datetime from datetime import timedelta def lambda_handler(event, context): region = 'us-east-1' days_old = 60 client_ssm = boto3.client('ssm', region_name=region) client_ec2 = boto3.client('ec2', region_name=region) client_sns = boto3.client('sns', region_name=region) #amzn2_ami_parameter = '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' amzn2_ami_parameter = '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64' current_ami = client_ssm.get_parameter(Name=amzn2_ami_parameter)['Parameter']['Value'] #print(current_ami) ec2_role_name = event['EC2_ROLE_NAME'] domain_name = event['DOMAIN_NAME'] certgen_bucket = event['CERTGEN_BUCKET'] sns_topic_arn = event['SNS_TOPIC_ARN'] if 'DNS_SERVICE' in event: dns_service = event['DNS_SERVICE'] else: dns_service = 'Route53' if 'IMPORT_ACM' in event: if event['IMPORT_ACM'] == 'Yes': import_acm = 'Yes' else: import_acm = 'No' else: import_acm = 'No' user_data = '''#!/bin/bash sudo yum -y install python3 sudo python3 -m ensurepip --upgrade python3 -m pip install certbot-route53 certbot-dns-cloudflare --user #pip3 install certbot-route53 --user AWS_DEFAULT_REGION="{}" DOMAINNAME="{}" echo "$DOMAINNAME" > /root/certgen-$DOMAINNAME.log CERTGENBUCKETNAME="{}" SNSTOPICARN="{}" IMPORT_ACM="{}" CERT_REGION="us-east-1" echo "$CERTGENBUCKETNAME" >> /root/certgen-$DOMAINNAME.log if [[ $DNS_SERVICE == "cloudflare" ]]; then echo "dns service is cloudflare" >> /root/certgen-$DOMAINNAME.log echo "dns_cloudflare_api_token = $(aws --region $CERT_REGION ssm get-parameter --name /certgen/$DOMAINNAME/cloudflare --with-decryption --query 'Parameter.Value' --output text)" > /root/cloudflare.ini chmod 600 /root/cloudflare.ini /root/.local/bin/certbot --config-dir /root/certbot/config --work-dir /root/certbot/work --logs-dir /root/certbot/logs certonly --dns-cloudflare --dns-cloudflare-credentials /root/cloudflare.ini --dns-cloudflare-propagation-seconds 30 -d *.$DOMAINNAME,$DOMAINNAME -n --email hostmaster@$DOMAINNAME --no-eff-email --agree-tos >> /root/certgen-$DOMAINNAME.log fi if [[ $DNS_SERVICE == "Route53" ]]; then echo "dns service is Route53" >> /root/certgen-$DOMAINNAME.log /root/.local/bin/certbot --config-dir /root/certbot/config --work-dir /root/certbot/work --logs-dir /root/certbot/logs certonly --dns-route53 --dns-route53-propagation-seconds 30 -d *.$DOMAINNAME,$DOMAINNAME -n --email hostmaster@$DOMAINNAME --no-eff-email --agree-tos >> /root/certgen-$DOMAINNAME.log fi ls -l /root/certgen/config/live/$DOMAINNAME/ >> /root/certgen-$DOMAINNAME.log P12PASSWORD="$(openssl rand -base64 32)" openssl pkcs12 -export -inkey /root/certgen/config/live/$DOMAINNAME/privkey.pem -in /root/certgen/config/live/$DOMAINNAME/fullchain.pem -out /root/certgen/$DOMAINNAME.p12 -password pass:"$P12PASSWORD" ## To encrypt and backup certbot configuration # tar zcf - /root/certgen | openssl enc -e -aes256 -out /root/certgen.tar.gz -pass pass:"$P12PASSWORD" ## To decrypt certbot configuration backup file # openssl enc -d -aes256 -in /root/certgen.tar.gz -pass pass:"$P12PASSWORD" | tar zxv aws --region $CERT_REGION ssm put-parameter --name /certgen/$DOMAINNAME/p12password --value "$P12PASSWORD" --type SecureString --overwrite >> /root/certgen-$DOMAINNAME.log aws --region $CERT_REGION s3 cp /root/certgen/$DOMAINNAME.p12 s3://$CERTGENBUCKETNAME/$DOMAINNAME/$DOMAINNAME.p12 >> /root/certgen-$DOMAINNAME.log ## To backup encrypted certbot configuration file # aws --region $CERT_REGION s3 cp /root/certgen.tar.gz s3://$CERTGENBUCKETNAME/$DOMAINNAME/certgen-$DOMAINNAME.tar.gz >> /root/certgen-$DOMAINNAME.log if [[ $IMPORT_ACM == "Yes" ]]; then CERT_ARN=$(aws --region $CERT_REGION acm list-certificates --query 'CertificateSummaryList[?DomainName==`'*.$DOMAINNAME'`].CertificateArn' --output text) echo $CERT_ARN >> /root/certgen-$DOMAINNAME.log if [[ $CERT_ARN != "" ]]; then echo "Existing certificate for $DOMAINNAME was found with ARN $CERT_ARN. Updating." >> /root/certgen-$DOMAINNAME.log aws --region $CERT_REGION acm import-certificate \ --certificate file:///root/certgen/config/live/$DOMAINNAME/cert.pem \ --private-key file:///root/certgen/config/live/$DOMAINNAME/privkey.pem \ --certificate-chain file:///root/certgen/config/live/$DOMAINNAME/chain.pem \ --certificate-arn $CERT_ARN >> /root/certgen-$DOMAINNAME.log else echo "Existing certificate for $DOMAINNAME was not found. Importing." >> /root/certgen-$DOMAINNAME.log aws --region $CERT_REGION acm import-certificate \ --certificate file:///root/certgen/config/live/$DOMAINNAME/cert.pem \ --private-key file:///root/certgen/config/live/$DOMAINNAME/privkey.pem \ --certificate-chain file:///root/certgen/config/live/$DOMAINNAME/chain.pem >> /root/certgen-$DOMAINNAME.log fi fi aws --region $CERT_REGION s3 cp /root/certgen-$DOMAINNAME.log s3://$CERTGENBUCKETNAME/certgen-$DOMAINNAME.log aws --region us-east-1 sns publish --topic-arn $SNSTOPICARN --subject "Status $DOMAINNAME" --message file:///root/certgen-$DOMAINNAME.log sleep 60 sudo shutdown -h now '''.format(region, domain_name, certgen_bucket, sns_topic_arn, import_acm) temp_instance_paramaters = { 'ImageId': current_ami, 'InstanceType': 't3.micro', 'Monitoring': {'Enabled':False}, 'IamInstanceProfile': {'Name':ec2_role_name}, 'InstanceInitiatedShutdownBehavior': 'terminate', 'CreditSpecification': {'CpuCredits':'standard'}, 'MinCount': 1, 'MaxCount': 1, 'UserData': user_data } #print(event) #print(ec2_role_name) print(domain_name) print(certgen_bucket) #print(user_data) #print(temp_instance_paramaters) try: cert_password = client_ssm.get_parameter(Name='/certgen/{}/p12password'.format(domain_name)) #print(cert_password) parameter_date = cert_password['Parameter']['LastModifiedDate'].astimezone() current_date = datetime.utcnow().astimezone() expiration_date = current_date - timedelta(days = days_old) if expiration_date < parameter_date: print('Certificate for {} does not need to be updated'.format(domain_name)) if 'FOURCE_RUN' in event: if event['FOURCE_RUN'] == 'Yes': print('Force run requested, generating certificate anyways.') else: return { 'statusCode': 200, 'body': 'Certificate for {} does not need to be updated'.format(domain_name) } else: return { 'statusCode': 200, 'body': 'Certificate for {} does not need to be updated'.format(domain_name) } print('Certificate is about to expire and needs to be updated') client_sns.publish( TopicArn=sns_topic_arn, Subject='Renewing {} certificate'.format(domain_name), Message=''' Certificate for {} will expire in {} days. Launching instance to renew certificate. '''.format(domain_name, 90 - days_old) ) except: print('Certificate for domain {} was not found'.format(domain_name)) client_sns.publish( TopicArn=sns_topic_arn, Subject='Creating {} certificate'.format(domain_name), Message=''' Certificate for {} was not found. Launching instance to create certificate. '''.format(domain_name) ) temp_instance = client_ec2.run_instances(**temp_instance_paramaters) #print(temp_instance) print('Launched instance {}'.format(temp_instance['Instances'][0]['InstanceId'])) return { 'statusCode': 200, 'body': 'Launched instance {}'.format(temp_instance['Instances'][0]['InstanceId']) }