From 0366c1c5e4528309dcb8c858e8ed3c34988830eb Mon Sep 17 00:00:00 2001 From: sqozz Date: Tue, 15 Oct 2019 07:59:23 +0200 Subject: [PATCH] Add collectd support --- collectd-pyopenssl.py | 140 +++++++++++++++++++++++++++++++++--------- 1 file changed, 112 insertions(+), 28 deletions(-) diff --git a/collectd-pyopenssl.py b/collectd-pyopenssl.py index de5dd03..e6d7c10 100755 --- a/collectd-pyopenssl.py +++ b/collectd-pyopenssl.py @@ -8,9 +8,7 @@ from pytz import utc as UTC from datetime import datetime #collectd stuff -#import collectd - -import ipaddress +import collectd import pdb @@ -18,39 +16,125 @@ DOMAIN = "geekify.de" address_family = "ipv4" TIMEOUT_SECONDS = 1 +plugin_config = {} + def configure(config): - for c in config.children: - print(c) + 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}) -today = datetime.now(UTC) -port = 443 -if address_family == "ipv4": - af = socket.AF_INET -elif address_family == "ipv6": - af = socket.AF_INET6 -else: - proto = None +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 -try: - addrinfo = socket.getaddrinfo(DOMAIN, None, af, proto=socket.IPPROTO_TCP) +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] -except socket.gaierror: - host = DOMAIN -context = ssl.create_default_context() -with socket.create_connection((host, port), TIMEOUT_SECONDS) as sock: - with context.wrap_socket(sock, server_hostname=DOMAIN) as sslsock: - der_cert = sslsock.getpeercert(True) - pem_cert = ssl.DER_cert_to_PEM_cert(der_cert) + 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) -exp = x509.get_notAfter().decode("utf-8") -expirary_date = date_parser.parse(exp) -delta = expirary_date - today -print(delta.total_seconds()) + 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 -pdb.set_trace() + 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)