Get Alerts for Expiring SSL Certificates
Get Alerts for Expiring SSL Certificates -- Serverless Code

        
                  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.

                  
                  
                  Tweet this,
                  send to Hackernews, or
                  post on Reddit
                  
                
    