Certificate rotation enhancements (#570)
This commit is contained in:
167
lemur/certificates/cli.py
Normal file
167
lemur/certificates/cli.py
Normal 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)
|
@ -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'])
|
@ -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"))
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user