Adding the ability to specify a per-certificate rotation policy. (#851)

This commit is contained in:
kevgliss
2017-07-12 16:46:11 -07:00
committed by GitHub
parent 560bd5a872
commit 443eb43d1f
18 changed files with 291 additions and 50 deletions

View File

@ -6,6 +6,7 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import arrow
from datetime import timedelta
from flask import current_app
@ -15,7 +16,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
from idna.core import InvalidCodepoint
from sqlalchemy.orm import relationship
from sqlalchemy.sql.expression import case
from sqlalchemy.sql.expression import case, extract
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import event, Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean
@ -37,6 +38,7 @@ from lemur.models import certificate_associations, certificate_source_associatio
certificate_replacement_associations, roles_certificates
from lemur.domains.models import Domain
from lemur.policies.models import RotationPolicy
def get_sequence(name):
@ -102,10 +104,10 @@ class Certificate(db.Model):
san = Column(String(1024)) # TODO this should be migrated to boolean
rotation = Column(Boolean, default=False)
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"))
rotation_policy_id = Column(Integer, ForeignKey('rotation_policies.id'))
notifications = relationship('Notification', secondary=certificate_notification_associations, backref='certificate')
destinations = relationship('Destination', secondary=certificate_destination_associations, backref='certificate')
@ -120,6 +122,7 @@ class Certificate(db.Model):
logs = relationship('Log', backref='certificate')
endpoints = relationship('Endpoint', backref='certificate')
rotation_policy = relationship("RotationPolicy")
def __init__(self, **kwargs):
cert = lemur.common.utils.parse_certificate(kwargs['body'])
@ -134,7 +137,8 @@ class Certificate(db.Model):
if kwargs.get('name'):
self.name = get_or_increase_name(kwargs['name'])
else:
self.name = get_or_increase_name(defaults.certificate_name(self.cn, self.issuer, self.not_before, self.not_after, self.san))
self.name = get_or_increase_name(
defaults.certificate_name(self.cn, self.issuer, self.not_before, self.not_after, self.san))
self.owner = kwargs['owner']
self.body = kwargs['body'].strip()
@ -152,6 +156,7 @@ class Certificate(db.Model):
self.roles = list(set(kwargs.get('roles', [])))
self.replaces = kwargs.get('replaces', [])
self.rotation = kwargs.get('rotation')
self.rotation_policy = kwargs.get('rotation_policy')
self.signing_algorithm = defaults.signing_algorithm(cert)
self.bits = defaults.bitstrength(cert)
self.serial = defaults.serial(cert)
@ -240,6 +245,33 @@ class Certificate(db.Model):
else_=False
)
@hybrid_property
def in_rotation_window(self):
"""
Determines if a certificate is available for rotation based
on the rotation policy associated.
:return:
"""
now = arrow.utcnow()
end = now + timedelta(days=self.rotation_policy.days)
if self.not_after <= end:
return True
@in_rotation_window.expression
def in_rotation_window(cls):
"""
Determines if a certificate is available for rotation based
on the rotation policy associated.
:return:
"""
return case(
[
(extract('day', cls.not_after - func.now()) <= RotationPolicy.days, True)
],
else_=False
)
@property
def extensions(self):
# setup default values
@ -298,16 +330,6 @@ class Certificate(db.Model):
return return_extensions
def get_arn(self, account_number):
"""
Generate a valid AWS IAM arn
:rtype : str
:param account_number:
:return:
"""
return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name)
def __repr__(self):
return "Certificate(name={name})".format(name=self.name)
@ -329,7 +351,8 @@ def update_destinations(target, value, initiator):
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
except Exception as e:
current_app.logger.exception(e)
metrics.send('destination_upload_failure', 'counter', 1, metric_tags={'certificate': target.name, 'destination': value.label})
metrics.send('destination_upload_failure', 'counter', 1,
metric_tags={'certificate': target.name, 'destination': value.label})
@event.listens_for(Certificate.replaces, 'append')

View File

