From 4a0103a88db823510f5cc24d78cffcc68d43da8b Mon Sep 17 00:00:00 2001 From: Dmitry Zykov Date: Tue, 3 Apr 2018 20:30:19 +0300 Subject: [PATCH] SFTP destination plugin (#1170) * add sftp destination plugin --- docs/requirements.txt | 2 +- lemur/plugins/lemur_sftp/__init__.py | 5 + lemur/plugins/lemur_sftp/plugin.py | 179 ++++++++++++++++++ .../destination/destination.tpl.html | 4 +- requirements.in | 2 +- setup.py | 3 +- 6 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 lemur/plugins/lemur_sftp/__init__.py create mode 100644 lemur/plugins/lemur_sftp/plugin.py diff --git a/docs/requirements.txt b/docs/requirements.txt index e89bedf4..5cb8b8b6 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -30,7 +30,7 @@ pem==17.1.0 raven[flask]==6.1.0 jinja2==2.9.6 # pyldap==2.4.37 # cannot be installed on rtd - required by ldap auth provider -paramiko==2.2.1 # required for lemur_linuxdst plugin +paramiko==2.4.1 # required for the SFTP destination plugin sphinx sphinxcontrib-httpdomain sphinx-rtd-theme diff --git a/lemur/plugins/lemur_sftp/__init__.py b/lemur/plugins/lemur_sftp/__init__.py new file mode 100644 index 00000000..8ce5a7f3 --- /dev/null +++ b/lemur/plugins/lemur_sftp/__init__.py @@ -0,0 +1,5 @@ +try: + VERSION = __import__('pkg_resources') \ + .get_distribution(__name__).version +except Exception as e: + VERSION = 'unknown' diff --git a/lemur/plugins/lemur_sftp/plugin.py b/lemur/plugins/lemur_sftp/plugin.py new file mode 100644 index 00000000..e782bcf7 --- /dev/null +++ b/lemur/plugins/lemur_sftp/plugin.py @@ -0,0 +1,179 @@ +""" +.. module: lemur.plugins.lemur_sftp.plugin + :platform: Unix + :synopsis: Allow the uploading of certificates to SFTP. + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + + Allow the uploading of certificates to SFTP. + + NGINX and Apache export formats are supported. + + Password and RSA private key are supported. + Passwords are not encrypted and stored as a plain text. + + Detailed logging when Lemur debug mode is enabled. + +.. moduleauthor:: Dmitry Zykov https://github.com/DmitryZykov +""" + +import paramiko + +from flask import current_app +from lemur.plugins import lemur_sftp +from lemur.common.defaults import common_name +from lemur.common.utils import parse_certificate +from lemur.plugins.bases import DestinationPlugin + + +class SFTPDestinationPlugin(DestinationPlugin): + title = 'SFTP' + slug = 'sftp-destination' + description = 'Allow the uploading of certificates to SFTP' + version = lemur_sftp.VERSION + + author = 'Dmitry Zykov' + author_url = 'https://github.com/DmitryZykov' + + options = [ + { + 'name': 'host', + 'type': 'str', + 'required': True, + 'helpMessage': 'The SFTP host.' + }, + { + 'name': 'port', + 'type': 'int', + 'required': True, + 'helpMessage': 'The SFTP port, default is 22.', + 'validation': '^(6553[0-5]|655[0-2][0-9]\d|65[0-4](\d){2}|6[0-4](\d){3}|[1-5](\d){4}|[1-9](\d){0,3})', + 'default': '22' + }, + { + 'name': 'user', + 'type': 'str', + 'required': True, + 'helpMessage': 'The SFTP user. Default is root.', + 'default': 'root' + }, + { + 'name': 'password', + 'type': 'str', + 'required': False, + 'helpMessage': 'The SFTP password (optional when the private key is used).', + 'default': None + }, + { + 'name': 'privateKeyPath', + 'type': 'str', + 'required': False, + 'helpMessage': 'The path to the RSA private key on the Lemur server (optional).', + 'default': None + }, + { + 'name': 'privateKeyPass', + 'type': 'str', + 'required': False, + 'helpMessage': 'The password for the encrypted RSA private key (optional).', + 'default': None + }, + { + 'name': 'destinationPath', + 'type': 'str', + 'required': True, + 'helpMessage': 'The SFTP path where certificates will be uploaded.', + 'default': '/etc/nginx/certs' + }, + { + 'name': 'exportFormat', + 'required': True, + 'value': 'NGINX', + 'helpMessage': 'The export format for certificates.', + 'type': 'select', + 'available': [ + 'NGINX', + 'Apache' + ] + } + ] + + def upload(self, name, body, private_key, cert_chain, options, **kwargs): + + current_app.logger.debug('SFTP destination plugin is started') + + cn = common_name(parse_certificate(body)) + host = self.get_option('host', options) + port = self.get_option('port', options) + user = self.get_option('user', options) + password = self.get_option('password', options) + ssh_priv_key = self.get_option('privateKeyPath', options) + ssh_priv_key_pass = self.get_option('privateKeyPass', options) + dst_path = self.get_option('destinationPath', options) + export_format = self.get_option('exportFormat', options) + + # prepare files for upload + files = {cn + '.key': private_key, + cn + '.pem': body} + + if cert_chain: + if export_format == 'NGINX': + # assemble body + chain in the single file + files[cn + '.pem'] += '\n' + cert_chain + + elif export_format == 'Apache': + # store chain in the separate file + files[cn + '.ca.bundle.pem'] = cert_chain + + # upload files + try: + current_app.logger.debug('Connecting to {0}@{1}:{2}'.format(user, host, port)) + ssh = paramiko.SSHClient() + + # allow connection to the new unknown host + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # open the ssh connection + if password: + current_app.logger.debug('Using password') + ssh.connect(host, username=user, port=port, password=password) + elif ssh_priv_key: + current_app.logger.debug('Using RSA private key') + pkey = paramiko.RSAKey.from_private_key_file(ssh_priv_key, ssh_priv_key_pass) + ssh.connect(host, username=user, port=port, pkey=pkey) + else: + current_app.logger.error("No password or private key provided. Can't proceed") + raise paramiko.ssh_exception.AuthenticationException + + # open the sftp session inside the ssh connection + sftp = ssh.open_sftp() + + # make sure that the destination path exist + try: + current_app.logger.debug('Creating {0}'.format(dst_path)) + sftp.mkdir(dst_path) + except IOError: + current_app.logger.debug('{0} already exist, resuming'.format(dst_path)) + try: + dst_path_cn = dst_path + '/' + cn + current_app.logger.debug('Creating {0}'.format(dst_path_cn)) + sftp.mkdir(dst_path_cn) + except IOError: + current_app.logger.debug('{0} already exist, resuming'.format(dst_path_cn)) + + # upload certificate files to the sftp destination + for filename, data in files.items(): + current_app.logger.debug('Uploading {0} to {1}'.format(filename, dst_path_cn)) + with sftp.open(dst_path_cn + '/' + filename, 'w') as f: + f.write(data) + # read only for owner, -r-------- + sftp.chmod(dst_path_cn + '/' + filename, 0o400) + + ssh.close() + + except Exception as e: + current_app.logger.error('ERROR in {0}: {1}'.format(e.__class__, e)) + try: + ssh.close() + except BaseException: + pass diff --git a/lemur/static/app/angular/destinations/destination/destination.tpl.html b/lemur/static/app/angular/destinations/destination/destination.tpl.html index 6ab9d78b..1d240dbb 100644 --- a/lemur/static/app/angular/destinations/destination/destination.tpl.html +++ b/lemur/static/app/angular/destinations/destination/destination.tpl.html @@ -42,7 +42,7 @@ {{ item.name | titleCase }}
- @@ -61,7 +61,7 @@ {{ item.name | titleCase }}
- diff --git a/requirements.in b/requirements.in index 4419594c..4dad196d 100644 --- a/requirements.in +++ b/requirements.in @@ -19,7 +19,7 @@ lockfile marshmallow-sqlalchemy marshmallow ndg-httpsclient -paramiko # required for lemur_linuxdst plugin +paramiko # required for the SFTP destination plugin pem psycopg2 pyjwt diff --git a/setup.py b/setup.py index a8f4a35a..9f5e2990 100644 --- a/setup.py +++ b/setup.py @@ -147,7 +147,8 @@ setup( 'digicert_issuer = lemur.plugins.lemur_digicert.plugin:DigiCertIssuerPlugin', 'digicert_cis_issuer = lemur.plugins.lemur_digicert.plugin:DigiCertCISIssuerPlugin', 'digicert_cis_source = lemur.plugins.lemur_digicert.plugin:DigiCertCISSourcePlugin', - 'csr_export = lemur.plugins.lemur_csr.plugin:CSRExportPlugin' + 'csr_export = lemur.plugins.lemur_csr.plugin:CSRExportPlugin', + 'sftp_destination = lemur.plugins.lemur_sftp.plugin:SFTPDestinationPlugin' ], }, classifiers=[