diff --git a/docs/administration/index.rst b/docs/administration/index.rst index 9c7fb73b..938a7eb0 100644 --- a/docs/administration/index.rst +++ b/docs/administration/index.rst @@ -209,8 +209,9 @@ Lemur supports sending certification expiration notifications through SES and SM Authority Options ----------------- -Authorities will each have their own configuration options. There are currently two plugins bundled with Lemur, -Verisign/Symantec and CloudCA +Authorities will each have their own configuration options. There are currently just one plugin bundled with Lemur, +Verisign/Symantec. Additional plugins may define additional options. Refer to the plugins own documentation +for those plugins. .. data:: VERISIGN_URL :noindex: @@ -253,23 +254,6 @@ Verisign/Symantec and CloudCA This is the root to be used for your CA chain -.. data:: CLOUDCA_URL - :noindex: - - This is the URL for CLoudCA API - - -.. data:: CLOUDCA_PEM_PATH - :noindex: - - This is the path to the mutual SSL Certificate use for communicating with CLOUDCA - -.. data:: CLOUDCA_BUNDLE - :noindex: - - This is the path to the CLOUDCA certificate bundle - - Authentication -------------- Lemur currently supports Basic Authentication and Ping OAuth2 out of the box, additional flows can be added relatively easily. diff --git a/docs/developer/internals/lemur.plugins.lemur_cloudca.rst b/docs/developer/internals/lemur.plugins.lemur_cloudca.rst deleted file mode 100644 index 22d997a5..00000000 --- a/docs/developer/internals/lemur.plugins.lemur_cloudca.rst +++ /dev/null @@ -1,20 +0,0 @@ -lemur_cloudca Package -===================== - -:mod:`lemur_cloudca` Package ----------------------------- - -.. automodule:: lemur.plugins.lemur_cloudca - :noindex: - :members: - :undoc-members: - :show-inheritance: - -:mod:`plugin` Module --------------------- - -.. automodule:: lemur.plugins.lemur_cloudca.plugin - :noindex: - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/developer/plugins/index.rst b/docs/developer/plugins/index.rst index f8ce90e9..647194da 100644 --- a/docs/developer/plugins/index.rst +++ b/docs/developer/plugins/index.rst @@ -91,7 +91,7 @@ Issuer Issuer plugins are used when you have an external service that creates certificates or authorities. In the simple case the third party only issues certificates (Verisign, DigiCert, etc.). -If you have a third party or internal service that creates authorities (CloudCA, EJBCA, etc.), Lemur has you covered, +If you have a third party or internal service that creates authorities (EJBCA, etc.), Lemur has you covered, it can treat any issuer plugin as both a source of creating new certificates as well as new authorities. diff --git a/lemur/manage.py b/lemur/manage.py index e7521038..d66d004c 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -112,13 +112,6 @@ SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur' # These will be dependent on which 3rd party that Lemur is # configured to use. -# CLOUDCA_URL = '' -# CLOUDCA_PEM_PATH = '' -# CLOUDCA_BUNDLE = '' - -# number of years to issue if not specified -# CLOUDCA_DEFAULT_VALIDITY = 2 - # VERISIGN_URL = '' # VERISIGN_PEM_PATH = '' # VERISIGN_FIRST_NAME = '' diff --git a/lemur/plugins/lemur_cloudca/__init__.py b/lemur/plugins/lemur_cloudca/__init__.py deleted file mode 100644 index 8ce5a7f3..00000000 --- a/lemur/plugins/lemur_cloudca/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -try: - VERSION = __import__('pkg_resources') \ - .get_distribution(__name__).version -except Exception as e: - VERSION = 'unknown' diff --git a/lemur/plugins/lemur_cloudca/plugin.py b/lemur/plugins/lemur_cloudca/plugin.py deleted file mode 100644 index 2fd6b8a7..00000000 --- a/lemur/plugins/lemur_cloudca/plugin.py +++ /dev/null @@ -1,364 +0,0 @@ -""" -.. module: lemur.common.services.issuers.plugins.cloudca - :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. - -.. moduleauthor:: Kevin Glisson - -""" -import re -import ssl -import base64 -from json import dumps - -import arrow -import requests -from requests.adapters import HTTPAdapter -from requests.exceptions import ConnectionError - -from flask import current_app - -from lemur.exceptions import LemurException -from lemur.plugins.bases import IssuerPlugin, SourcePlugin -from lemur.plugins import lemur_cloudca as cloudca - -from lemur.authorities import service as authority_service - - -class CloudCAException(LemurException): - def __init__(self, message): - self.message = message - current_app.logger.error(self) - - def __str__(self): - return repr("CloudCA request failed: {0}".format(self.message)) - - -class CloudCAHostNameCheckingAdapter(HTTPAdapter): - def cert_verify(self, conn, url, verify, cert): - super(CloudCAHostNameCheckingAdapter, self).cert_verify(conn, url, verify, cert) - conn.assert_hostname = False - - -def remove_none(options): - """ - Simple function that traverse the options and removed any None items - CloudCA really dislikes null values. - - :param options: - :return: - """ - new_dict = {} - for k, v in options.items(): - if v: - new_dict[k] = v - - # this is super hacky and gross, cloudca doesn't like null values - if new_dict.get('extensions'): - if len(new_dict['extensions']['subAltNames']['names']) == 0: - del new_dict['extensions']['subAltNames'] - - return new_dict - - -def get_default_issuance(options): - """ - Gets the default time range for certificates - - :param options: - :return: - """ - if not options.get('validityStart') and not options.get('validityEnd'): - start = arrow.utcnow() - options['validityStart'] = start.floor('second').isoformat() - options['validityEnd'] = start.replace(years=current_app.config.get('CLOUDCA_DEFAULT_VALIDITY'))\ - .ceil('second').isoformat() - return options - - -def convert_to_pem(der): - """ - Converts DER to PEM Lemur uses PEM internally - - :param der: - :return: - """ - decoded = base64.b64decode(der) - return ssl.DER_cert_to_PEM_cert(decoded) - - -def convert_date_to_utc_time(date): - """ - Converts a python `datetime` object to the current date + current time in UTC. - - :param date: - :return: - """ - d = arrow.get(date) - return arrow.utcnow().replace(year=d.naive.year).replace(month=d.naive.month).replace(day=d.naive.day)\ - .replace(microsecond=0) - - -def process_response(response): - """ - Helper function that processes responses from CloudCA. - - :param response: - :return: :raise CloudCAException: - """ - if response.status_code == 200: - res = response.json() - if res['returnValue'] != 'success': - current_app.logger.debug(res) - if res.get('data'): - raise CloudCAException(" ".join([res['returnMessage'], res['data']['dryRunResultMessage']])) - else: - raise CloudCAException(res['returnMessage']) - else: - raise CloudCAException("There was an error with your request: {0}".format(response.status_code)) - - return response.json() - - -def get_auth_data(ca_name): - """ - Creates the authentication record needed to authenticate a user request to CloudCA. - - :param ca_name: - :return: :raise CloudCAException: - """ - role = authority_service.get_authority_role(ca_name) - if role: - return { - "authInfo": { - "credType": "password", - "credentials": { - "username": role.username, - "password": role.password # we only decrypt when we need to - } - - } - } - - raise CloudCAException("You do not have the required role to issue certificates from {0}".format(ca_name)) - - -class CloudCA(object): - def __init__(self, *args, **kwargs): - self.session = requests.Session() - self.session.mount('https://', CloudCAHostNameCheckingAdapter()) - self.url = current_app.config.get('CLOUDCA_URL') - - if current_app.config.get('CLOUDCA_PEM_PATH') and current_app.config.get('CLOUDCA_BUNDLE'): - self.session.cert = current_app.config.get('CLOUDCA_PEM_PATH') - self.ca_bundle = current_app.config.get('CLOUDCA_BUNDLE') - else: - current_app.logger.warning( - "No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA" - ) - - super(CloudCA, self).__init__(*args, **kwargs) - - def post(self, endpoint, data): - """ - HTTP POST to CloudCA - - :param endpoint: - :param data: - :return: - """ - data = dumps(dict(data.items() + get_auth_data(data['caName']).items())) - - # we set a low timeout, if cloudca is down it shouldn't bring down - # lemur - try: - response = self.session.post(self.url + endpoint, data=data, timeout=10, verify=self.ca_bundle) - except ConnectionError: - raise Exception("Could not talk to CloudCA, is it up?") - - return process_response(response) - - def get(self, endpoint): - """ - HTTP GET to CloudCA - - :param endpoint: - :return: - """ - try: - response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle) - except ConnectionError: - raise Exception("Could not talk to CloudCA, is it up?") - - return process_response(response) - - def random(self, length=10): - """ - Uses CloudCA as a decent source of randomness. - - :param length: - :return: - """ - endpoint = '/v1/random/{0}'.format(length) - response = self.session.get(self.url + endpoint, verify=self.ca_bundle) - return response - - def get_authorities(self): - """ - Retrieves authorities that were made outside of Lemur. - - :return: - """ - endpoint = '{0}/listCAs'.format(current_app.config.get('CLOUDCA_API_ENDPOINT')) - authorities = [] - for ca in self.get(endpoint)['data']['caList']: - try: - authorities.append(ca['caName']) - except AttributeError: - current_app.logger.error("No authority has been defined for {}".format(ca['caName'])) - - return authorities - - -class CloudCAIssuerPlugin(IssuerPlugin, CloudCA): - title = 'CloudCA' - slug = 'cloudca-issuer' - description = 'Enables the creation of certificates from the cloudca API.' - version = cloudca.VERSION - - author = 'Kevin Glisson' - author_url = 'https://github.com/netflix/lemur' - - def create_authority(self, options): - """ - Creates a new certificate authority - - :param options: - :return: - """ - # this is weird and I don't like it - endpoint = '{0}/createCA'.format(current_app.config.get('CLOUDCA_API_ENDPOINT')) - options['caDN']['email'] = options['ownerEmail'] - - if options['caType'] == 'subca': - options = dict(options.items() + self.auth_data(options['caParent']).items()) - - options['validityStart'] = convert_date_to_utc_time(options['validityStart']).isoformat() - options['validityEnd'] = convert_date_to_utc_time(options['validityEnd']).isoformat() - options['description'] = re.sub(r'[^a-zA-Z0-9]', '', options['caDescription']) - - try: - response = self.session.post(self.url + endpoint, data=dumps(remove_none(options)), timeout=10, - verify=self.ca_bundle) - except ConnectionError: - raise Exception("Could not communicate with CloudCA, is it up?") - - json = process_response(response) - roles = [] - - for cred in json['data']['authInfo']: - role = { - 'username': cred['credentials']['username'], - 'password': cred['credentials']['password'], - 'name': "_".join([options['caName'], cred['credentials']['username']]) - } - roles.append(role) - - if options['caType'] == 'subca': - cert = convert_to_pem(json['data']['certificate']) - else: - cert = convert_to_pem(json['data']['rootCertificate']) - - intermediates = [] - for i in json['data']['intermediateCertificates']: - intermediates.append(convert_to_pem(i)) - - return cert, "".join(intermediates), roles, - - def create_certificate(self, csr, options): - """ - Creates a new certificate from cloudca - - If no start and end date are specified the default issue range - will be used. - - :param csr: - :param options: - """ - endpoint = '{0}/enroll'.format(current_app.config.get('CLOUDCA_API_ENDPOINT')) - # lets default to two years if it's not specified - # we do some last minute data massaging - options = get_default_issuance(options) - - cloudca_options = { - 'extensions': options['extensions'], - 'validityStart': convert_date_to_utc_time(options['validityStart']).isoformat(), - 'validityEnd': convert_date_to_utc_time(options['validityEnd']).isoformat(), - 'creator': options['creator'], - 'ownerEmail': options['owner'], - 'caName': options['authority'].name, - 'csr': csr, - 'comment': re.sub(r'[^a-zA-Z0-9]', '', options['description']) - } - - response = self.post(endpoint, remove_none(cloudca_options)) - - # we return a concatenated list of intermediate because that is what aws - # expects - cert = convert_to_pem(response['data']['certificate']) - - intermediates = [convert_to_pem(response['data']['rootCertificate'])] - for i in response['data']['intermediateCertificates']: - intermediates.append(convert_to_pem(i)) - - return cert, "".join(intermediates), - - -class CloudCASourcePlugin(SourcePlugin, CloudCA): - title = 'CloudCA' - slug = 'cloudca-source' - description = 'Discovers all SSL certificates in CloudCA' - version = cloudca.VERSION - - author = 'Kevin Glisson' - author_url = 'https://github.com/netflix/lemur' - - options = { - 'pollRate': {'type': 'int', 'default': '60'} - } - - def get_certificates(self, options, **kwargs): - certs = [] - for authority in self.get_authorities(): - certs += self.get_cert(ca_name=authority) - return certs - - def get_cert(self, ca_name=None, cert_handle=None): - """ - Returns a given cert from CloudCA. - - :param ca_name: - :param cert_handle: - :return: - """ - endpoint = '{0}/getCert'.format(current_app.config.get('CLOUDCA_API_ENDPOINT')) - response = self.session.post(self.url + endpoint, data=dumps({'caName': ca_name}), timeout=10, - verify=self.ca_bundle) - raw = process_response(response) - - certs = [] - for c in raw['data']['certList']: - cert = convert_to_pem(c['certValue']) - - intermediates = [] - for i in c['intermediateCertificates']: - intermediates.append(convert_to_pem(i)) - - certs.append({ - 'public_certificate': cert, - 'intermediate_certificate': "\n".join(intermediates), - 'owner': c['ownerEmail'] - }) - - return certs diff --git a/setup.py b/setup.py index 72508dae..29e416e5 100644 --- a/setup.py +++ b/setup.py @@ -151,8 +151,6 @@ setup( ], 'lemur.plugins': [ 'verisign_issuer = lemur.plugins.lemur_verisign.plugin:VerisignIssuerPlugin', - 'cloudca_issuer = lemur.plugins.lemur_cloudca.plugin:CloudCAIssuerPlugin', - 'cloudca_source = lemur.plugins.lemur_cloudca.plugin:CloudCASourcePlugin', 'aws_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin', 'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin', 'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin',