Modifying the way rotation works. (#629)
* Modifying the way rotation works. * Adding docs. * Fixing tests.
This commit is contained in:
parent
f8279d6972
commit
46f8ebd136
|
@ -6,7 +6,6 @@
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
import time
|
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
@ -14,62 +13,16 @@ from flask_script import Manager
|
||||||
|
|
||||||
from lemur import database
|
from lemur import database
|
||||||
from lemur.extensions import metrics
|
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.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
|
from lemur.certificates.verify import verify_string
|
||||||
|
|
||||||
manager = Manager(usage="Handles all certificate related tasks.")
|
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):
|
def print_certificate_details(details):
|
||||||
"""
|
"""
|
||||||
Print the certificate details with formatting.
|
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('-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('-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.')
|
@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.
|
been replaced, will use the replacement certificate for the rotation.
|
||||||
"""
|
"""
|
||||||
new_cert = old_cert = None
|
|
||||||
|
|
||||||
print("[+] Staring certificate rotation.")
|
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
print("[!] Running in COMMIT mode.")
|
print("[!] Running in COMMIT mode.")
|
||||||
|
|
||||||
if old_certificate_name:
|
print("[+] Starting endpoint rotation.")
|
||||||
old_cert = get_by_name(old_certificate_name)
|
|
||||||
|
|
||||||
if not old_cert:
|
old_cert = validate_certificate(old_certificate_name)
|
||||||
print("[-] No certificate found with name: {0}".format(old_certificate_name))
|
new_cert = validate_certificate(new_certificate_name)
|
||||||
sys.exit(1)
|
endpoint = validate_endpoint(endpoint_name)
|
||||||
|
|
||||||
if new_certificate_name:
|
if endpoint and new_cert:
|
||||||
new_cert = get_by_name(new_certificate_name)
|
print("[+] Rotating endpoint: {0} to certificate {1}".format(endpoint.name, new_cert.name))
|
||||||
|
request_rotation(endpoint, new_cert, message, commit)
|
||||||
|
|
||||||
if not new_cert:
|
elif old_cert and new_cert:
|
||||||
print("[-] No certificate found with name: {0}".format(old_certificate_name))
|
print("[+] Rotating all endpoints from {0} to {1}".format(old_cert.name, new_cert.name))
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if old_cert and new_cert:
|
for endpoint in old_cert.endpoints:
|
||||||
try:
|
print("[+] Rotating {0}".format(endpoint.name))
|
||||||
reissue_and_rotate(old_cert, new_certificate=new_cert, commit=commit, message=message)
|
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:
|
else:
|
||||||
for certificate in get_all_pending_rotation():
|
print("[+] Rotating all endpoints that have new certificates available")
|
||||||
try:
|
for endpoint in endpoint_service.get_all_pending_rotation():
|
||||||
reissue_and_rotate(certificate, commit=commit, message=message)
|
if len(endpoint.certificate.replaced) == 1:
|
||||||
|
print("[+] Rotating {0} to {1}".format(endpoint.name, endpoint.certificate.replaced[0].name))
|
||||||
if commit:
|
request_rotation(endpoint, endpoint.certificate.replaced[0], message, commit)
|
||||||
metrics.send('certificate_rotation_success', 'counter', 1)
|
else:
|
||||||
|
metrics.send('endpoint_rotation_failure', 'counter', 1)
|
||||||
except Exception as e:
|
print("[!] Failed to rotate endpoint {0} reason: Multiple replacement certificates found.".format(
|
||||||
current_app.logger.exception(e)
|
endpoint.name
|
||||||
|
))
|
||||||
if commit:
|
|
||||||
metrics.send('certificate_rotation_failure', 'counter', 1)
|
|
||||||
|
|
||||||
print("[+] Done!")
|
print("[+] Done!")
|
||||||
|
|
||||||
|
|
||||||
@manager.option('-o', '--old-certificate', dest='old_certificate_name', help='Name of the certificate you wish to reissue.')
|
@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.')
|
@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.
|
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
|
If not time period is provided, reissues certificate as valid from today to
|
||||||
today + length of original.
|
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:
|
if commit:
|
||||||
print("[!] Running in COMMIT mode.")
|
print("[!] Running in COMMIT mode.")
|
||||||
|
|
||||||
details = get_certificate_primitives(old_cert)
|
old_cert = validate_certificate(old_certificate_name)
|
||||||
print_certificate_details(details)
|
|
||||||
|
|
||||||
if commit:
|
if not old_cert:
|
||||||
new_cert = reissue_certificate(old_cert, replace=True)
|
for certificate in get_all_pending_reissue():
|
||||||
print("[+] Issued new certificate named: {0}".format(new_cert.name))
|
print("[+] {0} is eligible for re-issuance".format(certificate.name))
|
||||||
|
request_reissue(certificate, commit)
|
||||||
|
else:
|
||||||
|
request_reissue(old_cert, commit)
|
||||||
|
|
||||||
print("[+] Done!")
|
print("[+] Done!")
|
||||||
|
|
||||||
|
|
|
@ -116,6 +116,7 @@ class Certificate(db.Model):
|
||||||
self.description = kwargs.get('description')
|
self.description = kwargs.get('description')
|
||||||
self.roles = list(set(kwargs.get('roles', [])))
|
self.roles = list(set(kwargs.get('roles', [])))
|
||||||
self.replaces = kwargs.get('replacements', [])
|
self.replaces = kwargs.get('replacements', [])
|
||||||
|
self.rotation = kwargs.get('rotation')
|
||||||
self.signing_algorithm = defaults.signing_algorithm(cert)
|
self.signing_algorithm = defaults.signing_algorithm(cert)
|
||||||
self.bits = defaults.bitstrength(cert)
|
self.bits = defaults.bitstrength(cert)
|
||||||
self.serial = defaults.serial(cert)
|
self.serial = defaults.serial(cert)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import arrow
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from flask import current_app
|
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 import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
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))
|
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.
|
Retrieves all certificates that need to be rotated.
|
||||||
|
|
||||||
|
@ -94,6 +94,7 @@ def get_all_pending_rotation():
|
||||||
|
|
||||||
return Certificate.query.filter(Certificate.rotation == True)\
|
return Certificate.query.filter(Certificate.rotation == True)\
|
||||||
.filter(Certificate.endpoints.any())\
|
.filter(Certificate.endpoints.any())\
|
||||||
|
.filter(not_(Certificate.replaced.any()))\
|
||||||
.filter(Certificate.not_after <= end.format('YYYY-MM-DD')).all() # noqa
|
.filter(Certificate.not_after <= end.format('YYYY-MM-DD')).all() # noqa
|
||||||
|
|
||||||
|
|
||||||
|
@ -533,7 +534,8 @@ def get_certificate_primitives(certificate):
|
||||||
state=certificate.state,
|
state=certificate.state,
|
||||||
location=certificate.location,
|
location=certificate.location,
|
||||||
key_type=certificate.key_type,
|
key_type=certificate.key_type,
|
||||||
notifications=certificate.notifications
|
notifications=certificate.notifications,
|
||||||
|
rotation=certificate.rotation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"""
|
"""
|
||||||
import arrow
|
import arrow
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
|
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from sqlalchemy.sql.expression import case
|
from sqlalchemy.sql.expression import case
|
||||||
|
@ -70,6 +71,8 @@ class Endpoint(db.Model):
|
||||||
last_updated = Column(ArrowType, default=arrow.utcnow, nullable=False)
|
last_updated = Column(ArrowType, default=arrow.utcnow, nullable=False)
|
||||||
date_created = Column(ArrowType, default=arrow.utcnow, onupdate=arrow.utcnow, nullable=False)
|
date_created = Column(ArrowType, default=arrow.utcnow, onupdate=arrow.utcnow, nullable=False)
|
||||||
|
|
||||||
|
replaced = association_proxy('certificate', 'replaced')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def issues(self):
|
def issues(self):
|
||||||
issues = []
|
issues = []
|
||||||
|
|
|
@ -40,14 +40,24 @@ def get(endpoint_id):
|
||||||
return database.get(Endpoint, 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.
|
Retrieves an endpoint given it's name.
|
||||||
|
|
||||||
:param endpoint_dnsname:
|
:param name:
|
||||||
:return:
|
: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):
|
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
|
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):
|
def create(**kwargs):
|
||||||
"""
|
"""
|
||||||
Creates a new endpoint.
|
Creates a new endpoint.
|
||||||
|
|
|
@ -41,7 +41,7 @@ def test_get_certificate_primitives(certificate):
|
||||||
|
|
||||||
with freeze_time(datetime.date(year=2016, month=10, day=30)):
|
with freeze_time(datetime.date(year=2016, month=10, day=30)):
|
||||||
primitives = get_certificate_primitives(certificate)
|
primitives = get_certificate_primitives(certificate)
|
||||||
assert len(primitives) == 16
|
assert len(primitives) == 17
|
||||||
|
|
||||||
|
|
||||||
def test_certificate_edit_schema(session):
|
def test_certificate_edit_schema(session):
|
||||||
|
|
Loading…
Reference in New Issue