issue #127: ssh: reasonable solution to host key checking.

Ideally it would be possible to specify a callback function, but this is
not possible for proxied connections. So simply provide the 3 most
useful modes, defaulting to the most secure.

Closes #127. Closes #134.
pull/242/head
David Wilson 6 years ago
parent 92a2565507
commit 4d1c6d2101

@ -60,10 +60,15 @@ def _connect_local(spec):
def _connect_ssh(spec):
if C.HOST_KEY_CHECKING:
check_host_keys = 'enforce'
else:
check_host_keys = 'ignore'
return {
'method': 'ssh',
'kwargs': {
'check_host_keys': False, # TODO
'check_host_keys': check_host_keys,
'hostname': spec['remote_addr'],
'username': spec['remote_user'],
'password': spec['password'],

@ -842,7 +842,7 @@ Router Class
:py:class:`mitogen.core.StreamError` to be raised, and that
attributes of the stream match the actual behaviour of ``sudo``.
.. method:: ssh (hostname, username=None, ssh_path=None, port=None, check_host_keys=True, password=None, identity_file=None, compression=True, \**kwargs)
.. method:: ssh (hostname, username=None, ssh_path=None, port=None, check_host_keys='enforce', password=None, identity_file=None, compression=True, \**kwargs)
Construct a remote context over a ``ssh`` invocation. The ``ssh``
process is started in a newly allocated pseudo-terminal, and supports
@ -858,10 +858,16 @@ Router Class
:param int port:
Port number to connect to; default is unspecified, which causes SSH
to pick the port number.
:param bool check_host_keys:
If ``False``, arrange for SSH to perform no verification of host
keys. If ``True``, cause SSH to pick the default behaviour, which
is usually to verify host keys.
:param str check_host_keys:
Specifies the SSH host key checking mode:
* ``ignore``: no host key checking is performed. Connections never
fail due to an unknown or changed host key.
* ``accept``: known hosts keys are checked to ensure they match,
new host keys are automatically accepted and verified in future
connections.
* ``enforce``: known host keys are checke to ensure they match,
unknown hosts cause a connection failure.
:param str password:
Password to type if/when ``ssh`` requests it. If not specified and
a password is requested, :py:class:`mitogen.ssh.PasswordError` is

@ -45,12 +45,18 @@ LOG = logging.getLogger('mitogen')
PASSWORD_PROMPT = 'password:'
PERMDENIED_PROMPT = 'permission denied'
HOSTKEY_REQ_PROMPT = 'are you sure you want to continue connecting (yes/no)?'
HOSTKEY_FAIL = 'host key verification failed.'
class PasswordError(mitogen.core.StreamError):
pass
class HostKeyError(mitogen.core.StreamError):
pass
class Stream(mitogen.parent.Stream):
create_child = staticmethod(mitogen.parent.hybrid_tty_create_child)
python_path = 'python2.7'
@ -62,16 +68,24 @@ class Stream(mitogen.parent.Stream):
#: The path to the SSH binary.
ssh_path = 'ssh'
hostname = None
username = None
port = None
identity_file = None
password = None
port = None
ssh_args = None
check_host_keys_msg = 'host_keys= must be set to accept, enforce or ignore'
def construct(self, hostname, username=None, ssh_path=None, port=None,
check_host_keys=True, password=None, identity_file=None,
check_host_keys='enforce', password=None, identity_file=None,
compression=True, ssh_args=None, keepalive_enabled=True,
keepalive_count=3, keepalive_interval=15, **kwargs):
super(Stream, self).construct(**kwargs)
if check_host_keys not in ('accept', 'enforce', 'ignore'):
raise ValueError(self.check_host_keys_msg)
self.hostname = hostname
self.username = username
self.port = port
@ -93,8 +107,6 @@ class Stream(mitogen.parent.Stream):
def get_boot_command(self):
bits = [self.ssh_path]
# bits += ['-o', 'BatchMode yes']
if self.username:
bits += ['-l', self.username]
if self.port is not None:
@ -110,7 +122,11 @@ class Stream(mitogen.parent.Stream):
'-o', 'ServerAliveInterval %s' % (self.keepalive_interval,),
'-o', 'ServerAliveCountMax %s' % (self.keepalive_count,),
]
if not self.check_host_keys:
if self.check_host_keys == 'enforce':
bits += ['-o', 'StrictHostKeyChecking yes']
if self.check_host_keys == 'accept':
bits += ['-o', 'StrictHostKeyChecking ask']
elif self.check_host_keys == 'ignore':
bits += [
'-o', 'StrictHostKeyChecking no',
'-o', 'UserKnownHostsFile /dev/null',
@ -131,6 +147,27 @@ class Stream(mitogen.parent.Stream):
auth_incorrect_msg = 'SSH authentication is incorrect'
password_incorrect_msg = 'SSH password is incorrect'
password_required_msg = 'SSH password was requested, but none specified'
hostkey_config_msg = (
'SSH requested permission to accept unknown host key, but '
'check_host_keys=ignore. This is likely due to ssh_args= '
'conflicting with check_host_keys=. Please correct your '
'configuration.'
)
hostkey_failed_msg = (
'check_host_keys is set to enforce, and SSH reported an unknown '
'or changed host key.'
)
def _host_key_prompt(self):
if self.check_host_keys == 'accept':
LOG.debug('%r: accepting host key', self)
self.tty_stream.transmit_side.write('y\n')
return
# _host_key_prompt() should never be reached with ignore or enforce
# mode, SSH should have handled that. User's ssh_args= is conflicting
# with ours.
raise HostKeyError(self.hostkey_config_msg)
def _connect_bootstrap(self, extra_fd):
self.tty_stream = mitogen.parent.TtyLogStream(extra_fd, self)
@ -146,6 +183,10 @@ class Stream(mitogen.parent.Stream):
if buf.endswith('EC0\n'):
self._ec0_received()
return
elif HOSTKEY_REQ_PROMPT in buf.lower():
self._host_key_prompt()
elif HOSTKEY_FAIL in buf.lower():
raise HostKeyError(self.hostkey_failed_msg)
elif PERMDENIED_PROMPT in buf.lower():
if self.password is not None and password_sent:
raise PasswordError(self.password_incorrect_msg)
@ -154,7 +195,7 @@ class Stream(mitogen.parent.Stream):
elif PASSWORD_PROMPT in buf.lower():
if self.password is None:
raise PasswordError(self.password_required_msg)
LOG.debug('sending password')
LOG.debug('%r: sending password', self)
self.tty_stream.transmit_side.write(self.password + '\n')
password_sent = True

Loading…
Cancel
Save