Get Alerts for Expiring SSL Certificates

Not Only Are Expired SSL Certificates a Security Risk, They Damage Customer Trust

Posted by Ryan S. Brown on Sat, Jan 16, 2016
In Mini-Project
Tags: lambda, ssl, security, alerting

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?

  1. List CloudFront distributions
  2. Check the SSL certificate if it’s using a custom cert
  3. 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: Launch stack TestStack .

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.


Tweet this, send to Hackernews, or post on Reddit