Skip to content

Instantly share code, notes, and snippets.

@crashdump
Last active January 9, 2023 09:45
Show Gist options
  • Star 48 You must be signed in to star a gist
  • Fork 18 You must be signed in to fork a gist
  • Save crashdump/5683952 to your computer and use it in GitHub Desktop.
Save crashdump/5683952 to your computer and use it in GitHub Desktop.
Report how many days before and http ssl certificate expire. I've also provided a template if you want to use it with Zabbix as an External Check: - Configure ExternalScripts variable in zabbix_server.conf - Put the script in the external script folder (I've used /etc/zabbix/externalscripts/) - Import the template & assign it to your host. - Wat…
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = "Adrien Pujol - http://www.crashdump.fr/"
__copyright__ = "Copyright 2013, Adrien Pujol"
__license__ = "Mozilla Public License"
__version__ = "0.3"
__email__ = "adrien.pujol@crashdump.fr"
__status__ = "Development"
__doc__ = "Check a TLS certificate validity."
import argparse
import socket
from datetime import datetime
import time
try:
# Try to load pyOpenSSL first
# aptitude install python-dev && pip install pyopenssl
from OpenSSL import SSL
PYOPENSSL = True
except ImportError:
# Else, fallback on standard ssl lib (doesn't support SNI)
import ssl
PYOPENSSL = False
CA_CERTS = "/etc/ssl/certs/ca-certificates.crt"
def exit_error(errcode, errtext):
print errtext
exit(errcode)
def pyopenssl_check_callback(connection, x509, errnum, errdepth, ok):
''' callback for pyopenssl ssl check'''
if x509.get_subject().commonName == HOST:
if x509.has_expired():
exit_error(1, 'Error: Certificate has expired!')
else:
print pyopenssl_check_expiration(x509.get_notAfter())
if not ok:
return False
return ok
def pyopenssl_check_expiration(asn1):
''' Return the numbers of day before expiration. False if expired.'''
try:
expire_date = datetime.strptime(asn1, "%Y%m%d%H%M%SZ")
except:
exit_error(1, 'Certificate date format unknow.')
expire_in = expire_date - datetime.now()
if expire_in.days > 0:
return expire_in.days
else:
return False
def pyssl_check_hostname(cert, hostname):
''' Return True if valid. False is invalid '''
if 'subjectAltName' in cert:
for typ, val in cert['subjectAltName']:
# Wilcard
if typ == 'DNS' and val.startswith('*'):
if val[2:] == hostname.split('.', 1)[1]:
return True
# Normal hostnames
elif typ == 'DNS' and val == hostname:
return True
else:
return False
def pyssl_check_expiration(cert):
''' Return the numbers of day before expiration. False if expired. '''
if 'notAfter' in cert:
try:
expire_date = datetime.strptime(cert['notAfter'],
"%b %d %H:%M:%S %Y %Z")
except:
exit_error(1, 'Certificate date format unknow.')
expire_in = expire_date - datetime.now()
if expire_in.days > 0:
return expire_in.days
else:
return False
def main():
parser = argparse.ArgumentParser()
parser.add_argument('host', help='specify an host to connect to')
parser.add_argument('-p', '--port', help='specify a port to connect to',
type=int, default=443)
args = parser.parse_args()
global HOST, PORT
HOST = args.host
PORT = args.port
# Check the DNS name
try:
socket.getaddrinfo(HOST, PORT)[0][4][0]
except socket.gaierror as e:
exit_error(1, e)
# Connect to the host and get the certificate
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
# If handled by python SSL library
if not PYOPENSSL:
try:
ssl_sock = ssl.wrap_socket(sock, cert_reqs=ssl.CERT_REQUIRED,
ca_certs=CA_CERTS,
ciphers=("HIGH:-aNULL:-eNULL:"
"-PSK:RC4-SHA:RC4-MD5"))
cert = ssl_sock.getpeercert()
if not pyssl_check_hostname(cert, HOST):
print 'Error: Hostname does not match!'
print pyssl_check_expiration(cert)
sock = ssl_sock.unwrap()
except ssl.SSLError as e:
exit_error(1, e)
# If handled by pyOpenSSL module
else:
try:
ctx = SSL.Context(SSL.TLSv1_METHOD)
ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
pyopenssl_check_callback)
ctx.load_verify_locations(CA_CERTS)
ssl_sock = SSL.Connection(ctx, sock)
ssl_sock.set_connect_state()
ssl_sock.set_tlsext_host_name(HOST)
ssl_sock.do_handshake()
x509 = ssl_sock.get_peer_certificate()
x509name = x509.get_subject()
if x509name.commonName != HOST:
print 'Error: Hostname does not match!'
ssl_sock.shutdown()
except SSL.Error as e:
exit_error(1, e)
sock.close()
if __name__ == "__main__":
main()
<?xml version="1.0" encoding="UTF-8"?>
<zabbix_export>
<version>2.0</version>
<date>2013-11-15T12:28:00Z</date>
<groups>
<group>
<name>Templates</name>
</group>
</groups>
<templates>
<template>
<template>Template External Check - SSL Cert Expire</template>
<name>Template External Check - SSL Cert Expire</name>
<groups>
<group>
<name>Templates</name>
</group>
</groups>
<applications>
<application>
<name>SSL certificate</name>
</application>
</applications>
<items>
<item>
<name>SSL certificate validity</name>
<type>10</type>
<snmp_community/>
<multiplier>0</multiplier>
<snmp_oid/>
<key>check-ssl-expire.py[&quot;{HOST.NAME}&quot;]</key>
<delay>86400</delay>
<history>14</history>
<trends>365</trends>
<status>0</status>
<value_type>0</value_type>
<allowed_hosts/>
<units>days</units>
<delta>0</delta>
<snmpv3_contextname/>
<snmpv3_securityname/>
<snmpv3_securitylevel>0</snmpv3_securitylevel>
<snmpv3_authprotocol>0</snmpv3_authprotocol>
<snmpv3_authpassphrase/>
<snmpv3_privprotocol>0</snmpv3_privprotocol>
<snmpv3_privpassphrase/>
<formula>1</formula>
<delay_flex/>
<params/>
<ipmi_sensor/>
<data_type>0</data_type>
<authtype>0</authtype>
<username/>
<password/>
<publickey/>
<privatekey/>
<port/>
<description/>
<inventory_link>0</inventory_link>
<applications>
<application>
<name>SSL certificate</name>
</application>
</applications>
<valuemap/>
</item>
</items>
<discovery_rules/>
<macros>
<macro>
<macro>{$SSL_PORT}</macro>
<value>443</value>
</macro>
</macros>
<templates/>
<screens/>
</template>
</templates>
<triggers>
<trigger>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].last(0)}&lt;1</expression>
<name>SSL certificate on {HOSTNAME} expired</name>
<url/>
<status>0</status>
<priority>5</priority>
<description/>
<type>0</type>
<dependencies/>
</trigger>
<trigger>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].last(0)}&lt;7</expression>
<name>SSL certificate on {HOSTNAME} expires in less than 7 days ({ITEM.VALUE} days remaining)</name>
<url/>
<status>0</status>
<priority>4</priority>
<description/>
<type>0</type>
<dependencies>
<dependency>
<name>SSL certificate on {HOSTNAME} expired</name>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].last(0)}&lt;1</expression>
</dependency>
</dependencies>
</trigger>
<trigger>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].last(0)}&lt;15</expression>
<name>SSL certificate on {HOSTNAME} expires in less than 15 days ({ITEM.VALUE} days remaining)</name>
<url/>
<status>0</status>
<priority>3</priority>
<description/>
<type>0</type>
<dependencies>
<dependency>
<name>SSL certificate on {HOSTNAME} expires in less than 7 days ({ITEM.VALUE} days remaining)</name>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].last(0)}&lt;7</expression>
</dependency>
</dependencies>
</trigger>
<trigger>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].last(0)}&lt;30</expression>
<name>SSL certificate on {HOSTNAME} expires in less than 30 days ({ITEM.VALUE} days remaining)</name>
<url/>
<status>0</status>
<priority>2</priority>
<description/>
<type>0</type>
<dependencies>
<dependency>
<name>SSL certificate on {HOSTNAME} expires in less than 15 days ({ITEM.VALUE} days remaining)</name>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].last(0)}&lt;15</expression>
</dependency>
</dependencies>
</trigger>
<trigger>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].last(0)}&lt;60</expression>
<name>SSL certificate on {HOSTNAME} expires in less than 60 days ({ITEM.VALUE} days remaining)</name>
<url/>
<status>0</status>
<priority>1</priority>
<description/>
<type>0</type>
<dependencies>
<dependency>
<name>SSL certificate on {HOSTNAME} expires in less than 30 days ({ITEM.VALUE} days remaining)</name>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].last(0)}&lt;30</expression>
</dependency>
</dependencies>
</trigger>
<trigger>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].last(0)}&lt;90</expression>
<name>SSL certificate on {HOSTNAME} expires in less than 90 days ({ITEM.VALUE} days remaining)</name>
<url/>
<status>0</status>
<priority>0</priority>
<description/>
<type>0</type>
<dependencies>
<dependency>
<name>SSL certificate on {HOSTNAME} expires in less than 60 days ({ITEM.VALUE} days remaining)</name>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].last(0)}&lt;60</expression>
</dependency>
</dependencies>
</trigger>
<trigger>
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py[&quot;{HOST.NAME}&quot;].nodata(86500)}=1</expression>
<name>SSL certificate on {HOSTNAME} No new data in the last 24h</name>
<url/>
<status>0</status>
<priority>2</priority>
<description>No data received for &gt; 24h. Check the script.</description>
<type>0</type>
<dependencies/>
</trigger>
</triggers>
</zabbix_export>
@christianchristensen
Copy link

