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

167
lemur/certificates/cli.py Normal file
View File

@ -0,0 +1,167 @@
"""
.. module: lemur.certificate.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>
"""
import sys
from flask import current_app
from flask_script import Manager
from lemur import database
from lemur.deployment.service import rotate_certificate
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.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))
print("[+] Done!")
else:
new_certificate = old_certificate.replaced
print("[!] Certificate has been replaced by: {0}".format(old_certificate.replaced.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.
:param details:
:return:
"""
print("[+] Re-issuing certificate with the following details: ")
print(
"[+] Common Name: {common_name}\n"
"[+] Subject Alternate Names: {sans}\n"
"[+] Authority: {authority_name}\n"
"[+] Validity Start: {validity_start}\n"
"[+] Validity End: {validity_end}\n"
"[+] Organization: {organization}\n"
"[+] Organizational Unit: {organizational_unit}\n"
"[+] Country: {country}\n"
"[+] State: {state}\n"
"[+] Location: {location}\n".format(
common_name=details['common_name'],
sans=",".join(x['value'] for x in details['extensions']['sub_alt_names']['names']),
authority_name=details['authority'].name,
validity_start=details['validity_start'].isoformat(),
validity_end=details['validity_end'].isoformat(),
organization=details['organization'],
organizational_unit=details['organizational_unit'],
country=details['country'],
state=details['state'],
location=details['location']
)
)
@manager.command
def rotate(new_certificate_name=False, old_certificate_name=False, message=False, commit=False):
new_cert = old_cert = None
if commit:
print("[!] Running in COMMIT mode.")
if old_certificate_name:
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 new_certificate_name:
new_cert = get_by_name(new_certificate_name)
if not new_cert:
print("[-] No certificate found with name: {0}".format(old_certificate_name))
sys.exit(1)
if old_cert and new_cert:
reissue_and_rotate(old_cert, new_certificate=new_cert, commit=commit, message=message)
else:
for certificate in get_all_pending_rotation():
reissue_and_rotate(certificate, commit=commit, message=message)
@manager.command
def reissue(old_certificate_name, commit=False):
from lemur.certificates.service import get_by_name, reissue_certificate, get_certificate_primitives
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)
if commit:
new_cert = reissue_certificate(old_cert, replace=True)
print("[+] Issued new certificate named: {0}".format(new_cert.name))
print("[+] Done!")
@manager.command
def check_revoked():
"""
Function attempts to update Lemur's internal cache with revoked
certificates. This is called periodically by Lemur. It checks both
CRLs and OCSP to see if a certificate is revoked. If Lemur is unable
encounters an issue with verification it marks the certificate status
as `unknown`.
"""
for cert in get_all_certs():
try:
if cert.chain:
status = verify_string(cert.body, cert.chain)
else:
status = verify_string(cert.body, "")
cert.status = 'valid' if status else 'invalid'
except Exception as e:
current_app.logger.exception(e)
cert.status = 'unknown'
database.update(cert)

View File

@ -1,88 +0,0 @@
"""
.. module: lemur.certificates.exceptions
:synopsis: Defines all monterey specific exceptions
: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 import current_app
from lemur.exceptions import LemurException
class UnknownAuthority(LemurException):
def __init__(self, authority):
self.code = 404
self.authority = authority
self.data = {"message": "The authority specified '{}' is not a valid authority".format(self.authority)}
current_app.logger.warning(self)
def __str__(self):
return repr(self.data['message'])
class InsufficientDomains(LemurException):
def __init__(self):
self.code = 400
self.data = {"message": "Need at least one domain specified in order create a certificate"}
current_app.logger.warning(self)
def __str__(self):
return repr(self.data['message'])
class InvalidCertificate(LemurException):
def __init__(self):
self.code = 400
self.data = {"message": "Need at least one domain specified in order create a certificate"}
current_app.logger.warning(self)
def __str__(self):
return repr(self.data['message'])
class UnableToCreateCSR(LemurException):
def __init__(self):
self.code = 500
self.data = {"message": "Unable to generate CSR"}
current_app.logger.error(self)
def __str__(self):
return repr(self.data['message'])
class UnableToCreatePrivateKey(LemurException):
def __init__(self):
self.code = 500
self.data = {"message": "Unable to generate Private Key"}
current_app.logger.error(self)
def __str__(self):
return repr(self.data['message'])
class MissingFiles(LemurException):
def __init__(self, path):
self.code = 500
self.path = path
self.data = {"path": self.path, "message": "Expecting missing files"}
current_app.logger.error(self)
def __str__(self):
return repr(self.data['message'])
class NoPersistanceFound(LemurException):
def __init__(self):
self.code = 500
self.data = {"code": 500, "message": "No peristence method found, Lemur cannot persist sensitive information"}
current_app.logger.error(self)
def __str__(self):
return repr(self.data['message'])

View File

@ -66,6 +66,8 @@ class Certificate(db.Model):
bits = Column(Integer())
san = Column(String(1024)) # TODO this should be migrated to boolean
rotation = Column(Boolean)
user_id = Column(Integer, ForeignKey('users.id'))
authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))

