Fixing an IAM syncing issue. Were duplicates were not properly sync'd… (#638)

* Fixing an IAM syncing issue. Were duplicates were not properly sync'd with Lemur. This resulted in a visibility gap. Even 'duplicates' need to sync'd to Lemur such that we can track rotation correctly. Failing on duplicates lead to missing those certificates and the endpoints onto which they were deployed. This commit removes the duplicate handling altogether.

* Fixing tests.
This commit is contained in:
kevgliss 2017-01-04 17:46:47 -08:00 committed by GitHub
parent e5dee2d7e6
commit 7aa5ba9c6b
6 changed files with 110 additions and 128 deletions

View File

@ -10,6 +10,7 @@ from flask import current_app
from retrying import retry from retrying import retry
from lemur.extensions import metrics
from lemur.exceptions import InvalidListener from lemur.exceptions import InvalidListener
from lemur.plugins.lemur_aws.sts import sts_client from lemur.plugins.lemur_aws.sts import sts_client
@ -22,11 +23,12 @@ def retry_throttled(exception):
""" """
if isinstance(exception, botocore.exceptions.ClientError): if isinstance(exception, botocore.exceptions.ClientError):
if exception.response['Error']['Code'] == 'LoadBalancerNotFound': if exception.response['Error']['Code'] == 'LoadBalancerNotFound':
return return False
if exception.response['Error']['Code'] == 'CertificateNotFound': if exception.response['Error']['Code'] == 'CertificateNotFound':
return return False
metrics.send('ec2_retry', 'counter', 1)
return True return True

View File

@ -10,7 +10,7 @@ import botocore
from retrying import retry from retrying import retry
from lemur.plugins.lemur_aws.sts import assume_service from lemur.extensions import metrics
from lemur.plugins.lemur_aws.sts import sts_client from lemur.plugins.lemur_aws.sts import sts_client
@ -23,6 +23,8 @@ def retry_throttled(exception):
if isinstance(exception, botocore.exceptions.ClientError): if isinstance(exception, botocore.exceptions.ClientError):
if exception.response['Error']['Code'] == 'NoSuchEntity': if exception.response['Error']['Code'] == 'NoSuchEntity':
return False return False
metrics.send('iam_retry', 'counter', 1)
return True return True
@ -36,69 +38,6 @@ def get_name_from_arn(arn):
return arn.split("/", 1)[1] return arn.split("/", 1)[1]
def upload_cert(account_number, name, body, private_key, cert_chain=None):
"""
Upload a certificate to AWS
:param account_number:
:param name:
:param private_key:
:param cert_chain:
:return:
"""
return assume_service(account_number, 'iam').upload_server_cert(name, str(body), str(private_key),
cert_chain=str(cert_chain))
@sts_client('iam')
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
def delete_cert(cert_name, **kwargs):
"""
Delete a certificate from AWS
:param cert_name:
:return:
"""
client = kwargs.pop('client')
client.delete_server_certificate(ServerCertificateName=cert_name)
def get_all_server_certs(account_number):
"""
Use STS to fetch all of the SSL certificates from a given account
:param account_number:
"""
marker = None
certs = []
while True:
response = assume_service(account_number, 'iam').get_all_server_certs(marker=marker)
result = response['list_server_certificates_response']['list_server_certificates_result']
for cert in result['server_certificate_metadata_list']:
certs.append(cert['arn'])
if result['is_truncated'] == 'true':
marker = result['marker']
else:
return certs
def get_cert_from_arn(arn):
"""
Retrieves an SSL certificate from a given ARN.
:param arn:
:return:
"""
name = get_name_from_arn(arn)
account_number = arn.split(":")[4]
name = name.split("/")[-1]
response = assume_service(account_number, 'iam').get_server_certificate(name.strip())
return digest_aws_cert_response(response)
def create_arn_from_cert(account_number, region, certificate_name): def create_arn_from_cert(account_number, region, certificate_name):
""" """
Create an ARN from a certificate. Create an ARN from a certificate.
@ -112,18 +51,92 @@ def create_arn_from_cert(account_number, region, certificate_name):
certificate_name=certificate_name) certificate_name=certificate_name)
def digest_aws_cert_response(response): @sts_client('iam')
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
def upload_cert(name, body, private_key, cert_chain=None, **kwargs):
""" """
Processes an AWS certifcate response and retrieves the certificate body and chain. Upload a certificate to AWS
:param response: :param name:
:param body:
:param private_key:
:param cert_chain:
:return: :return:
""" """
chain = None client = kwargs.pop('client')
cert = response['get_server_certificate_response']['get_server_certificate_result']['server_certificate'] try:
body = cert['certificate_body'] if cert_chain:
return client.upload_server_certificate(
ServerCertificateName=name,
CertificateBody=str(body),
PrivateKey=str(private_key),
CertificateChain=str(cert_chain)
)
else:
return client.upload_server_certificate(
ServerCertificateName=name,
CertificateBody=str(body),
PrivateKey=str(private_key)
)
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] != 'EntityAlreadyExists':
raise e
if 'certificate_chain' in cert:
chain = cert['certificate_chain']
return body, chain @sts_client('iam')
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
def delete_cert(cert_name, **kwargs):
"""
Delete a certificate from AWS
:param cert_name:
:return:
"""
client = kwargs.pop('client')
client.delete_server_certificate(ServerCertificateName=cert_name)
@sts_client('iam')
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
def get_certificate(name, **kwargs):
"""
Retrieves an SSL certificate.
:return:
"""
client = kwargs.pop('client')
return client.get_server_certificate(
ServerCertificateName=name
)['ServerCertificate']
@sts_client('iam')
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
def get_certificates(**kwargs):
"""
Fetches one page of certificate objects for a given account.
:param kwargs:
:return:
"""
client = kwargs.pop('client')
return client.list_server_certificates(**kwargs)
def get_all_certificates(**kwargs):
"""
Use STS to fetch all of the SSL certificates from a given account
"""
certificates = []
account_number = kwargs.get('account_number')
while True:
response = get_certificates(**kwargs)
metadata = response['ServerCertificateMetadataList']
for m in metadata:
certificates.append(get_certificate(m['ServerCertificateName'], account_number=account_number))
if not response.get('Marker'):
return certificates
else:
kwargs.update(dict(Marker=response['Marker']))