@tuwid
Copy link

tuwid commented Nov 17, 2014

AttributeError: '_socketobject' object has no attribute 'set_tlsext_host_name'

That happend because Ubuntu 12.04 (that is my server's OS) has old pyOpenSSL library which not accept attribute 'set_tlsext_host_name'.
For fix that, you need to add dependence pyOpenSSL >= 0.13.
On Ubuntu for update pyOpenSSL use pip, you also need to install libffi-dev and remove python-openssl by apt.
Add that to instruction.

@anthonysr
Copy link

FYI This also does not handle wildcard certs. x509name.commonName won't match HOST.

@vchoi
Copy link

vchoi commented Jun 16, 2015

Hey @crashdump, congrats on your work.

I've added a version check on my fork. https://gist.github.com/vchoi/f374b3d4fceb1b975b98

I couldn't find any way to make a pull request. Have you considered moving this gist into a github project?

@luponata
Copy link

luponata commented Sep 2, 2016

Wildcards check still not work!

@oclausen
Copy link

Hi @crashdump, thank you for providing this python script. We also have problems regarding wildcard certs. They always result in an error, even if they're valid. Do you have a version where the wildcard certs work?

@jbsmith86
Copy link

I'll try to modify this to get the wildcard working, will edit this post if I can figure it out.

@jbsmith86
Copy link

Well, I tried to wrestle with pyOpenSSL library all day and didn't have much luck. The good news is though I was able to rewrite the whole thing in Ruby and add Wildcard SSL and SNI support and did away with the hard coded CA cert path (ruby is able to detect it on its own and I added an option if you want to pass a filepath in). Let me know what you think: https://gist.github.com/jbsmith86/df7d9aad39e5acf23f2caf73d17b1796

@colttt
Copy link

colttt commented May 15, 2017

Hello i got the following error:

./ssl-exp.py <SERVER_FQDN>
1137
Traceback (most recent call last):
  File "./ssl-exp.py", line 158, in <module>
    main()
  File "./ssl-exp.py", line 126, in main
    sock = ssl_sock.unwrap()
  File "/usr/lib/python2.7/ssl.py", line 823, in unwrap
    s = self._sslobj.shutdown()
socket.error: [Errno 0] Error

@luigicarbone
Copy link

Hello i got the following error:
./ssl-exp.py <SERVER_FQDN>
1137
Traceback (most recent call last):
File "./ssl-exp.py", line 158, in
main()
File "./ssl-exp.py", line 126, in main
sock = ssl_sock.unwrap()
File "/usr/lib/python2.7/ssl.py", line 823, in unwrap
s = self._sslobj.shutdown()
socket.error: [Errno 0] Error

Try to install "python-pip" and you'll see that it works!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment