2019-10-14 14:34:34 +02:00
#openssl stuff
import OpenSSL
import socket , ssl
#date stuff
import dateutil . parser as date_parser
from pytz import utc as UTC
from datetime import datetime
#collectd stuff
2019-10-15 07:59:23 +02:00
import collectd
2019-10-14 14:34:34 +02:00
import pdb
DOMAIN = " geekify.de "
address_family = " ipv4 "
TIMEOUT_SECONDS = 1
2019-10-15 07:59:23 +02:00
plugin_config = { }
2019-10-14 14:34:34 +02:00
def configure ( config ) :
2019-10-15 07:59:23 +02:00
machines = { }
for config_child in config . children :
key = config_child . key . lower ( )
values = config_child . values
# currently only "host" entries are supported
if config_child . key . lower ( ) == " host " :
# host can contain several hosts at once, even the ones containing additional config
for machine_name in values :
machine_details = { }
# host entry contains advanced/additional config options
for detail in config_child . children :
key_name = detail . key . lower ( )
machine_detail_values = [ ]
# TODO: all the difference here is casting value into an int or not. This can maybe be improved or reduced somehow?
if key_name in ( " addressfamily " , ) :
for value in detail . values :
machine_detail_values . append ( int ( value ) )
elif key_name in ( " sninames " , ) :
machine_detail_values = [ ]
for value in detail . values :
machine_detail_values . append ( value )
else :
# additional keys are just ignored
pass
machine_details . update ( { key_name : machine_detail_values } )
machines . update ( { machine_name : machine_details } )
plugin_config . update ( { " machines " : machines } )
def check_hosts ( ) :
for host , host_config in plugin_config . get ( " machines " , { } ) . items ( ) :
for address_family in host_config . get ( " addressfamily " , [ 4 , 6 ] ) :
for sni_name in host_config . get ( " sninames " , [ host ] ) :
try :
cert_details = get_tls_metrics ( host , sni_name , address_family )
for key in cert_details . keys ( ) :
val = collectd . Values ( host = " " , plugin = " pyopenssl " , plugin_instance = " host_ {} -sni_ {} " . format ( host , sni_name ) , type_instance = " ipv {} - {} " . format ( address_family , key ) )
cert_detail = cert_details . get ( key )
if key in ( " valid " , " expired " ) :
val . type = " gauge "
val . dispatch ( values = [ 1 if cert_detail else 0 ] )
elif key in ( " notAfter " , " notBefore " ) :
val . type = " gauge "
val . dispatch ( values = [ cert_detail . timestamp ( ) ] )
elif key in ( " remaining " , " active_since " ) :
val . type = " gauge "
val . dispatch ( values = [ int ( cert_detail . total_seconds ( ) ) ] )
elif key in ( " issuer " , " digests " ) :
# currently there is no way to store static strings with collectd
pass
except Exception as e :
print ( " Failed to check host {} with SNI {} over IPv {} : " . format ( host , sni_name , address_family ) , end = " " )
print ( e )
pass
2019-10-14 14:34:34 +02:00
2019-10-15 07:59:23 +02:00
def get_tls_metrics ( host , sni_name , address_family ) :
cert_details = { " valid " : True }
today = datetime . now ( UTC )
port = 443
if address_family == 4 :
af = socket . AF_INET
elif address_family == 6 :
af = socket . AF_INET6
else :
proto = None
2019-10-14 14:34:34 +02:00
2019-10-15 07:59:23 +02:00
addrinfo = socket . getaddrinfo ( host , None , af , proto = socket . IPPROTO_TCP )
2019-10-14 14:34:34 +02:00
# addrinfo first contains an array of connections made, we want the first ([0]) and only one we made
# this tuple then contains (family, type, proto, canonname, sockaddr) - we want sockaddr == [4]
# this then can contain either (host, port) or (host, port, flowinfo, scopeid) depending on set address family. Since host is anyways at [0] we take this as final ip address
host = addrinfo [ 0 ] [ 4 ] [ 0 ]
2019-10-15 07:59:23 +02:00
context = ssl . create_default_context ( )
while True :
try :
with socket . create_connection ( ( host , port ) , TIMEOUT_SECONDS ) as sock :
with context . wrap_socket ( sock , server_hostname = sni_name ) as sslsock :
der_cert = sslsock . getpeercert ( True )
pem_cert = ssl . DER_cert_to_PEM_cert ( der_cert )
break
except ssl . CertificateError as error :
if context . verify_mode == ssl . CERT_REQUIRED and context . check_hostname :
cert_details [ " valid " ] = False
context . check_hostname = False
context . verify_mode = ssl . CERT_NONE
else :
raise error
x509 = OpenSSL . crypto . load_certificate ( OpenSSL . crypto . FILETYPE_ASN1 , der_cert )
notAfter = x509 . get_notAfter ( ) . decode ( " utf-8 " )
notAfter = date_parser . parse ( notAfter )
notBefore = x509 . get_notBefore ( ) . decode ( " utf-8 " )
notBefore = date_parser . parse ( notBefore )
remaining = notAfter - today
active_since = today - notBefore
issuer_components = x509 . get_issuer ( ) . get_components ( )
issuer_components_decoded = list ( map ( lambda bin_component : list ( map ( lambda keyvalue : keyvalue . decode ( " utf-8 " ) , bin_component ) ) , issuer_components ) )
joined_components = list ( map ( lambda x : " = " . join ( x ) , issuer_components_decoded ) )
issuer = " , " . join ( joined_components )
digests = { }
for digest_type in ( " md5 " , " sha1 " , " sha256 " ) :
digests . update ( { digest_type : x509 . digest ( digest_type ) . decode ( " utf-8 " ) } )
cert_details . update ( {
" expired " : x509 . has_expired ( ) ,
" notAfter " : notAfter ,
" notBefore " : notBefore ,
" remaining " : remaining ,
" active_since " : active_since ,
" issuer " : issuer ,
" digests " : digests ,
} )
return cert_details
collectd . register_config ( configure )
collectd . register_read ( check_hosts )