Wesley Hartford 437d918cf7 Fix textarea and validation on destination page
The destination configuration page did not previously support a textarea input as was supported on most other pages. The validation of string inputs was not being performed. This commit addresses both of those issues and corrects the validation expressions for the AWS and S3 destination plugins so that they continue to function. The SFTP destination plugin does not have any string validation. The Kubernetes plugin does not work at all as far as I can tell; there will be another PR in the coming days to address that.
2018-12-10 12:04:16 -08:00

333 lines
11 KiB
Python

"""
.. module: lemur.plugins.lemur_aws.plugin
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
Terraform example to setup the destination bucket:
resource "aws_s3_bucket" "certs_log_bucket" {
bucket = "certs-log-access-bucket"
acl = "log-delivery-write"
}
resource "aws_s3_bucket" "certs_lemur" {
bucket = "certs-lemur"
acl = "private"
logging {
target_bucket = "${aws_s3_bucket.certs_log_bucket.id}"
target_prefix = "log/lemur"
}
}
The IAM role Lemur is running as should have the following actions on the destination bucket:
"S3:PutObject",
"S3:PutObjectAcl"
The reader should have the following actions:
"s3:GetObject"
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
.. moduleauthor:: Harm Weites <harm@weites.com>
"""
from flask import current_app
from lemur.plugins import lemur_aws as aws
from lemur.plugins.bases import DestinationPlugin, ExportDestinationPlugin, SourcePlugin
from lemur.plugins.lemur_aws import iam, s3, elb, ec2
def get_region_from_dns(dns):
return dns.split('.')[-4]
def format_elb_cipher_policy_v2(policy):
"""
Attempts to format cipher policy information for elbv2 into a common format.
:param policy:
:return:
"""
ciphers = []
name = None
for descr in policy['SslPolicies']:
name = descr['Name']
for cipher in descr['Ciphers']:
ciphers.append(cipher['Name'])
return dict(name=name, ciphers=ciphers)
def format_elb_cipher_policy(policy):
"""
Attempts to format cipher policy information into a common format.
:param policy:
:return:
"""
ciphers = []
name = None
for descr in policy['PolicyDescriptions']:
for attr in descr['PolicyAttributeDescriptions']:
if attr['AttributeName'] == 'Reference-Security-Policy':
name = attr['AttributeValue']
continue
if attr['AttributeValue'] == 'true':
ciphers.append(attr['AttributeName'])
return dict(name=name, ciphers=ciphers)
def get_elb_endpoints(account_number, region, elb_dict):
"""
Retrieves endpoint information from elb response data.
:param account_number:
:param region:
:param elb_dict:
:return:
"""
endpoints = []
for listener in elb_dict['ListenerDescriptions']:
if not listener['Listener'].get('SSLCertificateId'):
continue
if listener['Listener']['SSLCertificateId'] == 'Invalid-Certificate':
continue
endpoint = dict(
name=elb_dict['LoadBalancerName'],
dnsname=elb_dict['DNSName'],
type='elb',
port=listener['Listener']['LoadBalancerPort'],
certificate_name=iam.get_name_from_arn(listener['Listener']['SSLCertificateId'])
)
if listener['PolicyNames']:
policy = elb.describe_load_balancer_policies(elb_dict['LoadBalancerName'], listener['PolicyNames'],
account_number=account_number, region=region)
endpoint['policy'] = format_elb_cipher_policy(policy)
current_app.logger.debug("Found new endpoint. Endpoint: {}".format(endpoint))
endpoints.append(endpoint)
return endpoints
def get_elb_endpoints_v2(account_number, region, elb_dict):
"""
Retrieves endpoint information from elbv2 response data.
:param account_number:
:param region:
:param elb_dict:
:return:
"""
endpoints = []
listeners = elb.describe_listeners_v2(account_number=account_number, region=region,
LoadBalancerArn=elb_dict['LoadBalancerArn'])
for listener in listeners['Listeners']:
if not listener.get('Certificates'):
continue
for certificate in listener['Certificates']:
endpoint = dict(
name=elb_dict['LoadBalancerName'],
dnsname=elb_dict['DNSName'],
type='elbv2',
port=listener['Port'],
certificate_name=iam.get_name_from_arn(certificate['CertificateArn'])
)
if listener['SslPolicy']:
policy = elb.describe_ssl_policies_v2([listener['SslPolicy']], account_number=account_number, region=region)
endpoint['policy'] = format_elb_cipher_policy_v2(policy)
endpoints.append(endpoint)
return endpoints
class AWSDestinationPlugin(DestinationPlugin):
title = 'AWS'
slug = 'aws-destination'
description = 'Allow the uploading of certificates to AWS IAM'
version = aws.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
options = [
{
'name': 'accountNumber',
'type': 'str',
'required': True,
'validation': '[0-9]{12}',
'helpMessage': 'Must be a valid AWS account number!',
},
{
'name': 'path',
'type': 'str',
'default': '/',
'helpMessage': 'Path to upload certificate.'
}
]
# 'elb': {
# 'name': {'type': 'name'},
# 'region': {'type': 'str'},
# 'port': {'type': 'int'}
# }
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
iam.upload_cert(name, body, private_key,
self.get_option('path', options),
cert_chain=cert_chain,
account_number=self.get_option('accountNumber', options))
def deploy(self, elb_name, account, region, certificate):
pass
class AWSSourcePlugin(SourcePlugin):
title = 'AWS'
slug = 'aws-source'
description = 'Discovers all SSL certificates and ELB endpoints in an AWS account'
version = aws.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
options = [
{
'name': 'accountNumber',
'type': 'str',
'required': True,
'validation': '/^[0-9]{12,12}$/',
'helpMessage': 'Must be a valid AWS account number!',
},
{
'name': 'regions',
'type': 'str',
'helpMessage': 'Comma separated list of regions to search in, if no region is specified we look in all regions.'
},
]
def get_certificates(self, options, **kwargs):
cert_data = iam.get_all_certificates(account_number=self.get_option('accountNumber', options))
return [dict(body=c['CertificateBody'], chain=c.get('CertificateChain'),
name=c['ServerCertificateMetadata']['ServerCertificateName']) for c in cert_data]
def get_endpoints(self, options, **kwargs):
endpoints = []
account_number = self.get_option('accountNumber', options)
regions = self.get_option('regions', options)
if not regions:
regions = ec2.get_regions(account_number=account_number)
else:
regions = regions.split(',')
for region in regions:
elbs = elb.get_all_elbs(account_number=account_number, region=region)
current_app.logger.info("Describing classic load balancers in {0}-{1}".format(account_number, region))
for e in elbs:
endpoints.extend(get_elb_endpoints(account_number, region, e))
# fetch advanced ELBs
elbs_v2 = elb.get_all_elbs_v2(account_number=account_number, region=region)
current_app.logger.info("Describing advanced load balancers in {0}-{1}".format(account_number, region))
for e in elbs_v2:
endpoints.extend(get_elb_endpoints_v2(account_number, region, e))
return endpoints
def update_endpoint(self, endpoint, certificate):
options = endpoint.source.options
account_number = self.get_option('accountNumber', options)
# relies on the fact that region is included in DNS name
region = get_region_from_dns(endpoint.dnsname)
arn = iam.create_arn_from_cert(account_number, region, certificate.name)
if endpoint.type == 'elbv2':
listener_arn = elb.get_listener_arn_from_endpoint(endpoint.name, endpoint.port,
account_number=account_number, region=region)
elb.attach_certificate_v2(listener_arn, endpoint.port, [{'CertificateArn': arn}],
account_number=account_number, region=region)
else:
elb.attach_certificate(endpoint.name, endpoint.port, arn, account_number=account_number, region=region)
def clean(self, certificate, options, **kwargs):
account_number = self.get_option('accountNumber', options)
iam.delete_cert(certificate.name, account_number=account_number)
class S3DestinationPlugin(ExportDestinationPlugin):
title = 'AWS-S3'
slug = 'aws-s3'
description = 'Allow the uploading of certificates to Amazon S3'
author = 'Mikhail Khodorovskiy, Harm Weites <harm@weites.com>'
author_url = 'https://github.com/Netflix/lemur'
additional_options = [
{
'name': 'bucket',
'type': 'str',
'required': True,
'validation': '[0-9a-z.-]{3,63}',
'helpMessage': 'Must be a valid S3 bucket name!',
},
{
'name': 'accountNumber',
'type': 'str',
'required': True,
'validation': '[0-9]{12}',
'helpMessage': 'A valid AWS account number with permission to access S3',
},
{
'name': 'region',
'type': 'str',
'default': 'us-east-1',
'required': False,
'helpMessage': 'Region bucket exists',
'available': ['us-east-1', 'us-west-2', 'eu-west-1']
},
{
'name': 'encrypt',
'type': 'bool',
'required': False,
'helpMessage': 'Enable server side encryption',
'default': True
},
{
'name': 'prefix',
'type': 'str',
'required': False,
'helpMessage': 'Must be a valid S3 object prefix!',
}
]
def __init__(self, *args, **kwargs):
super(S3DestinationPlugin, self).__init__(*args, **kwargs)
def upload(self, name, body, private_key, chain, options, **kwargs):
files = self.export(body, private_key, chain, options)
for ext, passphrase, data in files:
s3.put(
self.get_option('bucket', options),
self.get_option('region', options),
'{prefix}/{name}.{extension}'.format(
prefix=self.get_option('prefix', options),
name=name,
extension=ext),
data,
self.get_option('encrypt', options),
account_number=self.get_option('accountNumber', options)
)