From cd65a36437e05d6cd4f5e29b76c6f5e76567beb1 Mon Sep 17 00:00:00 2001 From: alwaysjolley Date: Mon, 25 Feb 2019 09:42:07 -0500 Subject: [PATCH] - support multiple bundle configuration, nginx, apache, cert only - update vault destination to support multi cert under one object - added san list as key value - read and update object with new keys, keeping other keys, allowing us to keep an iterable list of keys in an object for deploying multiple certs to a single node --- .gitignore | 5 ++ lemur/plugins/lemur_vault/plugin.py | 81 +++++++++++++++++++++++++---- requirements-dev.txt | 6 +-- requirements-docs.txt | 10 ++-- requirements-tests.txt | 16 +++--- requirements.txt | 10 ++-- 6 files changed, 98 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 97af00ca..72e85f26 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,11 @@ package-lock.json /lemur/static/dist/ /lemur/static/app/vendor/ /wheelhouse +/lemur/lib +/lemur/bin +/lemur/lib64 +/lemur/include + docs/_build .editorconfig .idea diff --git a/lemur/plugins/lemur_vault/plugin.py b/lemur/plugins/lemur_vault/plugin.py index 505170ad..58a9e601 100644 --- a/lemur/plugins/lemur_vault/plugin.py +++ b/lemur/plugins/lemur_vault/plugin.py @@ -18,6 +18,10 @@ from lemur.common.defaults import common_name from lemur.common.utils import parse_certificate from lemur.plugins.bases import DestinationPlugin +from cryptography import x509 +from cryptography.hazmat.backends import default_backend + + class VaultDestinationPlugin(DestinationPlugin): """Hashicorp Vault Destination plugin for Lemur""" title = 'Vault' @@ -48,6 +52,25 @@ class VaultDestinationPlugin(DestinationPlugin): 'required': True, 'validation': '^https?://[a-zA-Z0-9.-]+(?::[0-9]+)?$', 'helpMessage': 'Must be a valid Vault server url' + }, + { + 'name': 'bundleChain', + 'type': 'select', + 'value': 'cert only', + 'available': [ + 'Nginx', + 'Apache', + 'no chain' + ], + 'required': True, + 'helpMessage': 'Bundle the chain into the certificate' + }, + { + 'name': 'objectName', + 'type': 'str', + 'required': False, + 'validation': '[0-9a-zA-Z:_-]+', + 'helpMessage': 'Name to bundle certs under, if blank use cn' } ] @@ -62,24 +85,64 @@ class VaultDestinationPlugin(DestinationPlugin): :param cert_chain: :return: """ - cn = common_name(parse_certificate(body)) - data = {} - #current_app.logger.warning("Cert body content: {0}".format(body)) + cname = common_name(parse_certificate(body)) + secret = {'data':{}} + key_name = '{0}.key'.format(cname) + cert_name = '{0}.crt'.format(cname) + chain_name = '{0}.chain'.format(cname) + sans_name = '{0}.san'.format(cname) token = current_app.config.get('VAULT_TOKEN') mount = self.get_option('vaultMount', options) - path = '{0}/{1}'.format(self.get_option('vaultPath', options),cn) + path = self.get_option('vaultPath', options) url = self.get_option('vaultUrl', options) + bundle = self.get_option('bundleChain', options) + obj_name = self.get_option('objectName', options) client = hvac.Client(url=url, token=token) + if obj_name: + path = '{0}/{1}'.format(path, obj_name) + else: + path = '{0}/{1}'.format(path, cname) - data['cert'] = cert_chain - data['key'] = private_key + secret = get_secret(url, token, mount, path) + - ## upload certificate and key + if bundle == 'Nginx' and cert_chain: + secret['data'][cert_name] = '{0}\n{1}'.format(body, cert_chain) + elif bundle == 'Apache' and cert_chain: + secret['data'][cert_name] = body + secret['data'][chain_name] = cert_chain + else: + secret['data'][cert_name] = body + secret['data'][key_name] = private_key + san_list = get_san_list(body) + if isinstance(san_list, list): + secret['data'][sans_name] = san_list try: - client.secrets.kv.v1.create_or_update_secret(path=path, mount_point=mount, secret=data) - except Exception as err: + client.secrets.kv.v1.create_or_update_secret( + path=path, mount_point=mount, secret=secret['data']) + except ConnectionError as err: current_app.logger.exception( "Exception uploading secret to vault: {0}".format(err), exc_info=True) + +def get_san_list(body): + """ parse certificate for SAN names and return list, return empty list on error """ + try: + byte_body = body.encode('utf-8') + cert = x509.load_pem_x509_certificate(byte_body, default_backend()) + ext = cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME) + return ext.value.get_values_for_type(x509.DNSName) + except: + pass + return [] + +def get_secret(url, token, mount, path): + result = {'data': {}} + try: + client = hvac.Client(url=url, token=token) + result = client.secrets.kv.v1.read_secret(path=path, mount_point=mount) + except: + pass + return result diff --git a/requirements-dev.txt b/requirements-dev.txt index 6e2a3fb9..fd487bd7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --no-index --output-file requirements-dev.txt requirements-dev.in +# pip-compile --output-file requirements-dev.txt requirements-dev.in -U --no-index # aspy.yaml==1.1.2 # via pre-commit bleach==3.1.0 # via readme-renderer @@ -11,7 +11,7 @@ cfgv==1.4.0 # via pre-commit chardet==3.0.4 # via requests docutils==0.14 # via readme-renderer flake8==3.5.0 -identify==1.2.2 # via pre-commit +identify==1.3.0 # via pre-commit idna==2.8 # via requests importlib-metadata==0.8 # via pre-commit importlib-resources==1.0.2 # via pre-commit @@ -32,6 +32,6 @@ toml==0.10.0 # via pre-commit tqdm==4.31.1 # via twine twine==1.13.0 urllib3==1.24.1 # via requests -virtualenv==16.4.0 # via pre-commit +virtualenv==16.4.1 # via pre-commit webencodings==0.5.1 # via bleach zipp==0.3.3 # via importlib-metadata diff --git a/requirements-docs.txt b/requirements-docs.txt index e9dd92cb..8b9c3f2b 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --no-index --output-file requirements-docs.txt requirements-docs.in +# pip-compile --output-file requirements-docs.txt requirements-docs.in -U --no-index # acme==0.31.0 alabaster==0.7.12 # via sphinx @@ -17,8 +17,8 @@ babel==2.6.0 # via sphinx bcrypt==3.1.6 billiard==3.5.0.5 blinker==1.4 -boto3==1.9.98 -botocore==1.12.98 +boto3==1.9.101 +botocore==1.12.101 celery[redis]==4.2.1 certifi==2018.11.29 cffi==1.12.1 @@ -47,13 +47,13 @@ imagesize==1.1.0 # via sphinx inflection==0.3.1 itsdangerous==1.1.0 jinja2==2.10 -jmespath==0.9.3 +jmespath==0.9.4 josepy==1.1.0 jsonlines==1.2.0 kombu==4.3.0 lockfile==0.12.2 mako==1.0.7 -markupsafe==1.1.0 +markupsafe==1.1.1 marshmallow-sqlalchemy==0.16.0 marshmallow==2.18.1 mock==2.0.0 diff --git a/requirements-tests.txt b/requirements-tests.txt index 1bb8ba03..1c3a4969 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,15 +2,15 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --no-index --output-file requirements-tests.txt requirements-tests.in +# pip-compile --output-file requirements-tests.txt requirements-tests.in -U --no-index # asn1crypto==0.24.0 # via cryptography atomicwrites==1.3.0 # via pytest attrs==18.2.0 # via pytest aws-xray-sdk==0.95 # via moto -boto3==1.9.98 # via moto +boto3==1.9.101 # via moto boto==2.49.0 # via moto -botocore==1.12.98 # via boto3, moto, s3transfer +botocore==1.12.101 # via boto3, moto, s3transfer certifi==2018.11.29 # via requests cffi==1.12.1 # via cryptography chardet==3.0.4 # via requests @@ -29,17 +29,17 @@ future==0.17.1 # via python-jose idna==2.8 # via requests itsdangerous==1.1.0 # via flask jinja2==2.10 # via flask, moto -jmespath==0.9.3 # via boto3, botocore +jmespath==0.9.4 # via boto3, botocore jsondiff==1.1.1 # via moto jsonpickle==1.1 # via aws-xray-sdk -markupsafe==1.1.0 # via jinja2 +markupsafe==1.1.1 # via jinja2 mock==2.0.0 # via moto more-itertools==6.0.0 # via pytest moto==1.3.7 nose==1.3.7 pbr==5.1.2 # via mock -pluggy==0.8.1 # via pytest -py==1.7.0 # via pytest +pluggy==0.9.0 # via pytest +py==1.8.0 # via pytest pyaml==18.11.0 # via moto pycparser==2.19 # via cffi pycryptodome==3.7.3 # via python-jose @@ -58,7 +58,7 @@ s3transfer==0.2.0 # via boto3 six==1.12.0 # via cryptography, docker, docker-pycreds, faker, freezegun, mock, moto, pytest, python-dateutil, python-jose, requests-mock, responses, websocket-client text-unidecode==1.2 # via faker urllib3==1.24.1 # via botocore, requests -websocket-client==0.54.0 # via docker +websocket-client==0.55.0 # via docker werkzeug==0.14.1 # via flask, moto, pytest-flask wrapt==1.11.1 # via aws-xray-sdk xmltodict==0.12.0 # via moto diff --git a/requirements.txt b/requirements.txt index edd56b09..a8615094 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --no-index --output-file requirements.txt requirements.in +# pip-compile --output-file requirements.txt requirements.in -U --no-index # acme==0.31.0 alembic-autogenerate-enums==0.0.2 @@ -15,8 +15,8 @@ asyncpool==1.0 bcrypt==3.1.6 # via flask-bcrypt, paramiko billiard==3.5.0.5 # via celery blinker==1.4 # via flask-mail, flask-principal, raven -boto3==1.9.98 -botocore==1.12.98 +boto3==1.9.101 +botocore==1.12.101 celery[redis]==4.2.1 certifi==2018.11.29 cffi==1.12.1 # via bcrypt, cryptography, pynacl @@ -44,13 +44,13 @@ idna==2.8 # via requests inflection==0.3.1 itsdangerous==1.1.0 # via flask jinja2==2.10 -jmespath==0.9.3 # via boto3, botocore +jmespath==0.9.4 # via boto3, botocore josepy==1.1.0 # via acme jsonlines==1.2.0 # via cloudflare kombu==4.3.0 # via celery lockfile==0.12.2 mako==1.0.7 # via alembic -markupsafe==1.1.0 # via jinja2, mako +markupsafe==1.1.1 # via jinja2, mako marshmallow-sqlalchemy==0.16.0 marshmallow==2.18.1 mock==2.0.0 # via acme