@ -9,8 +9,17 @@ from flask import current_app
from marshmallow import fields, validate, validates_schema, post_load, pre_load
from marshmallow.exceptions import ValidationError
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema, EndpointNestedOutputSchema
from lemur.schemas import (
AssociatedAuthoritySchema,
AssociatedDestinationSchema,
AssociatedCertificateSchema,
AssociatedNotificationSchema,
PluginInputSchema,
ExtensionSchema,
AssociatedRoleSchema,
EndpointNestedOutputSchema,
AssociatedRotationPolicySchema
)
from lemur.authorities.schemas import AuthorityNestedOutputSchema
from lemur.destinations.schemas import DestinationNestedOutputSchema
@ -18,6 +27,7 @@ from lemur.notifications.schemas import NotificationNestedOutputSchema
from lemur.roles.schemas import RoleNestedOutputSchema
from lemur.domains.schemas import DomainNestedOutputSchema
from lemur.users.schemas import UserNestedOutputSchema
from lemur.policies.schemas import RotationPolicyNestedOutputSchema
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.common import validators, missing
@ -63,6 +73,7 @@ class CertificateInputSchema(CertificateCreationSchema):
notify = fields.Boolean(default=True)
rotation = fields.Boolean()
rotation_policy = fields.Nested(AssociatedRotationPolicySchema, missing={'name': 'default'}, default={'name': 'default'})
# certificate body fields
organizational_unit = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'))
@ -133,6 +144,7 @@ class CertificateNestedOutputSchema(LemurOutputSchema):
rotation = fields.Boolean()
notify = fields.Boolean()
rotation_policy = fields.Nested(RotationPolicyNestedOutputSchema)
# Note aliasing is the first step in deprecating these fields.
cn = fields.String() # deprecated
@ -198,6 +210,7 @@ class CertificateOutputSchema(LemurOutputSchema):
roles = fields.Nested(RoleNestedOutputSchema, many=True)
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
replaced_by = fields.Nested(CertificateNestedOutputSchema, many=True, attribute='replaced')
rotation_policy = fields.Nested(RotationPolicyNestedOutputSchema)
class CertificateUploadInputSchema(CertificateCreationSchema):

View File

@ -6,7 +6,6 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import arrow
from datetime import timedelta
from flask import current_app
from sqlalchemy import func, or_, not_, cast, Boolean, Integer
@ -85,20 +84,15 @@ def get_all_pending_reissue():
"""
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
Must be X days from expiration, uses the certificates rotation
policy 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.endpoints.any())\
.filter(not_(Certificate.replaced.any()))\
.filter(Certificate.not_after <= end.format('YYYY-MM-DD')).all() # noqa
.filter(Certificate.in_rotation_window == True).all() # noqa
def find_duplicates(cert):

View File

@ -232,6 +232,8 @@ class CertificatesList(AuthenticatedResource):
"replaces": [{
"id": 1
}],
"rotation": True,
"rotationPolicy": {"name": "default"},
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
@ -241,18 +243,6 @@ class CertificatesList(AuthenticatedResource):
"san": null
}
:arg extensions: extensions to be used in the certificate
:arg description: description for new certificate
:arg owner: owner email
:arg validityStart: when the certificate should start being valid
:arg validityEnd: when the certificate should expire
:arg authority: authority that should issue the certificate
:arg country: country for the CSR
:arg state: state for the CSR
:arg location: location for the CSR
:arg organization: organization for CSR
:arg commonName: certificate common name
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
@ -356,6 +346,8 @@ class CertificatesUpload(AuthenticatedResource):
"name": "*.test.example.net"
}],
"replaces": [],
"rotation": True,
"rotationPolicy": {"name": "default"},
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
@ -365,11 +357,6 @@ class CertificatesUpload(AuthenticatedResource):
"san": null
}
:arg owner: owner email for certificate
:arg publicCert: valid PEM public key for certificate
:arg intermediateCert valid PEM intermediate key for certificate
:arg privateKey: valid PEM private key for certificate
:arg destinations: list of aws destinations to upload the certificate to
:reqheader Authorization: OAuth token to authenticate
:statuscode 403: unauthenticated
:statuscode 200: no error
@ -522,6 +509,8 @@ class Certificates(AuthenticatedResource):
"id": 1090,
"name": "*.test.example.net"
}],
"rotation": True,
"rotationPolicy": {"name": "default"},
"replaces": [],
"replaced": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
@ -616,6 +605,8 @@ class Certificates(AuthenticatedResource):
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"rotation": True,
"rotationPolicy": {"name": "default"},
"san": null
}
@ -722,6 +713,8 @@ class NotificationCertificatesList(AuthenticatedResource):
}],
"replaces": [],
"replaced": [],
"rotation": True,
"rotationPolicy": {"name": "default"},
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
@ -827,6 +820,8 @@ class CertificatesReplacementsList(AuthenticatedResource):
}],
"replaces": [],
"replaced": [],
"rotation": True,
"rotationPolicy": {"name": "default"},
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,