Automating Let's Encrypt SSL Certificate Management Using DNS-01 challenge and AWS
- Tutorial
The post describes the steps for automating the management of SSL certificates from Let's Encrypt CA using the DNS-01 challenge and AWS .
acme-dns-route53 is a tool that will allow us to implement this feature. He knows how to work with SSL certificates from Let's Encrypt, save them in Amazon Certificate Manager, use the Route53 API to implement the DNS-01 challenge, and, in the end, push notifications into SNS. The acme-dns-route53 as there is built-in functionality for use within the AWS Lambda, and this is what we need.
This article is divided into 4 sections:
- create a zip file;
- creating an IAM role;
- creating a lambda function that runs acme-dns-route53 ;
- creating a CloudWatch timer that triggers a function 2 times a day;
Note: Before you begin, you must install GoLang 1.9+ and AWS CLI
Create a zip file
acme-dns-route53 is written in GoLang and supports version no lower than 1.9.
We need to create a zip file with a binary acme-dns-route53
inside. To do this, install the acme-dns-route53
repository from GitHub using the command go install
:
$ env GOOS=linux GOARCH=amd64 go install github.com/begmaroman/acme-dns-route53
The binary is installed in the $GOPATH/bin
directory. Please note that during installation we specified two environment variables: GOOS=linux
and GOARCH=amd64
. They make it clear to the Go compiler about the need to create a binary suitable for Linux OS and amd64 architecture - this is what runs in AWS.
AWS assumes that our program is deployed in a zip file, so let's create an acme-dns-route53.zip
archive that will contain the newly installed binary:
$ zip -j ~/acme-dns-route53.zip $GOPATH/bin/acme-dns-route53
Note: the binary must be at the root of the zip archive. For this we use the -j
flag.
Now our zip nickname is ready for deployment, it remains only to create a role with the necessary rights.
Create IAM Roles
We need to assert the IAM role with the privileges our lambda needs during its execution.
Let's call this policy lambda-acme-dns-route53-executor
and give it a basic role right away AWSLambdaBasicExecutionRole
. This will allow our lambda to start and write logs to AWS CloudWatch service.
First, create a JSON file that describes our rights. This will essentially allow lambda services to use the role lambda-acme-dns-route53-executor
:
$ touch ~/lambda-acme-dns-route53-executor-policy.json
The contents of our file are as follows:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup"
],
"Resource": "arn:aws:logs:::*"
},
{
"Effect": "Allow",
"Action": [
"logs:PutLogEvents",
"logs:CreateLogStream"
],
"Resource": "arn:aws:logs:::log-group:/aws/lambda/acme-dns-route53:*"
},
{
"Sid": "",
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"cloudwatch:PutMetricData",
"acm:ImportCertificate",
"acm:ListCertificates"
],
"Resource": "*"
},
{
"Sid": "",
"Effect": "Allow",
"Action": [
"sns:Publish",
"route53:GetChange",
"route53:ChangeResourceRecordSets",
"acm:ImportCertificate",
"acm:DescribeCertificate"
],
"Resource": [
"arn:aws:sns:::",
"arn:aws:route53:::hostedzone/*",
"arn:aws:route53:::change/*",
"arn:aws:acm:::certificate/*"
]
}
]
}
Now run the command aws iam create-role
to create the role:
$ aws iam create-role --role-name lambda-acme-dns-route53-executor \
--assume-role-policy-document ~/lambda-acme-dns-route53-executor-policy.json
Note: remember policy ARN (Amazon Resource Name) - we will need it in the next steps.
The role is lambda-acme-dns-route53-executor
created, now we need to specify permissions for it. The easiest way to do this is to use the command aws iam attach-role-policy
by passing the policy ARN AWSLambdaBasicExecutionRole
as follows:
$ aws iam attach-role-policy --role-name lambda-acme-dns-route53-executor \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Note: a list with other policies can be found here .
Creating a lambda function that runs acme-dns-route53
Hurrah! Now you can deploy our function on AWS using the command aws lambda create-function
. The lambda must be configured using the following environment variables:
AWS_LAMBDA
- makes acme-dns-route53 understand that execution occurs inside AWS Lambda.DOMAINS
- A list of domains separated by commas.LETSENCRYPT_EMAIL
- Contains Let's Encrypt Email .NOTIFICATION_TOPIC
- SNS Notification Topic name (optional).STAGING
- if the value is1
used, the staging environment.RENEW_BEFORE
- the number of days that determine the period before the expiration of the period during which the certificate must be renewed.1024
MB - memory limit, subject to change.900
secs (15 min) - timeout.acme-dns-route53
- the name of our binary, which is in the archive.fileb://~/acme-dns-route53.zip
- the path to the archive that we created.
Now deploy:
$ aws lambda create-function \
--function-name acme-dns-route53 \
--runtime go1.x \
--role arn:aws:iam:::role/lambda-acme-dns-route53-executor \
--environment Variables="{AWS_LAMBDA=1,DOMAINS=\"example1.com,example2.com\",LETSENCRYPT_EMAIL=begmaroman@gmail.com,STAGING=0,NOTIFICATION_TOPIC=acme-dns-route53-obtained,RENEW_BEFORE=7}" \
--memory-size 1024 \
--timeout 900 \
--handler acme-dns-route53 \
--zip-file fileb://~/acme-dns-route53.zip
{
"FunctionName": "acme-dns-route53",
"LastModified": "2019-05-03T19:07:09.325+0000",
"RevisionId": "e3fadec9-2180-4bff-bb9a-999b1b71a558",
"MemorySize": 1024,
"Environment": {
"Variables": {
"DOMAINS": "example1.com,example2.com",
"STAGING": "1",
"LETSENCRYPT_EMAIL": "your@email.com",
"NOTIFICATION_TOPIC": "acme-dns-route53-obtained",
"RENEW_BEFORE": "7",
"AWS_LAMBDA": "1"
}
},
"Version": "$LATEST",
"Role": "arn:aws:iam:::role/lambda-acme-dns-route53-executor",
"Timeout": 900,
"Runtime": "go1.x",
"TracingConfig": {
"Mode": "PassThrough"
},
"CodeSha256": "+2KgE5mh5LGaOsni36pdmPP9O35wgZ6TbddspyaIXXw=",
"Description": "",
"CodeSize": 8456317,
"FunctionArn": "arn:aws:lambda:us-east-1::function:acme-dns-route53",
"Handler": "acme-dns-route53"
}
Creating a CloudWatch timer that triggers a function 2 times a day
The last step is to set up the crown, which calls our function twice a day:
- create a CloudWatch rule with a value
schedule_expression
. - create the goal of the rule (what should be done) by specifying the ARN of the lambda function.
- give permission to the rule calling the lambda function.
Below I attached my Terraform config, but in fact it is done very simply using the AWS console or AWS CLI.
# Cloudwatch event rule that runs acme-dns-route53 lambda every 12 hours
resource "aws_cloudwatch_event_rule" "acme_dns_route53_sheduler" {
name = "acme-dns-route53-issuer-scheduler"
schedule_expression = "cron(0 */12 * * ? *)"
}
# Specify the lambda function to run
resource "aws_cloudwatch_event_target" "acme_dns_route53_sheduler_target" {
rule = "${aws_cloudwatch_event_rule.acme_dns_route53_sheduler.name}"
arn = "${aws_lambda_function.acme_dns_route53.arn}"
}
# Give CloudWatch permission to invoke the function
resource "aws_lambda_permission" "permission" {
action = "lambda:InvokeFunction"
function_name = "${aws_lambda_function.acme_dns_route53.function_name}"
principal = "events.amazonaws.com"
source_arn = "${aws_cloudwatch_event_rule.acme_dns_route53_sheduler.arn}"
}
Now you are configured to automatically create and renew SSL certificates