View File

@ -33,7 +33,6 @@
.. moduleauthor:: Harm Weites <harm@weites.com> .. moduleauthor:: Harm Weites <harm@weites.com>
""" """
from flask import current_app from flask import current_app
from boto.exception import BotoServerError
from lemur.plugins.bases import DestinationPlugin, SourcePlugin from lemur.plugins.bases import DestinationPlugin, SourcePlugin
from lemur.plugins.lemur_aws import iam, s3, elb, ec2 from lemur.plugins.lemur_aws import iam, s3, elb, ec2
@ -125,7 +124,7 @@ def get_elb_endpoints_v2(account_number, region, elb_dict):
endpoints = [] endpoints = []
listeners = elb.describe_listeners_v2(account_number=account_number, region=region, LoadBalancerArn=elb_dict['LoadBalancerArn']) listeners = elb.describe_listeners_v2(account_number=account_number, region=region, LoadBalancerArn=elb_dict['LoadBalancerArn'])
for listener in listeners['Listeners']: for listener in listeners['Listeners']:
if not listener['Certificates']: if not listener.get('Certificates'):
continue continue
for certificate in listener['Certificates']: for certificate in listener['Certificates']:
@ -172,12 +171,9 @@ class AWSDestinationPlugin(DestinationPlugin):
# } # }
def upload(self, name, body, private_key, cert_chain, options, **kwargs): def upload(self, name, body, private_key, cert_chain, options, **kwargs):
try: iam.upload_cert(name, body, private_key,
iam.upload_cert(self.get_option('accountNumber', options), name, body, private_key, cert_chain=cert_chain,
cert_chain=cert_chain) account_number=self.get_option('accountNumber', options))
except BotoServerError as e:
if e.error_code != 'EntityAlreadyExists':
raise Exception(e)
def deploy(self, elb_name, account, region, certificate): def deploy(self, elb_name, account, region, certificate):
pass pass
@ -208,18 +204,8 @@ class AWSSourcePlugin(SourcePlugin):
] ]
def get_certificates(self, options, **kwargs): def get_certificates(self, options, **kwargs):
certs = [] cert_data = iam.get_all_certificates(account_number=self.get_option('accountNumber', options))
arns = iam.get_all_server_certs(self.get_option('accountNumber', options)) return [dict(body=c['CertificateBody'], chain=c.get('CertificateChain'), name=c['ServerCertificateMetadata']['ServerCertificateName']) for c in cert_data]
for arn in arns:
cert_body, cert_chain = iam.get_cert_from_arn(arn)
cert_name = iam.get_name_from_arn(arn)
cert = dict(
body=cert_body,
chain=cert_chain,
name=cert_name
)
certs.append(cert)
return certs
def get_endpoints(self, options, **kwargs): def get_endpoints(self, options, **kwargs):
endpoints = [] endpoints = []

