By John Hanley, Alibaba Cloud Tech Share Author. Tech Share is Alibaba Cloud’s incentive program to encourage the sharing of technical knowledge and best practices within the cloud community.
After working with Alibaba Cloud services, you may find that you have dozens or hundreds of services that depend on SSL certificates. This can include Elastic Compute Service (ECS) instances, websites, API Gateway services, Function Compute functions, and CDN endpoints. This article will discuss how to monitor SSL certificates and send emails on the status of your SSL certificates. We will use Function Compute and DirectMail for automation, monitoring and reporting.
This article is an add-on to my series of using Let's Encrypt SSL Certificates on Alibaba Cloud. These certificates expire after 90 days, therefore, keeping track of certificate expiration dates is very important. However, tracking services that use SSL certificates is mundane, tedious and we often forget about them. In another article, we will develop software that can automatically renew Let's Encrypt SSL certificates.
The goal of this article is to show you how to do this. The code is not production quality, rather it is education quality. All software that you plan to deploy for production purposes needs to be reviewed and tested for quality and suitability for your requirements.
This article assumes that you have a basic understanding of Alibaba Cloud Function Compute and DirectMail. If not, I have written this article to help you understand these services. The last part of this tutorial shows how to speed up testing and updates using the Alibaba Cloud FCLI command line program.
NeoPrime SSL Certificate Status Report
Sat, 23 Jun 2018 18:24:41 GMT
The key columns are the "Status" and "Expires". As long as the status shows OK, all is good. Otherwise a message will be displayed such as "Expired" and "Time to Renew".
g_program_mode = PROGRAM_MODE_CMDLINE
to g_program_mode = PROGRAM_MODE_ACS_FUNC
PROGRAM_MODE_CMDLINE = 0 # The program operates from the command line
PROGRAM_MODE_ACS_FUNC = 1 # The program operates as a Alibaba Cloud Function Compute function
g_program_mode = PROGRAM_MODE_CMDLINE
g_days_left = 14 # Warn if a certificate will expire in less than this number of days
g_no_send = False # if set, don't actually send an email. This is used for debugging
g_only_send_notices = False # If set, only send emails if a certificate will expire soon or on error
g_email_required = False # This is set during processing if a warning or error was detected
Configure the hostnames that you want to monitor. In this example, we are monitoring four hostnames.
g_hostnames = [
"neoprime.xyz",
"api.neoprime.xyz",
"cdn.neoprime.xyz",
"www.neoprime.xyz",
]
Configure the report subject and send to email address.
email_params['Subject'] = 'NeoPrime SSL Cerificate Status Report'
email_params['To'] = 'someone@example.com'
Configure the Alibaba Cloud DirectMail account parameters. This example uses Singapore for the region.
# From the DirectMail Console
dm_account['Debug'] = 0
dm_account['Account'] = 'sender_address_from_directmail_console'
dm_account['Alias'] = 'my_alias_name_such_as_NeoPrime'
dm_account['host'] = "dm.ap-southeast-1.aliyuncs.com"
dm_account['url'] = "https://dm.ap-southeast-1.aliyuncs.com/"
def ssl_get_cert(hostname):
""" This function returns an SSL certificate from a host """
context = ssl.create_default_context()
conn = context.wrap_socket(
socket.socket(socket.AF_INET),
server_hostname=hostname)
# 3 second timeout because Function Compute has runtime limitations
conn.settimeout(3.0)
try:
conn.connect((hostname, 443))
except Exception as ex:
print("{}: Exception: {}".format(hostname, ex), file=sys.stderr)
return False, str(ex)
host_ssl_info = conn.getpeercert()
return host_ssl_info, ''
def process_hostnames(msg_body, hostnames):
""" Process the SSL certificate for each hostname """
# pylint: disable=global-statement
global g_email_required
ssl_date_fmt = r'%b %d %H:%M:%S %Y %Z'
for host in hostnames:
f_expired = False
print('Processing host:', host)
ssl_info, err = get_ssl_info(host)
if ssl_info is False:
msg_body = add_row(msg_body, host, err, '', '', '', True)
g_email_required = True
continue
#print(ssl_info)
issuerName = get_ssl_issuer_name(ssl_info)
altNames = get_ssl_subject_alt_names(ssl_info)
l_expires = datetime.datetime.strptime(ssl_info['notAfter'], ssl_date_fmt)
remaining = l_expires - datetime.datetime.utcnow()
if remaining < datetime.timedelta(days=0):
# cert has already expired - uhoh!
cert_status = "Expired"
f_expired = True
g_email_required = True
elif remaining < datetime.timedelta(days=g_days_left):
# expires sooner than the buffer
cert_status = "Time to Renew"
f_expired = True
g_email_required = True
else:
# everything is fine
cert_status = "OK"
f_expired = False
msg_body = add_row(msg_body, host, cert_status, str(l_expires), issuerName, altNames, f_expired)
return msg_body
############################################################
# Version 0.90
# Date Created: 2018-06-11
# Last Update: 2018-06-23
# https://www.neoprime.io
# Copyright (c) 2018, NeoPrime, LLC
# Author: John Hanley
############################################################
""" Alibaba Cloud Function Compute Example """
import sys
import datetime
import socket
import json
import ssl
import time
import myemail
import myhtml
PROGRAM_MODE_CMDLINE = 0 # The program operates from the command line
PROGRAM_MODE_ACS_FUNC = 1 # The program operates as a Alibaba Cloud Function Compute function
g_program_mode = PROGRAM_MODE_ACS_FUNC
#g_program_mode = PROGRAM_MODE_CMDLINE
g_days_left = 14 # Warn if a certificate will expire in less than this number of days
g_no_send = False # if set, don't actually send an email. This is used for debugging
g_only_send_notices = False # If set, only send emails if a certificate will expire soon or on error
g_email_required = False # This is set during processing if a warning or error was detected
g_hostnames = [
"neoprime.xyz",
"api.neoprime.xyz",
"cdn.neoprime.xyz",
"www.neoprime.xyz",
]
email_params = {
'To': '',
'Subject': '',
'Body': '',
'BodyText': ''
}
email_params['Subject'] = 'NeoPrime SSL Certificate Status Report'
email_params['To'] = 'someone@example.com'
dm_account = {
'Debug': 0, # Debug flag
'Account': '', # DirectMail account
'Alias': '', # DirectMail alias
'host': '', # HTTP Host header
'url': '' # URL for POST
}
# From the DirectMail Console
dm_account['Debug'] = 0
dm_account['Account'] = ''
dm_account['Alias'] = ''
dm_account['host'] = "dm.ap-southeast-1.aliyuncs.com"
dm_account['url'] = "https://dm.ap-southeast-1.aliyuncs.com/"
def ssl_get_cert(hostname):
""" This function returns an SSL certificate from a host """
context = ssl.create_default_context()
conn = context.wrap_socket(
socket.socket(socket.AF_INET),
server_hostname=hostname)
# 3 second timeout because Function Compute has runtime limitations
conn.settimeout(3.0)
try:
conn.connect((hostname, 443))
except Exception as ex:
print("{}: Exception: {}".format(hostname, ex), file=sys.stderr)
return False, str(ex)
host_ssl_info = conn.getpeercert()
return host_ssl_info, ''
def add_row(body, domain, status, expires, issuerName, names, flag_hl):
""" Add a row to the HTML table """
#build the url
url = '<a href="https://' + domain + '">' + domain + '</a>'
# begin a new table row
if flag_hl is False:
body += '<tr>\n'
else:
body += '<tr bgcolor="#FFFF00">\n' # yellow
body += '<td>' + url + '</td>\n'
body += '<td>' + status + '</td>\n'
body += '<td>' + expires + '</td>\n'
body += '<td>' + issuerName + '</td>\n'
body += '<td>' + names + '</td>\n'
return body + '</tr>\n'
# Email specific
def send(account, credentials, params):
""" email send function """
# pylint: disable=global-statement
global g_only_send_notices
global g_email_required
# If set, only send emails if a certificate will expire soon or on error
if g_only_send_notices is True:
if g_email_required is False:
print('')
print('All hosts have valid certificates')
print('Sending an email is not required')
return
myemail.sendEmail(credentials, account, params, g_no_send)
def get_ssl_info(host):
""" This function retrieves the SSL certificate for host """
# If we receive an error, retry up to three times waiting 10 seconds each time.
retry = 0
err = ''
while retry < 3:
ssl_info, err = ssl_get_cert(host)
if ssl_info is not False:
return ssl_info, ''
retry += 1
print(' retrying ...')
time.sleep(10)
return False, err
def get_ssl_issuer_name(ssl_info):
""" Return the IssuerName from the SSL certificate """
issuerName = ''
issuer = ssl_info['issuer']
# pylint: disable=line-too-long
# issuer looks like this:
# This is a set of a set of a set of key / value pairs.
# ((('countryName', 'US'),), (('organizationName', "Let's Encrypt"),), (('commonName', "Let's Encrypt Authority X3"),))
for item in issuer:
# item will look like this as it goes thru the issuer set
# Note that this is a set of a set
#
# (('countryName', 'US'),)
# (('organizationName', "Let's Encrypt"),)
# (('commonName', "Let's Encrypt Authority X3"),)
s = item[0]
# s will look like this as it goes thru the isser set
# Note that this is now a set
#
# ('countryName', 'US')
# ('organizationName', "Let's Encrypt")
# ('commonName', "Let's Encrypt Authority X3")
# break the set into "key" and "value" pairs
k = s[0]
v = s[1]
if k == 'organizationName':
if v != '':
issuerName = v
continue
if k == 'commonName':
if v != '':
issuerName = v
return issuerName
def get_ssl_subject_alt_names(ssl_info):
""" Return the Subject Alt Names """
altNames = ''
subjectAltNames = ssl_info['subjectAltName']
index = 0
for item in subjectAltNames:
altNames += item[1]
index += 1
if index < len(subjectAltNames):
altNames += ', '
return altNames
def process_hostnames(msg_body, hostnames):
""" Process the SSL certificate for each hostname """
# pylint: disable=global-statement
global g_email_required
ssl_date_fmt = r'%b %d %H:%M:%S %Y %Z'
for host in hostnames:
f_expired = False
print('Processing host:', host)
ssl_info, err = get_ssl_info(host)
if ssl_info is False:
msg_body = add_row(msg_body, host, err, '', '', '', True)
g_email_required = True
continue
#print(ssl_info)
issuerName = get_ssl_issuer_name(ssl_info)
altNames = get_ssl_subject_alt_names(ssl_info)
l_expires = datetime.datetime.strptime(ssl_info['notAfter'], ssl_date_fmt)
remaining = l_expires - datetime.datetime.utcnow()
if remaining < datetime.timedelta(days=0):
# cert has already expired - uhoh!
cert_status = "Expired"
f_expired = True
g_email_required = True
elif remaining < datetime.timedelta(days=g_days_left):
# expires sooner than the buffer
cert_status = "Time to Renew"
f_expired = True
g_email_required = True
else:
# everything is fine
cert_status = "OK"
f_expired = False
msg_body = add_row(msg_body, host, cert_status, str(l_expires), issuerName, altNames, f_expired)
return msg_body
def main_cmdline():
""" This is the main function """
# My library for processing Alibaba Cloud Services (ACS) credentials
# This library is only used when running from the desktop and not from the cloud
import mycred_acs
# Load the Alibaba Cloud Credentials (AccessKey)
cred = mycred_acs.LoadCredentials()
if cred is False:
print('Error: Cannot load credentials', file=sys.stderr)
sys.exit(1)
now = datetime.datetime.utcnow()
date = now.strftime("%a, %d %b %Y %H:%M:%S GMT")
msg_body = ''
msg_body = myhtml.build_body_top()
msg_body += '<h4>NeoPrime SSL Cerificate Status Report</h4>'
msg_body += date + '<br />'
msg_body += '<br />'
msg_body = myhtml.build_table_top(msg_body)
#
# This is where the SSL processing happens
#
msg_body = process_hostnames(msg_body, g_hostnames)
msg_body = myhtml.build_table_bottom(msg_body)
msg_body = myhtml.build_body_bottom(msg_body)
email_params['Body'] = msg_body
email_params['BodyText'] = ''
#print(msg_body)
send(dm_account, cred, email_params)
def main_acs_func(event, context):
""" This is the main function """
cred = {
'accessKeyId': '',
'accessKeySecret': '',
'securityToken': '',
'Region': ''
}
cred['accessKeyId'] = context.credentials.accessKeyId
cred['accessKeySecret'] = context.credentials.accessKeySecret
cred['securityToken'] = context.credentials.securityToken
now = datetime.datetime.utcnow()
date = now.strftime("%a, %d %b %Y %H:%M:%S GMT")
msg_body = ''
msg_body = myhtml.build_body_top()
msg_body += '<h4>NeoPrime SSL Cerificate Status Report</h4>'
msg_body += date + '<br />'
msg_body += '<br />'
msg_body = myhtml.build_table_top(msg_body)
#
# This is where the SSL processing happens
#
msg_body = process_hostnames(msg_body, g_hostnames)
msg_body = myhtml.build_table_bottom(msg_body)
msg_body = myhtml.build_body_bottom(msg_body)
email_params['Body'] = msg_body
email_params['BodyText'] = ''
#print(msg_body)
send(dm_account, cred, email_params)
return msg_body
def handler(event, context):
""" This is the Function Compute entry point """
body = ""
body = main_acs_func(event, context)
res = {
'isBase64Encoded': False,
'statusCode': 200,
'headers': {
'content-type' : 'text/html'
},
'body': body
}
return json.dumps(res)
# Main Program
if g_program_mode == PROGRAM_MODE_CMDLINE:
main_cmdline()
This role is created but no permissions have been granted to the role.
Steps to grant permissions (authorize) to a role:
An important concept with Function Compute and RAM Roles, is that roles are assigned to Function Compute Services. All functions under a service inherit this role. This means that if you have a Function Compute service with several functions, the RAM Role will need the sum of the required permissions for each functions. If you need tighter security, create separate services based upon role permissions.
The RAM Policy will create a JSON document that describes the granted permissions. In this case the role is granting all actions that start with dm (Action: dm:) on all resources (Resource: ).
{
"Version": "1",
"Statement": [
{
"Action": "dm:*",
"Resource": "*",
"Effect": "Allow"
}
]
}
An often overlooked component of assigning a RAM Role to a service is that the service requires permissions to assume that role. A RAM Role has two components, the STS (Security Token Service) permissions to assume a role and the role permissions.
This JSON describes the permissions that the Function Compute service itself has to assume a role via the AssumeRole action. Notice the service name "fc.aliyncs.com" and the Action "sts:AssumeRole".
{
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": [
"fc.aliyuncs.com"
]
}
}
],
"Version": "1"
}
Error Code: 404
Error: {
"Recommend":"https://error-center.aliyun.com/status/search?Keyword=InvalidAccessKeyId.NotFound&source=PopGw",
"Message":"Specified access key is not found.",
"RequestId":"31BEFC34-DD4F-4916-927A-773A7C4F26C5",
"HostId":"dm.ap-southeast-1.aliyuncs.com",
"Code":"InvalidAccessKeyId.NotFound"
}
del index.zip
pkzipc -add index.zip index.py myemail.py myhtml.py
fcli function update --code-file index.zip -s service_name -f function_name
fcli function create --code-file index.zip -t python3 -h index.handler -s ssl -f ssl_check
fcli function invoke -s service_name -f function_name
triggerConfig:
payload: ""
cronExpression: "0 0/60 * * * *"
enable: true
fcli trigger create -t OncePerHour -s ssl -f ssl_check -c TimeTrigger.yaml --type timer
g_only_send_notices = True
to only receive an email if there is a problem. This would be a service check feature that can report to you if any of the HTTPS services are failing.Installing Matomo (Piwik) for Web and Mobile Data Analytics on Alibaba Cloud ECS
2,599 posts | 764 followers
FollowAlibaba Clouder - June 16, 2020
JJ Lim - January 4, 2022
Alibaba Clouder - June 25, 2018
5544031433091282 - August 16, 2022
Alibaba Clouder - June 26, 2018
Hironobu Ohara - June 26, 2023
2,599 posts | 764 followers
FollowAlibaba Cloud Function Compute is a fully-managed event-driven compute service. It allows you to focus on writing and uploading code without the need to manage infrastructure such as servers.
Learn MoreYou can use Certificate Management Service to issue, deploy, and manage public and private SSL/TLS certificates.
Learn MoreRobotic Process Automation (RPA) allows you to automate repetitive tasks and integrate business rules and decisions into processes.
Learn MoreAlibaba Cloud is committed to safeguarding the cloud security for every business.
Learn MoreMore Posts by Alibaba Clouder