diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index e71a98c3..5baba223 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -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'], diff --git a/docs/api.rst b/docs/api.rst index 7d928b42..f373664d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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 diff --git a/mitogen/ssh.py b/mitogen/ssh.py index c6b4fe4d..327a4efd 100644 --- a/mitogen/ssh.py +++ b/mitogen/ssh.py @@ -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