View File

@ -201,9 +201,20 @@ class CertificateExportInputSchema(LemurInputSchema):
plugin = fields.Nested(PluginInputSchema)
class CertificateNotificationOutputSchema(LemurOutputSchema):
description = fields.String()
issuer = fields.String()
name = fields.String()
owner = fields.Email()
user = fields.Nested(UserNestedOutputSchema)
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
certificate_input_schema = CertificateInputSchema()
certificate_output_schema = CertificateOutputSchema()
certificates_output_schema = CertificateOutputSchema(many=True)
certificate_upload_input_schema = CertificateUploadInputSchema()
certificate_export_input_schema = CertificateExportInputSchema()
certificate_edit_input_schema = CertificateEditInputSchema()
certificate_notification_output_schema = CertificateNotificationOutputSchema()

View File

@ -1,14 +1,15 @@
"""
.. module: service
.. module: lemur.certificate.service
: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 datetime import timedelta
from sqlalchemy import func, or_
from flask import current_app
from sqlalchemy import func, or_
from cryptography import x509
from cryptography.hazmat.backends import default_backend
@ -17,15 +18,15 @@ from cryptography.hazmat.primitives import hashes, serialization
from lemur import database
from lemur.extensions import metrics
from lemur.plugins.base import plugins
from lemur.certificates.models import Certificate
from lemur.common.utils import generate_private_key
from lemur.destinations.models import Destination
from lemur.notifications.models import Notification
from lemur.authorities.models import Authority
from lemur.domains.models import Domain
from lemur.roles.models import Role
from lemur.domains.models import Domain
from lemur.authorities.models import Authority
from lemur.destinations.models import Destination
from lemur.certificates.models import Certificate
from lemur.notifications.models import Notification
from lemur.roles import service as role_service
@ -77,6 +78,24 @@ def get_by_source(source_label):
return Certificate.query.filter(Certificate.sources.any(label=source_label))
def get_all_pending_rotation():
"""
Retrieves all certificates that need to be rotated.
Must be X days from expiration, uses `LEMUR_DEFAULT_ROTATION_INTERVAL`
to determine how many days from expiration the certificate must be
for rotation to be pending.
:return:
"""
now = arrow.utcnow()
interval = current_app.config.get('LEMUR_DEFAULT_ROTATION_INTERVAL', 30)
end = now + timedelta(days=interval)
return Certificate.query.filter(Certificate.rotation == True)\
.filter(Certificate.not_after <= end.format('YYYY-MM-DD')).all() # noqa
def find_duplicates(cert):
"""
Finds certificates that already exist within Lemur. We do this by looking for
@ -527,9 +546,9 @@ def reissue_certificate(certificate, replace=None, user=None):
else:
primitives['creator'] = user
if replace:
primitives['replaces'] = certificate
new_cert = create(**primitives)
if replace:
certificate.notify = False
return new_cert