Jelajahi Sumber

Get router/registry certs. Collect common names and subjectAltNames

Tim Bielawa 8 tahun lalu
induk
melakukan
4273b21105

+ 146 - 21
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'],

+ 40 - 26
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>&nbsp;</th>
-            <th>Certificate Common Name</th>
+            <th>&nbsp;</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>