Repository: Netflix/lemur

Issue:
Destination: Linux Host: Fails to copy to host. "Private key file is encrypted"
Even when the private key password is provided in the destination plugin configuration the copy fails. The workaround is to comment out the lines referenced below. The intention of these lines are to check if a private key password is present or not and return None if there is not one given.

https://github.com/Netflix/lemur/blob/e43268f58504bdecc98e591dc0c7f3d8731ec897/lemur/plugins/lemur_linuxdst/plugin.py#L79-L80

```
Private key file is encrypted
Traceback (most recent call last):
  File "/www/lemur/lemur/certificates/models.py", line 355, in update_destinations
    destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
  File "/www/lemur/lemur/plugins/lemur_linuxdst/plugin.py", line 82, in upload
    remote_host.create_cert(name, dst_dir, export_type, dst_user, dst_priv, dst_priv_key, dst_host, int(dst_host_port))
  File "/www/lemur/lemur/plugins/lemur_linuxdst/remote_host.py", line 65, in create_cert
    copy_cert(lem_cert.cn, dst_user, dst_priv, dst_priv_key, dst_host, dst_host_port, dst_dir, dst_file, dst_data)
  File "/www/lemur/lemur/plugins/lemur_linuxdst/remote_host.py", line 14, in copy_cert
    priv_key = paramiko.RSAKey.from_private_key_file(dst_priv)
  File "/www/lemur/lib/python3.5/site-packages/paramiko/pkey.py", line 205, in from_private_key_file
    key = cls(filename=filename, password=password)
  File "/www/lemur/lib/python3.5/site-packages/paramiko/rsakey.py", line 47, in __init__
    self._from_private_key_file(filename, password)
  File "/www/lemur/lib/python3.5/site-packages/paramiko/rsakey.py", line 172, in _from_private_key_file
    data = self._read_private_key_file('RSA', filename, password)
  File "/www/lemur/lib/python3.5/site-packages/paramiko/pkey.py", line 278, in _read_private_key_file
    data = self._read_private_key(tag, f, password)
  File "/www/lemur/lib/python3.5/site-packages/paramiko/pkey.py", line 325, in _read_private_key
    raise PasswordRequiredException('Private key file is encrypted')
paramiko.ssh_exception.PasswordRequiredException: Private key file is encrypted

```


Discussion:

Resolving patch:
diff --git a/lemur/plugins/lemur_sftp/__init__.py b/lemur/plugins/lemur_sftp/__init__.py
new file mode 100644
--- /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
--- /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/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -147,7 +147,8 @@ def run(self):
             '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=[


Test patch: