├── certbot_vault ├── __init__.py └── plugin.py ├── readme.md ├── LICENSE ├── setup.py └── .gitignore /certbot_vault/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Certbot-Vault 2 | Certbot plugin for Vault 3 | 4 | Installation guide: 5 | * Install [Vault](https://www.vaultproject.io/) 6 | * Get a Vault token 7 | * Deploy latest version of [Certbot](https://github.com/certbot/certbot) 8 | * Install certbot-vault `pip install certbot-vault` 9 | 10 | ## Use cases: 11 | * Get/Renew and store new certificate in vault 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Csergő Bálint 4 | Copyright (c) 2020 The VitalVas 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools import find_packages 3 | 4 | 5 | setup( 6 | name='certbot-vault', # Required 7 | version='0.3.8', # Required 8 | description='Certbot plugin to store certificates in Hashicorp Vault', 9 | url='https://github.com/deathowl/certbot-vault-plugin', # Optional 10 | 11 | 12 | author='Balint Csergo', # Optional 13 | author_email='', # Optional 14 | 15 | classifiers=[ 16 | 'Development Status :: 4 - Beta', 17 | 'Environment :: Plugins', 18 | 'Operating System :: POSIX :: Linux', 19 | 'Programming Language :: Python :: 2', 20 | 'Programming Language :: Python :: 2.7', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.6', 23 | 'Programming Language :: Python :: 3.7', 24 | 'Programming Language :: Python :: 3.8', 25 | 'Topic :: Internet :: WWW/HTTP', 26 | 'Topic :: Security', 27 | 'Topic :: System :: Installation/Setup', 28 | 'Topic :: System :: Networking', 29 | 'Topic :: System :: Systems Administration', 30 | 'Topic :: Utilities' 31 | ], 32 | 33 | packages=find_packages(), # Required 34 | include_package_data=True, 35 | 36 | install_requires=[ 37 | 'acme>=0.22.0', 38 | 'certbot>=0.22.0', 39 | 'PyOpenSSL', 40 | 'setuptools', 41 | 'zope.component', 42 | 'zope.event', 43 | 'zope.interface', 44 | 'hvac' 45 | ], 46 | 47 | entry_points={ 48 | 'certbot.plugins': [ 49 | 'vault = certbot_vault.plugin:VaultInstaller', 50 | ], 51 | } 52 | ) 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | .idea 103 | .pytest_cache/ 104 | 105 | # idea 106 | *.iml 107 | .idea/ 108 | -------------------------------------------------------------------------------- /certbot_vault/plugin.py: -------------------------------------------------------------------------------- 1 | """Vault Let's Encrypt installer plugin.""" 2 | 3 | from __future__ import print_function 4 | 5 | import os 6 | import logging 7 | import hvac 8 | 9 | import zope.interface 10 | import OpenSSL.crypto 11 | 12 | from datetime import datetime 13 | from certbot import interfaces 14 | from certbot import errors 15 | from certbot.plugins import common 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | @zope.interface.implementer(interfaces.IInstaller) 22 | @zope.interface.provider(interfaces.IPluginFactory) 23 | class VaultInstaller(common.Plugin): 24 | description = "Vault Cert Installer" 25 | 26 | @classmethod 27 | def add_parser_arguments(cls, add): 28 | add('auth-path', 29 | default=os.getenv('VAULT_AUTH_PATH'), 30 | help='Auth path' 31 | ) 32 | add("token", 33 | default=os.getenv('VAULT_TOKEN'), 34 | help="Vault access token" 35 | ) 36 | add("role-id", 37 | default=os.getenv('VAULT_ROLE_ID'), 38 | help='AppRole ID' 39 | ) 40 | add("secret-id", 41 | default=os.getenv('VAULT_SECRET_ID'), 42 | help='AppRole Secret ID' 43 | ) 44 | add("jwt-role", 45 | default=os.getenv('VAULT_JWT_ROLE'), 46 | help='JWT Role' 47 | ) 48 | add("jwt-key", 49 | default=os.getenv('VAULT_JWT_KEY'), 50 | help='JWT Key' 51 | ) 52 | add("addr", 53 | default=os.getenv('VAULT_ADDR'), 54 | help="Vault URL" 55 | ) 56 | add("mount", 57 | default=os.getenv('VAULT_MOUNT'), 58 | help="Vault Mount Point" 59 | ) 60 | add("path", 61 | default=os.getenv('VAULT_PATH'), 62 | help="Vault Mount Point" 63 | ) 64 | 65 | def __init__(self, *args, **kwargs): 66 | super(VaultInstaller, self).__init__(*args, **kwargs) 67 | self.hvac_client = hvac.Client(self.conf('addr')) 68 | 69 | if self.conf('token'): 70 | self.hvac_client.token = self.conf('token') 71 | 72 | if self.conf('role-id') and self.conf('secret-id'): 73 | auth_mount_point = self.conf('auth-path') or 'approle' 74 | self.hvac_client.auth.approle.login( 75 | self.conf('role-id'), 76 | self.conf('secret-id'), 77 | mount_point=auth_mount_point 78 | ) 79 | 80 | if self.conf('jwt-role') and self.conf('jwt-key'): 81 | self.hvac_client.auth.jwt.jwt_login( 82 | self.conf('jwt-role'), 83 | self.conf('jwt-key'), 84 | path=self.conf('auth-path') 85 | ) 86 | 87 | def prepare(self): # pylint: disable=missing-docstring,no-self-use 88 | """ 89 | Prepare the plugin 90 | """ 91 | 92 | if not self.hvac_client.is_authenticated(): 93 | raise errors.PluginError('Not authenticated') 94 | 95 | def more_info(self): # pylint: disable=missing-docstring,no-self-use 96 | """ 97 | Human-readable string to help understand the module 98 | """ 99 | return ( 100 | "Hashicorp Vault Plugin", 101 | "Vault: %s" % ( 102 | self.conf('addr'), 103 | ) 104 | ) 105 | 106 | def get_all_names(self): # pylint: disable=missing-docstring,no-self-use 107 | return [] 108 | 109 | def deploy_cert(self, domain, cert_path, key_path, chain_path, fullchain_path): 110 | """ 111 | Upload Certificate to Vault 112 | 113 | :param str domain: domain to deploy certificate file 114 | :param str cert_path: absolute path to the certificate file 115 | :param str key_path: absolute path to the private key file 116 | :param str chain_path: absolute path to the certificate chain file 117 | :param str fullchain_path: absolute path to the certificate fullchain file (cert plus chain) 118 | 119 | :raises .PluginError: when cert cannot be deployed 120 | """ 121 | 122 | cert = open(cert_path).read() 123 | 124 | date_format = "%Y%m%d%H%M%SZ" 125 | 126 | openssl_cert = OpenSSL.crypto.load_certificate( 127 | OpenSSL.crypto.FILETYPE_PEM, 128 | cert 129 | ) 130 | 131 | data = { 132 | 'type': 'urn:scheme:type:certificate', 133 | 'cert': cert, 134 | 'key': open(key_path).read(), 135 | 'chain': open(fullchain_path).read(), 136 | 'serial': str(openssl_cert.get_serial_number()), 137 | 'life': { 138 | 'issued': int(datetime.strptime(openssl_cert.get_notBefore().decode(), date_format).timestamp()), 139 | 'expires': int(datetime.strptime(openssl_cert.get_notAfter().decode(), date_format).timestamp()), 140 | } 141 | } 142 | 143 | domains = [] 144 | ext_count = openssl_cert.get_extension_count() 145 | for i in range(0, ext_count): 146 | ext = openssl_cert.get_extension(i) 147 | if 'subjectAltName' in str(ext.get_short_name()): 148 | sub = ext._subjectAltNameString() 149 | for row in [x.strip() for x in sub.split(',')]: 150 | if row.startswith('DNS:'): 151 | domains.append(row[len('DNS:'):]) 152 | 153 | if domains: 154 | data['domains'] = domains 155 | 156 | int_path = domain 157 | if self.conf('path'): 158 | int_path = os.path.join(self.conf('path'), domain) 159 | 160 | self.hvac_client.secrets.kv.v2.create_or_update_secret( 161 | mount_point=self.conf('mount'), 162 | path=int_path, 163 | secret=data 164 | ) 165 | 166 | def enhance(self, domain, enhancement, options=None): # pylint: disable=missing-docstring,no-self-use 167 | pass # pragma: no cover 168 | 169 | def supported_enhancements(self): # pylint: disable=missing-docstring,no-self-use 170 | return [] # pragma: no cover 171 | 172 | def get_all_certs_keys(self): # pylint: disable=missing-docstring,no-self-use 173 | pass # pragma: no cover 174 | 175 | def save(self, title=None, temporary=False): # pylint: disable=missing-docstring,no-self-use 176 | pass # pragma: no cover 177 | 178 | def rollback_checkpoints(self, rollback=1): # pylint: disable=missing-docstring,no-self-use 179 | pass # pragma: no cover 180 | 181 | def recovery_routine(self): # pylint: disable=missing-docstring,no-self-use 182 | pass # pragma: no cover 183 | 184 | def view_config_changes(self): # pylint: disable=missing-docstring,no-self-use 185 | pass # pragma: no cover 186 | 187 | def config_test(self): # pylint: disable=missing-docstring,no-self-use 188 | pass # pragma: no cover 189 | 190 | def restart(self): # pylint: disable=missing-docstring,no-self-use 191 | pass # pragma: no cover 192 | 193 | def renew_deploy(self, lineage, *args, **kwargs): # pylint: disable=missing-docstring,no-self-use,unused-argument 194 | """ 195 | Renew certificates when calling `certbot renew` 196 | """ 197 | self.deploy_cert(lineage.names()[0], lineage.cert_path, lineage.key_path, lineage.chain_path, lineage.fullchain_path) 198 | 199 | interfaces.RenewDeployer.register(VaultInstaller) 200 | --------------------------------------------------------------------------------