From 4273b21105dd11f52de354b4777d33e4296ba7e0 Mon Sep 17 00:00:00 2001 From: Tim Bielawa Date: Thu, 6 Oct 2016 10:01:48 -0700 Subject: Get router/registry certs. Collect common names and subjectAltNames --- library/openshift_cert_expiry.py | 167 ++++++++++++++++++++++++++++++++++----- 1 file changed, 146 insertions(+), 21 deletions(-) (limited to 'library') diff --git a/library/openshift_cert_expiry.py b/library/openshift_cert_expiry.py index 4e66de755..f18ab75d0 100644 --- a/library/openshift_cert_expiry.py +++ b/library/openshift_cert_expiry.py @@ -4,6 +4,8 @@ """For details on this module see DOCUMENTATION (below)""" +# router/registry cert grabbing +import subprocess # etcd config file import ConfigParser # Expiration parsing @@ -15,7 +17,6 @@ import yaml # Certificate loading import OpenSSL.crypto - DOCUMENTATION = ''' --- module: openshift_cert_expiry @@ -126,8 +127,59 @@ A 3-tuple of the form: (certificate_common_name, certificate_expiry_date, certif cert_loaded = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, _cert_string) + ###################################################################### + # Read just the first name from the cert - DISABLED while testing + # out the 'get all possible names' function (below) + # # Strip the subject down to just the value of the first name - cert_subject = cert_loaded.get_subject().get_components()[0][1] + # cert_subject = cert_loaded.get_subject().get_components()[0][1] + + ###################################################################### + # Read all possible names from the cert + cert_subjects = [] + for name, value in cert_loaded.get_subject().get_components(): + cert_subjects.append('{}:{}'.format(name, value)) + + # To read SANs from a cert we must read the subjectAltName + # extension from the X509 Object. What makes this more difficult + # is that pyOpenSSL does not give extensions as a list, nor does + # it provide a count of all loaded extensions. + # + # Rather, extensions are REQUESTED by index. We must iterate over + # all extensions until we find the one called 'subjectAltName'. If + # we don't find that extension we'll eventually request an + # extension at an index where no extension exists (IndexError is + # raised). When that happens we know that the cert has no SANs so + # we break out of the loop. + i = 0 + checked_all_extensions = False + while not checked_all_extensions: + try: + # Read the extension at index 'i' + ext = cert_loaded.get_extension(i) + except IndexError: + # We tried to read an extension but it isn't there, that + # means we ran out of extensions to check. Abort + san = None + checked_all_extensions = True + else: + # We were able to load the extension at index 'i' + if ext.get_short_name() == 'subjectAltName': + san = ext + checked_all_extensions = True + else: + # Try reading the next extension + i += 1 + + if san is not None: + # The X509Extension object for subjectAltName prints as a + # string with the alt names separated by a comma and a + # space. Split the string by ', ' and then add our new names + # to the list of existing names + cert_subjects.extend(str(san).split(', ')) + + cert_subject = ', '.join(cert_subjects) + ###################################################################### # Grab the expiration date cert_expiry = cert_loaded.get_notAfter() @@ -174,7 +226,7 @@ Return: return cert_list -def tabulate_summary(certificates, kubeconfigs, etcd_certs): +def tabulate_summary(certificates, kubeconfigs, etcd_certs, router_certs, registry_certs): """Calculate the summary text for when the module finishes running. This includes counds of each classification and what have you. @@ -190,12 +242,14 @@ Return: - `summary_results` (dict) - Counts of each cert type classification and total items examined. """ - items = certificates + kubeconfigs + etcd_certs + items = certificates + kubeconfigs + etcd_certs + router_certs + registry_certs summary_results = { 'system_certificates': len(certificates), 'kubeconfig_certificates': len(kubeconfigs), 'etcd_certificates': len(etcd_certs), + 'router_certs': len(router_certs), + 'registry_certs': len(registry_certs), 'total': len(items), 'ok': 0, 'warning': 0, @@ -213,7 +267,7 @@ Return: # This is our module MAIN function after all, so there's bound to be a # lot of code bundled up into one block # -# pylint: disable=too-many-locals,too-many-locals,too-many-statements +# pylint: disable=too-many-locals,too-many-locals,too-many-statements,too-many-branches def main(): """This module examines certificates (in various forms) which compose an OpenShift Container Platform cluster @@ -250,21 +304,19 @@ an OpenShift Container Platform cluster openshift_node_config_path, ] - # Paths for Kubeconfigs. Additional kubeconfigs are conditionally checked later in the code - kubeconfig_paths = [ - os.path.normpath( - os.path.join(openshift_base_config_path, "master/admin.kubeconfig") - ), - os.path.normpath( - os.path.join(openshift_base_config_path, "master/openshift-master.kubeconfig") - ), - os.path.normpath( - os.path.join(openshift_base_config_path, "master/openshift-node.kubeconfig") - ), - os.path.normpath( - os.path.join(openshift_base_config_path, "master/openshift-router.kubeconfig") - ), - ] + # Paths for Kubeconfigs. Additional kubeconfigs are conditionally + # checked later in the code + master_kube_configs = ['admin', 'openshift-master', + 'openshift-node', 'openshift-router', + 'openshift-registry'] + + kubeconfig_paths = [] + for m_kube_config in master_kube_configs: + kubeconfig_paths.append( + os.path.normpath( + os.path.join(openshift_base_config_path, "master/%s.kubeconfig" % m_kube_config) + ) + ) # etcd, where do you hide your certs? Used when parsing etcd.conf etcd_cert_params = [ @@ -460,7 +512,80 @@ an OpenShift Container Platform cluster # /Check etcd certs ###################################################################### - res = tabulate_summary(ocp_certs, kubeconfigs, etcd_certs) + ###################################################################### + # Check router/registry certs + # + # These are saved as secrets in etcd. That means that we can not + # simply read a file to grab the data. Instead we're going to + # subprocess out to the 'oc get' command. On non-masters this + # command will fail, that is expected so we catch that exception. + ###################################################################### + router_certs = [] + registry_certs = [] + + ###################################################################### + # First the router certs + try: + router_secrets_raw = subprocess.Popen('oc get secret router-certs -o yaml'.split(), + stdout=subprocess.PIPE) + router_ds = yaml.load(router_secrets_raw.communicate()[0]) + router_c = router_ds['data']['tls.crt'] + router_path = router_ds['metadata']['selfLink'] + except TypeError: + # YAML couldn't load the result, this is not a master + pass + else: + (cert_subject, + cert_expiry_date, + time_remaining) = load_and_handle_cert(router_c, now, base64decode=True) + + expire_check_result = { + 'cert_cn': cert_subject, + 'path': router_path, + 'expiry': cert_expiry_date, + 'days_remaining': time_remaining.days, + 'health': None, + } + + classify_cert(expire_check_result, now, time_remaining, expire_window, router_certs) + + check_results['router'] = router_certs + + ###################################################################### + # Now for registry + # registry_secrets = subprocess.call('oc get secret registry-certificates -o yaml'.split()) + # out = subprocess.PIPE + try: + registry_secrets_raw = subprocess.Popen('oc get secret registry-certificates -o yaml'.split(), + stdout=subprocess.PIPE) + registry_ds = yaml.load(registry_secrets_raw.communicate()[0]) + registry_c = registry_ds['data']['registry.crt'] + registry_path = registry_ds['metadata']['selfLink'] + except TypeError: + # YAML couldn't load the result, this is not a master + pass + else: + (cert_subject, + cert_expiry_date, + time_remaining) = load_and_handle_cert(registry_c, now, base64decode=True) + + expire_check_result = { + 'cert_cn': cert_subject, + 'path': registry_path, + 'expiry': cert_expiry_date, + 'days_remaining': time_remaining.days, + 'health': None, + } + + classify_cert(expire_check_result, now, time_remaining, expire_window, registry_certs) + + check_results['registry'] = registry_certs + + ###################################################################### + # /Check router/registry certs + ###################################################################### + + res = tabulate_summary(ocp_certs, kubeconfigs, etcd_certs, router_certs, registry_certs) msg = "Checked {count} total certificates. Expired/Warning/OK: {exp}/{warn}/{ok}. Warning window: {window} days".format( count=res['total'], -- cgit v1.2.3