diff options
| -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> | 
