#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 import collectd import pdb DOMAIN = "geekify.de" address_family = "ipv4" TIMEOUT_SECONDS = 1 plugin_config = {} def configure(config): 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 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 addrinfo = socket.getaddrinfo(host, None, af, proto=socket.IPPROTO_TCP) # 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] 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)