diff options
author | Tim Bielawa <tbielawa@redhat.com> | 2016-10-06 10:01:48 -0700 |
---|---|---|
committer | Tim Bielawa <tbielawa@redhat.com> | 2016-10-20 07:49:40 -0700 |
commit | 4273b21105dd11f52de354b4777d33e4296ba7e0 (patch) | |
tree | bc8cdda1e6147e2b0020feb3544217ab5422a464 | |
parent | 5f7f6a6023c470337f0d879f55eb619fd63e2dbe (diff) | |
download | openshift-4273b21105dd11f52de354b4777d33e4296ba7e0.tar.gz openshift-4273b21105dd11f52de354b4777d33e4296ba7e0.tar.bz2 openshift-4273b21105dd11f52de354b4777d33e4296ba7e0.tar.xz openshift-4273b21105dd11f52de354b4777d33e4296ba7e0.zip |
Get router/registry certs. Collect common names and subjectAltNames
-rw-r--r-- | library/openshift_cert_expiry.py | 167 | ||||
-rw-r--r-- | playbooks/common/openshift-cluster/templates/cert-expiry-table.html.j2 | 66 |
2 files changed, 186 insertions, 47 deletions
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'], diff --git a/playbooks/common/openshift-cluster/templates/cert-expiry-table.html.j2 b/playbooks/common/openshift-cluster/templates/cert-expiry-table.html.j2 index da7844c37..f74d7f1ce 100644 --- a/playbooks/common/openshift-cluster/templates/cert-expiry-table.html.j2 +++ b/playbooks/common/openshift-cluster/templates/cert-expiry-table.html.j2 @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8" /> <title>OCP Certificate Expiry Report</title> - {# For fancy icons #} + {# For fancy icons and a pleasing font #} <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" /> <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700" rel="stylesheet" /> <style type="text/css"> @@ -12,6 +12,7 @@ margin-left: 50px; margin-right: 50px; margin-bottom: 20px; + padding-top: 70px; } table { border-collapse: collapse; @@ -37,62 +38,75 @@ </style> </head> <body> - <center><h1>OCP Certificate Expiry Report</h1></center> - - <hr /> + <nav class="navbar navbar-default navbar-fixed-top"> + <div class="container-fluid"> + <div class="navbar-header"> + <a class="navbar-brand" href="#">OCP Certificate Expiry Report</a> + </div> + <div class="collapse navbar-collapse"> + <p class="navbar-text navbar-right"> + <a href="https://docs.openshift.com/container-platform/latest/install_config/redeploying_certificates.html" + target="_blank" + class="navbar-link"> + <i class="glyphicon glyphicon-book"></i> Redeploying Certificates + </a> + </p> + </div> + </div> + </nav> {# Each host has a header and table to itself #} {% for host in play_hosts %} <h1>{{ host }}</h1> <p> - {{ hostvars[host].check_results.msg }} + {{ hostvars[host].check_results.msg }} </p> <ul> - <li><b>Expirations checked at:</b> {{ hostvars[host].check_results.check_results.meta.checked_at_time }}</li> - <li><b>Warn after date:</b> {{ hostvars[host].check_results.check_results.meta.warn_after_date }}</li> + <li><b>Expirations checked at:</b> {{ hostvars[host].check_results.check_results.meta.checked_at_time }}</li> + <li><b>Warn after date:</b> {{ hostvars[host].check_results.check_results.meta.warn_after_date }}</li> </ul> <table border="1" width="100%"> {# These are hard-coded right now, but should be grabbed dynamically from the registered results #} - {%- for kind in ['ocp_certs', 'etcd', 'kubeconfigs'] -%} + {%- for kind in ['ocp_certs', 'etcd', 'kubeconfigs', 'router', 'registry'] -%} <tr> <th colspan="6" style="text-align:center"><h2 class="cert-kind">{{ kind }}</h2></th> </tr> <tr> - <th> </th> - <th>Certificate Common Name</th> + <th> </th> + <th style="width:33%">Certificate Common/Alt Name(s)</th> <th>Health</th> <th>Days Remaining</th> <th>Expiration Date</th> <th>Path</th> </tr> - {# A row for each certificate examined #} + {# A row for each certificate examined #} {%- for v in hostvars[host].check_results.check_results[kind] -%} - {# Let's add some flair and show status visually with fancy icons #} - {% if v.health == 'ok' %} - {% set health_icon = 'glyphicon glyphicon-ok' %} - {% elif v.health == 'warning' %} - {% set health_icon = 'glyphicon glyphicon-alert' %} - {% else %} - {% set health_icon = 'glyphicon glyphicon-remove' %} - {% endif %} + {# Let's add some flair and show status visually with fancy icons #} + {% if v.health == 'ok' %} + {% set health_icon = 'glyphicon glyphicon-ok' %} + {% elif v.health == 'warning' %} + {% set health_icon = 'glyphicon glyphicon-alert' %} + {% else %} + {% set health_icon = 'glyphicon glyphicon-remove' %} + {% endif %} - <tr class="{{ loop.cycle('odd', 'even') }}"> - <td style="text-align:center"><i class="{{ health_icon }}"></i></td> - <td>{{ v.cert_cn }}</td> + <tr class="{{ loop.cycle('odd', 'even') }}"> + <td style="text-align:center"><i class="{{ health_icon }}"></i></td> + <td style="width:33%">{{ v.cert_cn }}</td> <td>{{ v.health }}</td> <td>{{ v.days_remaining }}</td> <td>{{ v.expiry }}</td> <td>{{ v.path }}</td> </tr> {% endfor %} - {# end row generation per cert of this type #} + {# end row generation per cert of this type #} {% endfor %} - {# end generation for each kind of cert block #} + {# end generation for each kind of cert block #} </table> <hr /> {% endfor %} @@ -100,10 +114,10 @@ <footer> <p> - Expiration report generated by <a href="https://github.com/openshift/openshift-ansible" target="_blank">openshift-ansible</a> + Expiration report generated by <a href="https://github.com/openshift/openshift-ansible" target="_blank">openshift-ansible</a> </p> <p> - Status icons from bootstrap/glyphicon + Status icons from bootstrap/glyphicon </p> </footer> </body> |