Certificate rotation enhancements (#570)

This commit is contained in:
kevgliss
2016-12-07 16:24:59 -08:00
committed by GitHub
parent 9adc5ad59e
commit fc205713c8
19 changed files with 607 additions and 598 deletions

View File

@ -0,0 +1,29 @@
"""
.. module: lemur.notifications.cli
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask_script import Manager
from lemur.notifications.messaging import send_expiration_notifications
manager = Manager(usage="Handles notification related tasks.")
@manager.command
def notify():
"""
Runs Lemur's notification engine, that looks for expired certificates and sends
notifications out to those that have subscribed to them.
:return:
"""
print("Starting to notify subscribers about expiring certificates!")
count = send_expiration_notifications()
print(
"Finished notifying subscribers about expiring certificates! Sent {count} notifications!".format(
count=count
)
)

View File

@ -0,0 +1,102 @@
"""
.. module: lemur.notifications.messaging
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import arrow
from flask import current_app
from lemur import database, metrics
from lemur.certificates.schemas import certificate_notification_output_schema
from lemur.notifications.models import Notification
from lemur.plugins import plugins
from lemur.plugins.utils import get_plugin_option
def send_expiration_notifications():
"""
This function will check for upcoming certificate expiration,
and send out notification emails at given intervals.
"""
for plugin in plugins.all(plugin_type='notification'):
notifications = database.db.session.query(Notification)\
.filter(Notification.plugin_name == plugin.slug)\
.filter(Notification.active == True).all() # noqa
messages = []
for n in notifications:
for certificate in n.certificates:
if needs_notification(certificate):
data = certificate_notification_output_schema.dump(certificate).data
messages.append((data, n.options))
for data, targets, options in messages:
try:
plugin.send('expiration', data, targets, options)
metrics.send('expiration_notification_sent', 'counter', 1)
except Exception as e:
metrics.send('expiration_notification_failure', 'counter', 1)
current_app.logger.exception(e)
def send_rotation_notification(certificate):
"""
Sends a report to certificate owners when their certificate as been
rotated.
:param certificate:
:return:
"""
plugin = plugins.get(current_app.config.get('LEMUR_DEFAULT_NOTIFICATION_PLUGIN'))
data = certificate_notification_output_schema.dump(certificate).data
try:
plugin.send('rotation', data, [data.owner])
metrics.send('rotation_notification_sent', 'counter', 1)
except Exception as e:
metrics.send('rotation_notification_failure', 'counter', 1)
current_app.logger.exception(e)
def needs_notification(certificate):
"""
Determine if notifications for a given certificate should
currently be sent
:param certificate:
:return:
"""
if not certificate.notify:
return
if not certificate.notifications:
return
now = arrow.utcnow()
days = (certificate.not_after - now).days
for notification in certificate.notifications:
interval = get_plugin_option('interval', notification.options)
unit = get_plugin_option('unit', notification.options)
if unit == 'weeks':
interval *= 7
elif unit == 'months':
interval *= 30
elif unit == 'days': # it's nice to be explicit about the base unit
pass
else:
raise Exception("Invalid base unit for expiration interval: {0}".format(unit))
if days == interval:
return certificate

View File

@ -1,5 +1,5 @@
"""
.. module: lemur.notifications
.. module: lemur.notifications.service
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
@ -8,181 +8,11 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import ssl
import arrow
from flask import current_app
from lemur import database
from lemur.domains.models import Domain
from lemur.notifications.models import Notification
from lemur.certificates.models import Certificate
from lemur.certificates import service as cert_service
from lemur.plugins.base import plugins
def get_options(name, options):
for o in options:
if o.get('name') == name:
return o
def _get_message_data(cert):
"""
Parse our the certification information needed for our notification
:param cert:
:return:
"""
cert_dict = {}
if cert.user:
cert_dict['creator'] = cert.user.email
cert_dict['not_after'] = cert.not_after
cert_dict['owner'] = cert.owner
cert_dict['name'] = cert.name
cert_dict['body'] = cert.body
cert_dict['endpoints'] = [{'name': x.name, 'dnsname': x.dnsname} for x in cert.endpoints]
return cert_dict
def _deduplicate(messages):
"""
Take all of the messages that should be sent and provide
a roll up to the same set if the recipients are the same
"""
roll_ups = []
for data, options in messages:
o = get_options('recipients', options)
targets = o['value'].split(',')
for m, r, o in roll_ups:
if r == targets:
for cert in m:
if cert['body'] == data['body']:
break
else:
m.append(data)
current_app.logger.info(
"Sending expiration alert about {0} to {1}".format(
data['name'], ",".join(targets)))
break
else:
roll_ups.append(([data], targets, options))
return roll_ups
def send_expiration_notifications():
"""
This function will check for upcoming certificate expiration,
and send out notification emails at given intervals.
"""
sent = 0
for plugin in plugins.all(plugin_type='notification'):
notifications = database.db.session.query(Notification)\
.filter(Notification.plugin_name == plugin.slug)\
.filter(Notification.active == True).all() # noqa
messages = []
for n in notifications:
for c in n.certificates:
if _is_eligible_for_notifications(c):
messages.append((_get_message_data(c), n.options))
messages = _deduplicate(messages)
for data, targets, options in messages:
sent += 1
plugin.send('expiration', data, targets, options)
current_app.logger.info("Lemur has sent {0} certification notifications".format(sent))
return sent
def _get_domain_certificate(name):
"""
Fetch the SSL certificate currently hosted at a given domain (if any) and
compare it against our all of our know certificates to determine if a new
SSL certificate has already been deployed
:param name:
:return:
"""
try:
pub_key = ssl.get_server_certificate((name, 443))
return cert_service.find_duplicates(pub_key.strip())
except Exception as e:
current_app.logger.info(str(e))
return []
def _find_superseded(cert):
"""
Here we try to fetch any domain in the certificate to see if we can resolve it
and to try and see if it is currently serving the certificate we are
alerting on.
:param domains:
:return:
"""
query = database.session_query(Certificate)
ss_list = []
# determine what is current host at our domains
for domain in cert.domains:
dups = _get_domain_certificate(domain.name)
for c in dups:
if c.body != cert.body:
ss_list.append(dups)
current_app.logger.info("Trying to resolve {0}".format(domain.name))
# look for other certificates that may not be hosted but cover the same domains
query = query.filter(Certificate.domains.any(Domain.name.in_([x.name for x in cert.domains])))
query = query.filter(Certificate.active == True) # noqa
query = query.filter(Certificate.not_after >= arrow.utcnow().format('YYYY-MM-DD'))
query = query.filter(Certificate.body != cert.body)
ss_list.extend(query.all())
return ss_list
def _is_eligible_for_notifications(cert):
"""
Determine if notifications for a given certificate should
currently be sent
:param cert:
:return:
"""
if not cert.notify:
return
now = arrow.utcnow()
days = (cert.not_after - now.naive).days
for notification in cert.notifications:
interval = get_options('interval', notification.options)['value']
unit = get_options('unit', notification.options)['value']
if unit == 'weeks':
interval *= 7
elif unit == 'months':
interval *= 30
elif unit == 'days': # it's nice to be explicit about the base unit
pass
else:
raise Exception("Invalid base unit for expiration interval: {0}".format(unit))
if days == interval:
return cert
from lemur.notifications.models import Notification
def create_default_expiration_notifications(name, recipients):