View File

@ -14,16 +14,7 @@ def test_get_name_from_arn():
@mock_sts() @mock_sts()
@mock_iam() @mock_iam()
def test_get_all_server_certs(app): def test_get_all_server_certs(app):
from lemur.plugins.lemur_aws.iam import upload_cert, get_all_server_certs from lemur.plugins.lemur_aws.iam import upload_cert, get_all_certificates
upload_cert('123456789012', 'testCert', EXTERNAL_VALID_STR, PRIVATE_KEY_STR) upload_cert('123456789012', 'testCert', EXTERNAL_VALID_STR, PRIVATE_KEY_STR)
certs = get_all_server_certs('123456789012') certs = get_all_certificates('123456789012')
assert len(certs) == 1 assert len(certs) == 1
@mock_sts()
@mock_iam()
def test_get_cert_from_arn(app):
from lemur.plugins.lemur_aws.iam import upload_cert, get_cert_from_arn
upload_cert('123456789012', 'testCert', EXTERNAL_VALID_STR, PRIVATE_KEY_STR)
body, chain = get_cert_from_arn('arn:aws:iam::123456789012:server-certificate/testCert')
assert body.replace('\n', '') == EXTERNAL_VALID_STR.replace('\n', '')

View File

@ -72,20 +72,11 @@ def sync_endpoints(source):
for endpoint in endpoints: for endpoint in endpoints:
exists = endpoint_service.get_by_dnsname(endpoint['dnsname']) exists = endpoint_service.get_by_dnsname(endpoint['dnsname'])
certificate_name = endpoint.pop('certificate_name', None) cert = certificate_service.get_by_name(endpoint['certificate_name'])
certificate = endpoint.pop('certificate', None)
if certificate_name:
cert = certificate_service.get_by_name(certificate_name)
elif certificate:
cert = certificate_service.find_duplicates(certificate)
if not cert:
cert = certificate_service.import_certificate(**certificate)
if not cert: if not cert:
current_app.logger.error( current_app.logger.error(
"Unable to find associated certificate, be sure that certificates are sync'ed before endpoints") "Certificate Not Found. Name: {0} Endpoint: {1}".format(endpoint['certificate_name'], endpoint['name']))
continue continue
endpoint['certificate'] = cert endpoint['certificate'] = cert
@ -101,10 +92,12 @@ def sync_endpoints(source):
endpoint['source'] = source endpoint['source'] = source
if not exists: if not exists:
current_app.logger.debug("Endpoint Created: Name: {name}".format(name=endpoint['name']))
endpoint_service.create(**endpoint) endpoint_service.create(**endpoint)
new += 1 new += 1
else: else:
current_app.logger.debug("Endpoint Updated: Name: {name}".format(name=endpoint['name']))
endpoint_service.update(exists.id, **endpoint) endpoint_service.update(exists.id, **endpoint)
updated += 1 updated += 1
@ -119,25 +112,22 @@ def sync_certificates(source, user):
certificates = s.get_certificates(source.options) certificates = s.get_certificates(source.options)
for certificate in certificates: for certificate in certificates:
exists = certificate_service.find_duplicates(certificate) exists = certificate_service.get_by_name(certificate['name'])
certificate['owner'] = user.email certificate['owner'] = user.email
certificate['creator'] = user certificate['creator'] = user
if not exists: if not exists:
current_app.logger.debug("Creating Certificate. Name: {name}".format(name=certificate['name']))
certificate_create(certificate, source) certificate_create(certificate, source)
new += 1 new += 1
# check to make sure that existing certificates have the current source associated with it
elif len(exists) == 1:
certificate_update(exists[0], source)
updated += 1
else: else:
current_app.logger.warning( current_app.logger.debug("Updating Certificate. Name: {name}".format(name=certificate['name']))
"Multiple certificates found, attempt to deduplicate the following certificates: {0}".format( certificate_update(exists, source)
",".join([x.name for x in exists]) updated += 1
)
) assert len(certificates) == new + updated
return new, updated return new, updated

View File

@ -218,7 +218,7 @@ def test_certificate_valid_years(client, authority):
'owner': 'jim@example.com', 'owner': 'jim@example.com',
'authority': {'id': authority.id}, 'authority': {'id': authority.id},
'description': 'testtestest', 'description': 'testtestest',
'validityYears': 3 'validityYears': 2
} }
data, errors = CertificateInputSchema().load(input_data) data, errors = CertificateInputSchema().load(input_data)