Browse Source

Get router/registry certs. Collect common names and subjectAltNames

Tim Bielawa 8 years ago
parent
commit
4273b21105

+ 146 - 21
library/openshift_cert_expiry.py

@@ -4,6 +4,8 @@
 
 
 """For details on this module see DOCUMENTATION (below)"""
 """For details on this module see DOCUMENTATION (below)"""
 
 
+# router/registry cert grabbing
+import subprocess
 # etcd config file
 # etcd config file
 import ConfigParser
 import ConfigParser
 # Expiration parsing
 # Expiration parsing
@@ -15,7 +17,6 @@ import yaml
 # Certificate loading
 # Certificate loading
 import OpenSSL.crypto
 import OpenSSL.crypto
 
 
-
 DOCUMENTATION = '''
 DOCUMENTATION = '''
 ---
 ---
 module: openshift_cert_expiry
 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(
     cert_loaded = OpenSSL.crypto.load_certificate(
         OpenSSL.crypto.FILETYPE_PEM, _cert_string)
         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
     # 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
     # Grab the expiration date
     cert_expiry = cert_loaded.get_notAfter()
     cert_expiry = cert_loaded.get_notAfter()
@@ -174,7 +226,7 @@ Return:
     return cert_list
     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
     """Calculate the summary text for when the module finishes
 running. This includes counds of each classification and what have
 running. This includes counds of each classification and what have
 you.
 you.
@@ -190,12 +242,14 @@ Return:
 - `summary_results` (dict) - Counts of each cert type classification
 - `summary_results` (dict) - Counts of each cert type classification
   and total items examined.
   and total items examined.
     """
     """
-    items = certificates + kubeconfigs + etcd_certs
+    items = certificates + kubeconfigs + etcd_certs + router_certs + registry_certs
 
 
     summary_results = {
     summary_results = {
         'system_certificates': len(certificates),
         'system_certificates': len(certificates),
         'kubeconfig_certificates': len(kubeconfigs),
         'kubeconfig_certificates': len(kubeconfigs),
         'etcd_certificates': len(etcd_certs),
         'etcd_certificates': len(etcd_certs),
+        'router_certs': len(router_certs),
+        'registry_certs': len(registry_certs),
         'total': len(items),
         'total': len(items),
         'ok': 0,
         'ok': 0,
         'warning': 0,
         'warning': 0,
@@ -213,7 +267,7 @@ Return:
 # This is our module MAIN function after all, so there's bound to be a
 # This is our module MAIN function after all, so there's bound to be a
 # lot of code bundled up into one block
 # 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():
 def main():
     """This module examines certificates (in various forms) which compose
     """This module examines certificates (in various forms) which compose
 an OpenShift Container Platform cluster
 an OpenShift Container Platform cluster
@@ -250,21 +304,19 @@ an OpenShift Container Platform cluster
         openshift_node_config_path,
         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, where do you hide your certs? Used when parsing etcd.conf
     etcd_cert_params = [
     etcd_cert_params = [
@@ -460,7 +512,80 @@ an OpenShift Container Platform cluster
     # /Check etcd certs
     # /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(
     msg = "Checked {count} total certificates. Expired/Warning/OK: {exp}/{warn}/{ok}. Warning window: {window} days".format(
         count=res['total'],
         count=res['total'],

+ 40 - 26
playbooks/common/openshift-cluster/templates/cert-expiry-table.html.j2

@@ -3,7 +3,7 @@
   <head>
   <head>
     <meta charset="UTF-8" />
     <meta charset="UTF-8" />
     <title>OCP Certificate Expiry Report</title>
     <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 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" />
     <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700" rel="stylesheet" />
     <style type="text/css">
     <style type="text/css">
@@ -12,6 +12,7 @@
       margin-left: 50px;
       margin-left: 50px;
       margin-right: 50px;
       margin-right: 50px;
       margin-bottom: 20px;
       margin-bottom: 20px;
+      padding-top: 70px;
       }
       }
       table {
       table {
       border-collapse: collapse;
       border-collapse: collapse;
@@ -37,62 +38,75 @@
     </style>
     </style>
   </head>
   </head>
   <body>
   <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 #}
     {# Each host has a header and table to itself #}
     {% for host in play_hosts %}
     {% for host in play_hosts %}
       <h1>{{ host }}</h1>
       <h1>{{ host }}</h1>
 
 
       <p>
       <p>
-	{{ hostvars[host].check_results.msg }}
+        {{ hostvars[host].check_results.msg }}
       </p>
       </p>
       <ul>
       <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>
       </ul>
 
 
       <table border="1" width="100%">
       <table border="1" width="100%">
         {# These are hard-coded right now, but should be grabbed dynamically from the registered results #}
         {# 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>
           <tr>
             <th colspan="6" style="text-align:center"><h2 class="cert-kind">{{ kind }}</h2></th>
             <th colspan="6" style="text-align:center"><h2 class="cert-kind">{{ kind }}</h2></th>
           </tr>
           </tr>
 
 
           <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>Health</th>
             <th>Days Remaining</th>
             <th>Days Remaining</th>
             <th>Expiration Date</th>
             <th>Expiration Date</th>
             <th>Path</th>
             <th>Path</th>
           </tr>
           </tr>
 
 
-	  {# A row for each certificate examined #}
+          {# A row for each certificate examined #}
           {%- for v in hostvars[host].check_results.check_results[kind] -%}
           {%- 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.health }}</td>
               <td>{{ v.days_remaining }}</td>
               <td>{{ v.days_remaining }}</td>
               <td>{{ v.expiry }}</td>
               <td>{{ v.expiry }}</td>
               <td>{{ v.path }}</td>
               <td>{{ v.path }}</td>
             </tr>
             </tr>
           {% endfor %}
           {% endfor %}
-	  {# end row generation per cert of this type #}
+          {# end row generation per cert of this type #}
         {% endfor %}
         {% endfor %}
-	{# end generation for each kind of cert block #}
+        {# end generation for each kind of cert block #}
       </table>
       </table>
       <hr />
       <hr />
     {% endfor %}
     {% endfor %}
@@ -100,10 +114,10 @@
 
 
     <footer>
     <footer>
       <p>
       <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>
       <p>
       <p>
-	Status icons from bootstrap/glyphicon
+        Status icons from bootstrap/glyphicon
       </p>
       </p>
     </footer>
     </footer>
   </body>
   </body>