Commit 59fa7ee7 authored by Jérome Perrin's avatar Jérome Perrin

software/proftpd: support authentication with ssh key

parent d92dcf8f
...@@ -4,14 +4,13 @@ http://www.proftpd.org/docs/ ...@@ -4,14 +4,13 @@ http://www.proftpd.org/docs/
# Features # Features
* sftp only is enabled * sftp only is enabled, with authentication by key or password
* partially uploadloaded are not visible thanks to [`HiddenStores`](http://proftpd.org/docs/directives/linked/config_ref_HiddenStores.html) ( in fact they are, but name starts with `.` ) * partially uploadloaded are not visible thanks to [`HiddenStores`](http://proftpd.org/docs/directives/linked/config_ref_HiddenStores.html) ( in fact they are, but name starts with `.` )
* 5 failed login attempts will cause the host to be temporary banned * 5 failed login attempts will cause the host to be temporary banned
# TODO # TODO
* only password login is enabled. enabling [`SFTPAuthorizedUserKeys`](http://www.proftpd.org/docs/contrib/mod_sftp.html#SFTPAuthorizedUserKeys) seems to break password only login
* log rotation * log rotation
* make sure SFTPLog is useful (seems very verbose and does not contain more than stdout) * make sure SFTPLog is useful (seems very verbose and does not contain more than stdout)
* make it easier to manage users ( using `mod_auth_web` against an ERP5 endpoint or accepting a list of user/password as instance parameter ) * make it easier to manage users ( using `mod_auth_web` against an ERP5 endpoint or accepting a list of user/password as instance parameter )
......
...@@ -19,8 +19,8 @@ md5sum = efb4238229681447aa7fe73898dffad4 ...@@ -19,8 +19,8 @@ md5sum = efb4238229681447aa7fe73898dffad4
[instance-default] [instance-default]
filename = instance-default.cfg.in filename = instance-default.cfg.in
md5sum = 2a2c066d7d40dd8545f3008f434ee842 md5sum = 830a2e759d64b01ddcf593467493abce
[proftpd-config-file] [proftpd-config-file]
filename = proftpd-config-file.cfg.in filename = proftpd-config-file.cfg.in
md5sum = a7c0f4607c378b640379cc258a8aadfa md5sum = 336bad8d0283739be9e0e62da445f33e
...@@ -66,7 +66,7 @@ ban-log=${directory:log}/proftpd-ban.log ...@@ -66,7 +66,7 @@ ban-log=${directory:log}/proftpd-ban.log
ssh-host-rsa-key=${ssh-host-rsa-key:output} ssh-host-rsa-key=${ssh-host-rsa-key:output}
ssh-host-dsa-key=${ssh-host-dsa-key:output} ssh-host-dsa-key=${ssh-host-dsa-key:output}
ssh-host-ecdsa-key=${ssh-host-ecdsa-key:output} ssh-host-ecdsa-key=${ssh-host-ecdsa-key:output}
ssh-authorized-keys-dir = ${directory:ssh-authorized-keys-dir} ssh-authorized-key = ${ssh-authorized-keys:rendered}
ban-table=${directory:srv}/proftpd-ban-table ban-table=${directory:srv}/proftpd-ban-table
control-socket=${directory:var}/proftpd.sock control-socket=${directory:var}/proftpd.sock
auth-user-file=${auth-user-file:output} auth-user-file=${auth-user-file:output}
...@@ -76,6 +76,13 @@ command-line = ...@@ -76,6 +76,13 @@ command-line =
{{ proftpd_bin }} --nodaemon --config ${proftpd-config-file:rendered} {{ proftpd_bin }} --nodaemon --config ${proftpd-config-file:rendered}
wrapper-path = ${directory:service}/proftpd wrapper-path = ${directory:service}/proftpd
[ssh-authorized-keys]
rendered = ${directory:ssh-authorized-keys-dir}/authorized_keys
{% if slapparameter_dict.get('ssh-key') %}
recipe = slapos.recipe.template:jinja2
template = inline:{{ slapparameter_dict['ssh-key'] | indent }}
{% endif %}
[proftpd-listen-promise] [proftpd-listen-promise]
<= monitor-promise-base <= monitor-promise-base
module = check_port_listening module = check_port_listening
...@@ -134,4 +141,6 @@ instance-promises = ...@@ -134,4 +141,6 @@ instance-promises =
recipe = slapos.cookbook:publish recipe = slapos.cookbook:publish
url = ${proftpd:url} url = ${proftpd:url}
username = ${proftpd-password:username} username = ${proftpd-password:username}
{% if not slapparameter_dict.get('ssh-key') %}
password = ${proftpd-password:passwd} password = ${proftpd-password:passwd}
{% endif %}
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Parameters to instantiate PoFTPd", "description": "Parameters to instantiate ProFTPd",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"port": { "port": {
"description": "Port number to listen to - default to 8022", "description": "Port number to listen to",
"type": "number" "type": "number",
"default": 8022
},
"ssh-key": {
"description": "SSH public key, in RFC4716 format. Note that this is not the default format used by openssh and that openssh keys must be converted with `ssh-keygen -e -f ~/.ssh/id_rsa.pub`",
"type": "string"
} }
} }
} }
...@@ -10,11 +10,10 @@ ...@@ -10,11 +10,10 @@
}, },
"username": { "username": {
"description": "Default username", "description": "Default username",
"type": "string", "type": "string"
"optional": true
}, },
"password": { "password": {
"description": "Password for default username", "description": "Password for default username, when not using ssh-key",
"type": "string", "type": "string",
"optional": true "optional": true
} }
......
...@@ -20,7 +20,7 @@ SFTPEngine on ...@@ -20,7 +20,7 @@ SFTPEngine on
SFTPHostKey {{ proftpd['ssh-host-rsa-key'] }} SFTPHostKey {{ proftpd['ssh-host-rsa-key'] }}
SFTPHostKey {{ proftpd['ssh-host-dsa-key'] }} SFTPHostKey {{ proftpd['ssh-host-dsa-key'] }}
SFTPHostKey {{ proftpd['ssh-host-ecdsa-key'] }} SFTPHostKey {{ proftpd['ssh-host-ecdsa-key'] }}
#SFTPAuthorizedUserKeys file:{{ proftpd['ssh-authorized-keys-dir'] }}%u SFTPAuthorizedUserKeys file:{{ proftpd['ssh-authorized-key'] }}
# Logging # Logging
......
...@@ -34,12 +34,14 @@ import subprocess ...@@ -34,12 +34,14 @@ import subprocess
import pysftp import pysftp
import psutil import psutil
import paramiko
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
from paramiko.ssh_exception import AuthenticationException from paramiko.ssh_exception import AuthenticationException
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
from slapos.testing.utils import findFreeTCPPort from slapos.testing.utils import findFreeTCPPort
setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass( setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath( os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg'))) os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
...@@ -176,7 +178,7 @@ class TestUserManagement(ProFTPdTestCase): ...@@ -176,7 +178,7 @@ class TestUserManagement(ProFTPdTestCase):
class TestBan(ProFTPdTestCase): class TestBan(ProFTPdTestCase):
def test_client_are_banned_after_5_wrong_passwords(self): def test_client_are_banned_after_5_wrong_passwords(self):
# Simulate failed 5 login attempts # Simulate failed 5 login attempts
for i in range(5): for _ in range(5):
with self.assertRaisesRegex(AuthenticationException, with self.assertRaisesRegex(AuthenticationException,
'Authentication failed'): 'Authentication failed'):
self._getConnection(password='wrong') self._getConnection(password='wrong')
...@@ -237,3 +239,66 @@ class TestFilesAndSocketsInInstanceDir(ProFTPdTestCase): ...@@ -237,3 +239,66 @@ class TestFilesAndSocketsInInstanceDir(ProFTPdTestCase):
s for s in self.proftpdProcess.connections('unix') s for s in self.proftpdProcess.connections('unix')
if not s.laddr.startswith(self.computer_partition_root_path) if not s.laddr.startswith(self.computer_partition_root_path)
]) ])
class TestSSHKey(TestSFTPOperations):
@classmethod
def getInstanceParameterDict(cls):
cls.ssh_key = paramiko.DSSKey.generate(1024)
return {
'ssh-key':
'---- BEGIN SSH2 PUBLIC KEY ----\n{}\n---- END SSH2 PUBLIC KEY ----'.
format(cls.ssh_key.get_base64())
}
def _getConnection(self, username=None):
"""Override to log in with the SSH key
"""
parameter_dict = self.computer_partition.getConnectionParameterDict()
sftp_url = urlparse(parameter_dict['url'])
username = username or parameter_dict['username']
cnopts = pysftp.CnOpts()
cnopts.hostkeys = None
with tempfile.NamedTemporaryFile(mode='w') as keyfile:
self.ssh_key.write_private_key(keyfile)
keyfile.flush()
return pysftp.Connection(
sftp_url.hostname,
port=sftp_url.port,
cnopts=cnopts,
username=username,
private_key=keyfile.name,
)
def test_authentication_failure(self):
parameter_dict = self.computer_partition.getConnectionParameterDict()
sftp_url = urlparse(parameter_dict['url'])
with self.assertRaisesRegex(AuthenticationException,
'Authentication failed'):
self._getConnection(username='wrong username')
cnopts = pysftp.CnOpts()
cnopts.hostkeys = None
# wrong private key
with tempfile.NamedTemporaryFile(mode='w') as keyfile:
paramiko.DSSKey.generate(1024).write_private_key(keyfile)
keyfile.flush()
with self.assertRaisesRegex(AuthenticationException,
'Authentication failed'):
pysftp.Connection(
sftp_url.hostname,
port=sftp_url.port,
cnopts=cnopts,
username=parameter_dict['username'],
private_key=keyfile.name,
)
def test_published_parameters(self):
# no password is published, we only login with key
parameter_dict = self.computer_partition.getConnectionParameterDict()
self.assertIn('username', parameter_dict)
self.assertNotIn('password', parameter_dict)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment