Modifying the way rotation works. (#629)

* Modifying the way rotation works.

* Adding docs.

* Fixing tests.
This commit is contained in:
kevgliss 2016-12-23 13:18:42 -08:00 committed by GitHub
parent f8279d6972
commit 46f8ebd136
6 changed files with 154 additions and 111 deletions

View File

@ -6,7 +6,6 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import sys
import time
from flask import current_app
@ -14,62 +13,16 @@ from flask_script import Manager
from lemur import database
from lemur.extensions import metrics
from lemur.deployment.service import rotate_certificate
from lemur.deployment import service as deployment_service
from lemur.endpoints import service as endpoint_service
from lemur.notifications.messaging import send_rotation_notification
from lemur.certificates.service import reissue_certificate, get_certificate_primitives, get_all_pending_rotation, get_by_name, get_all_certs
from lemur.certificates.service import reissue_certificate, get_certificate_primitives, get_all_pending_reissue, get_by_name, get_all_certs
from lemur.certificates.verify import verify_string
manager = Manager(usage="Handles all certificate related tasks.")
def reissue_and_rotate(old_certificate, new_certificate=None, commit=False, message=False):
if not new_certificate:
# we don't want to re-issue if it's already been replaced
if not old_certificate.replaced:
details = get_certificate_primitives(old_certificate)
print_certificate_details(details)
if commit:
new_certificate = reissue_certificate(old_certificate, replace=True)
print("[+] Issued new certificate named: {0}".format(new_certificate.name))
time.sleep(10)
print("[!] Sleeping to ensure that certificate propagates before rotating.")
else:
new_certificate = old_certificate
print("[+] Done!")
else:
if len(old_certificate.replaced) > 1:
raise Exception(
"Unable to rotate certificate based on replacement, found more than one!"
)
else:
new_certificate = old_certificate.replaced[0]
print("[!] Certificate has been replaced by: {0}".format(old_certificate.replaced[0].name))
if len(old_certificate.endpoints) > 0:
for endpoint in old_certificate.endpoints:
print(
"[+] Certificate deployed on endpoint: name:{name} dnsname:{dnsname} port:{port} type:{type}".format(
name=endpoint.name,
dnsname=endpoint.dnsname,
port=endpoint.port,
type=endpoint.type
)
)
print("[+] Rotating certificate from: {0} to: {1}".format(old_certificate.name, new_certificate.name))
if commit:
rotate_certificate(endpoint, new_certificate)
print("[+] Done!")
if message:
send_rotation_notification(old_certificate)
def print_certificate_details(details):
"""
Print the certificate details with formatting.
@ -102,90 +55,155 @@ def print_certificate_details(details):
)
def validate_certificate(certificate_name):
"""
Ensuring that the specified certificate exists.
:param certificate_name:
:return:
"""
if certificate_name:
cert = get_by_name(certificate_name)
if not cert:
print("[-] No certificate found with name: {0}".format(certificate_name))
sys.exit(1)
return cert
def validate_endpoint(endpoint_name):
"""
Ensuring that the specified endpoint exists.
:param endpoint_name:
:return:
"""
if endpoint_name:
endpoint = endpoint_service.get_by_name(endpoint_name)
if not endpoint:
print("[-] No endpoint found with name: {0}".format(endpoint_name))
sys.exit(1)
return endpoint
def request_rotation(endpoint, certificate, message, commit):
"""
Rotates a certificate and handles any exceptions during
execution.
:param endpoint:
:param certificate:
:param message:
:param commit:
:return:
"""
if commit:
try:
deployment_service.rotate_certificate(endpoint, certificate)
metrics.send('endpoint_rotation_success', 'counter', 1)
if message:
send_rotation_notification(certificate)
except Exception as e:
metrics.send('endpoint_rotation_failure', 'counter', 1)
print(
"[!] Failed to rotate endpoint {0} to certificate {1} reason: {2}".format(
endpoint.name,
certificate.name,
e
)
)
def request_reissue(certificate, commit):
"""
Reissuing certificate and handles any exceptions.
:param certificate:
:param commit:
:return:
"""
details = get_certificate_primitives(certificate)
print_certificate_details(details)
if commit:
try:
new_cert = reissue_certificate(certificate, replace=True)
metrics.send('certificate_reissue_success', 'counter', 1)
print("[+] New certificate named: {0}".format(new_cert.name))
except Exception as e:
metrics.send('certificate_reissue_failure', 'counter', 1)
print(
"[!] Failed to reissue certificate {1} reason: {2}".format(
certificate.name,
e
)
)
@manager.option('-e', '--endpoint', dest='endpoint_name', help='Name of the endpoint you wish to rotate.')
@manager.option('-n', '--new-certificate', dest='new_certificate_name', help='Name of the certificate you wish to rotate to.')
@manager.option('-o', '--old-certificate', dest='old_certificate_name', help='Name of the certificate you wish to rotate.')
@manager.option('-a', '--notify', dest='message', help='Send a rotation notification to the certificates owner.')
@manager.option('-a', '--notify', dest='message', action='store_true', help='Send a rotation notification to the certificates owner.')
@manager.option('-c', '--commit', dest='commit', action='store_true', default=False, help='Persist changes.')
def rotate(new_certificate_name, old_certificate_name, message, commit):
def rotate(endpoint_name, new_certificate_name, old_certificate_name, message, commit):
"""
Rotates a certificate and reissues it if it has not already been replaced. If it has
Rotates an endpoint and reissues it if it has not already been replaced. If it has
been replaced, will use the replacement certificate for the rotation.
"""
new_cert = old_cert = None
print("[+] Staring certificate rotation.")
if commit:
print("[!] Running in COMMIT mode.")
if old_certificate_name:
old_cert = get_by_name(old_certificate_name)
print("[+] Starting endpoint rotation.")
if not old_cert:
print("[-] No certificate found with name: {0}".format(old_certificate_name))
sys.exit(1)
old_cert = validate_certificate(old_certificate_name)
new_cert = validate_certificate(new_certificate_name)
endpoint = validate_endpoint(endpoint_name)
if new_certificate_name:
new_cert = get_by_name(new_certificate_name)
if endpoint and new_cert:
print("[+] Rotating endpoint: {0} to certificate {1}".format(endpoint.name, new_cert.name))
request_rotation(endpoint, new_cert, message, commit)
if not new_cert:
print("[-] No certificate found with name: {0}".format(old_certificate_name))
sys.exit(1)
elif old_cert and new_cert:
print("[+] Rotating all endpoints from {0} to {1}".format(old_cert.name, new_cert.name))
if old_cert and new_cert:
try:
reissue_and_rotate(old_cert, new_certificate=new_cert, commit=commit, message=message)
for endpoint in old_cert.endpoints:
print("[+] Rotating {0}".format(endpoint.name))
request_rotation(endpoint, new_cert, message, commit)
if commit:
metrics.send('certificate_rotation_success', 'counter', 1)
except Exception as e:
current_app.logger.exception(e)
if commit:
metrics.send('certificate_rotation_failure', 'counter', 1)
else:
for certificate in get_all_pending_rotation():
try:
reissue_and_rotate(certificate, commit=commit, message=message)
if commit:
metrics.send('certificate_rotation_success', 'counter', 1)
except Exception as e:
current_app.logger.exception(e)
if commit:
metrics.send('certificate_rotation_failure', 'counter', 1)
print("[+] Rotating all endpoints that have new certificates available")
for endpoint in endpoint_service.get_all_pending_rotation():
if len(endpoint.certificate.replaced) == 1:
print("[+] Rotating {0} to {1}".format(endpoint.name, endpoint.certificate.replaced[0].name))
request_rotation(endpoint, endpoint.certificate.replaced[0], message, commit)
else:
metrics.send('endpoint_rotation_failure', 'counter', 1)
print("[!] Failed to rotate endpoint {0} reason: Multiple replacement certificates found.".format(
endpoint.name
))
print("[+] Done!")
@manager.option('-o', '--old-certificate', dest='old_certificate_name', help='Name of the certificate you wish to reissue.')
@manager.option('-s', '--validity-start', dest='validity_start', help='Validity starting date. Format: YYYY-MM-DD.')
@manager.option('-e', '--validity-end', dest='validity_end', help='Validity ending date. Format: YYYY-MM-DD.')
@manager.option('-c', '--commit', dest='commit', action='store_true', default=False, help='Persist changes.')
def reissue(old_certificate_name, validity_start, validity_end, commit):
def reissue(old_certificate_name, commit):
"""
Reissues certificate with the same parameters as it was originally issued with.
If not time period is provided, reissues certificate as valid from today to
today + length of original.
"""
old_cert = get_by_name(old_certificate_name)
if not old_cert:
print("[-] No certificate found with name: {0}".format(old_certificate_name))
sys.exit(1)
if commit:
print("[!] Running in COMMIT mode.")
details = get_certificate_primitives(old_cert)
print_certificate_details(details)
old_cert = validate_certificate(old_certificate_name)
if commit:
new_cert = reissue_certificate(old_cert, replace=True)
print("[+] Issued new certificate named: {0}".format(new_cert.name))
if not old_cert:
for certificate in get_all_pending_reissue():
print("[+] {0} is eligible for re-issuance".format(certificate.name))
request_reissue(certificate, commit)
else:
request_reissue(old_cert, commit)
print("[+] Done!")

View File

@ -116,6 +116,7 @@ class Certificate(db.Model):
self.description = kwargs.get('description')
self.roles = list(set(kwargs.get('roles', [])))
self.replaces = kwargs.get('replacements', [])
self.rotation = kwargs.get('rotation')
self.signing_algorithm = defaults.signing_algorithm(cert)
self.bits = defaults.bitstrength(cert)
self.serial = defaults.serial(cert)

View File

@ -9,7 +9,7 @@ import arrow
from datetime import timedelta
from flask import current_app
from sqlalchemy import func, or_, cast, Boolean
from sqlalchemy import func, or_, not_, cast, Boolean
from cryptography import x509
from cryptography.hazmat.backends import default_backend
@ -78,7 +78,7 @@ def get_by_source(source_label):
return Certificate.query.filter(Certificate.sources.any(label=source_label))
def get_all_pending_rotation():
def get_all_pending_reissue():
"""
Retrieves all certificates that need to be rotated.
@ -94,6 +94,7 @@ def get_all_pending_rotation():
return Certificate.query.filter(Certificate.rotation == True)\
.filter(Certificate.endpoints.any())\
.filter(not_(Certificate.replaced.any()))\
.filter(Certificate.not_after <= end.format('YYYY-MM-DD')).all() # noqa
@ -533,7 +534,8 @@ def get_certificate_primitives(certificate):
state=certificate.state,
location=certificate.location,
key_type=certificate.key_type,
notifications=certificate.notifications
notifications=certificate.notifications,
rotation=certificate.rotation
)

View File

@ -8,6 +8,7 @@
"""
import arrow
from sqlalchemy.orm import relationship
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql.expression import case
@ -70,6 +71,8 @@ class Endpoint(db.Model):
last_updated = Column(ArrowType, default=arrow.utcnow, nullable=False)
date_created = Column(ArrowType, default=arrow.utcnow, onupdate=arrow.utcnow, nullable=False)
replaced = association_proxy('certificate', 'replaced')
@property
def issues(self):
issues = []

View File

@ -40,14 +40,24 @@ def get(endpoint_id):
return database.get(Endpoint, endpoint_id)
def get_by_dnsname(endpoint_dnsname):
def get_by_name(name):
"""
Retrieves an endpoint given it's name.
:param endpoint_dnsname:
:param name:
:return:
"""
return database.get(Endpoint, endpoint_dnsname, field='dnsname')
return database.get(Endpoint, name, field='name')
def get_by_dnsname(dnsname):
"""
Retrieves an endpoint given it's name.
:param dnsname:
:return:
"""
return database.get(Endpoint, dnsname, field='dnsname')
def get_by_source(source_label):
@ -59,6 +69,15 @@ def get_by_source(source_label):
return Endpoint.query.filter(Endpoint.source.label == source_label).all() # noqa
def get_all_pending_rotation():
"""
Retrieves all endpoints which have certificates deployed
that have been replaced.
:return:
"""
return Endpoint.query.filter(Endpoint.replaced.any()).all()
def create(**kwargs):
"""
Creates a new endpoint.

View File

@ -41,7 +41,7 @@ def test_get_certificate_primitives(certificate):
with freeze_time(datetime.date(year=2016, month=10, day=30)):
primitives = get_certificate_primitives(certificate)
assert len(primitives) == 16
assert len(primitives) == 17
def test_certificate_edit_schema(session):