Replacing SSL certificates is a task that happens just infrequently enough to forget it needs to be done, but often enough for you to feel like a total moron when it leads to downtime. Since it’s infrequent, most monitoring services don’t have alerts when your site has an SSL certificate that’s nearing expiration.
Want to know who else missed updating their certs? Check Twitter
In this project we’ll make a Lambda function that can send you alerts over SNS to email, SMS, or Slack so you don’t get blindsided. It’ll check all the CloudFront distributions that use custom certs, and send you a two-week notice.
The Specifications
So what does this function need to do?
- List CloudFront distributions
- Check the SSL certificate if it’s using a custom cert
- Send out notifications if the cert is expiring soon
So from that, we can infer what IAM permissions our function needs to do its
job. All we need is cloudfront:ListDistributions
to get the information about
the distributions, and sns:Publish
to send out alerts.
IAM Policy
In the AWS console, create a new Lambda service role named lambda-ssl-checker
with the policy below.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"cloudfront:ListDistributions",
"sns:Publish"
],
"Resource": [
"*"
]
}
]
}
This should give us everything we need to get the SSL configurations for your account’s CloudFront distributions and fire an SNS notification.
Getting SSL Expiration Date
In Python, it’s pretty simple to open up a secure connection to any server and grab the certificate information. The core of our function will look like this:
import socket
import ssl
def ssl_expiry_datetime(hostname):
ssl_date_fmt = r'%b %d %H:%M:%S %Y %Z'
context = ssl.create_default_context()
conn = context.wrap_socket(
socket.socket(socket.AF_INET),
server_hostname=hostname,
)
# 3 second timeout because Lambda has runtime limitations
conn.settimeout(3.0)
conn.connect((hostname, 443))
ssl_info = conn.getpeercert()
# parse the string from the certificate into a Python datetime object
return datetime.datetime.strptime(ssl_info['notAfter'], ssl_date_fmt)
The code above doesn’t make any actual requests once the socket is open, so the request should only do the SSL handshake, then destroy the connection at the end of the function. Now that we’ve got the datetime object, we can compare the expiration date to the current one, and figure out if the cert expired yet.
def ssl_valid_time_remaining(hostname):
"""Get the number of days left in a cert's lifetime."""
expires = ssl_expiry_datetime(hostname)
logger.debug(
"SSL cert for %s expires at %s",
hostname, expires.isoformat()
)
return expires - datetime.datetime.utcnow()
def ssl_expires_in(hostname, buffer_days=14):
"""Check if `hostname` SSL cert expires is within `buffer_days`.
Raises `AlreadyExpired` if the cert is past due
"""
remaining = ssl_valid_time_remaining(hostname)
# if the cert expires in less than two weeks, we should reissue it
if remaining < datetime.timedelta(days=0):
# cert has already expired - uhoh!
raise AlreadyExpired("Cert expired %s days ago" % remaining.days)
elif remaining < datetime.timedelta(days=buffer_days):
# expires sooner than the buffer
return True
else:
# everything is fine
return False
This code provides the logic we need to take the expiration date and convert it into one of:
- Yes, the cert is all good and won’t be expiring anytime soon.
- Danger! The cert is valid, but will expire soon.
- Emergency! Your cert already expired, and needs to be replaced ASAP.
Getting CloudFront Distributions
The ListDistributions
API call gives us everything we need. The DNS aliases
of the distributions, what type of SSL certificate (if any) they use. According
to the CloudFront docs on the ViewerCertificate
element, there is
a property CloudFrontDefaultCertificate
that indicates whether the
distribution uses a custom certificate. This makes it trivial to test if we
should worry about SSL expiration for a given distribution. The code to check
looks like this:
import boto3
cloudfront = boto3.client('cloudfront')
for dist in cloudfront.list_distributions()['DistributionList']['Items']:
if dist.get('ViewerCertificate', {}).get('CloudFrontDefaultCertificate', False):
# this distribution uses default cloudfront SSL
# so we aren't in charge of renewing it
continue
# handle custom SSL for the domains in dist['Aliases']
Once we have the distribution and know it’s using a custom certificate, the
Aliases
tell us what domain names to check to grab the SSL cert.
domain = dist['Aliases']['Items'][0]
result = check_domain(domain, event.get('buffer_days', 14))
if result['cert_status'] != 'OK':
# notify the user
sns = boto3.client('sns')
sns.publish(
TopicArn=topic,
Message=json.dumps(result)
)
Putting It Together
Now we’ve seen how all the independent parts work to get the certificates, parse their validity dates, and sendj notifications, over SNS. Now we’ve got to put all that together into working code that we can actually deploy. If you’ve seen my posts on Deploying Lambda Functions or Hugo Shortcodes you’ll know it’s easy to build one-button deploys with CloudFormation. To add SSL expiration checks to your infrastructure, launch this stack: .
When the stack is finished, you’ll need to add a CloudWatch event. CloudWatch just added support for scheduled events that can be connected to Lambda functions. There’s support for creating them in the AWS CLI, and for our function we need two commands. One to create the event schedule, and one to connect it to our Lambda function.
$ aws events put-rule --schedule-expression 'cron(0 12 ? * MON *)' \
--state ENABLED \
--description 'Check SSL certificates on Mondays' \
--name ssl-expiration-check
$ aws events put-targets --rule ssl-expiration-check \
--targets 'Arn=[LAMBDA ARN GOES HERE],Input="{\"topic\":\"[SNS ARN GOES HERE]\"}",InputPath="",Id=Id123456789'
$ aws lambda add-permission \
--function-name [LAMBDA FUNCTION NAME GOES HERE] \
--statement-id MyId \
--action 'lambda:InvokeFunction' \
--principal events.amazonaws.com \
--source-arn arn:aws:events:us-east-1:[AWS ACCOUNT NUMBER GOES HERE]:rule/ssl-expiration-check
To get the Lambda and SNS ARNs, go to the “Outputs” tab of the
CloudFormation stack. If you have the AWS CLI installed already, copy the
AddScheduleCommand1
and AddScheduleCommand2
commands to your terminal and
run them.
If you don’t have the AWS CLI, follow these instructions to use the AWS Console to do the same thing.
Wrapping Up
The only thing that’s left is to subscribe to the SNS topic by email, SMS, or whatever method will reach you best. In the SNS console, you can add your email address and you’ll be set; just click the link in the confirmation email it sends you.
Keep up with future posts via RSS. If you have suggestions, questions, or comments feel free to email me ryan@serverlesscode.com or tweet me @ryan_sb.