From e609d1b1fb113c523619164b75ee7c6bd1b249f0 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 12 Jul 2018 05:44:24 +0100 Subject: [PATCH 01/14] docs: glaring ancient typo. --- docs/ansible.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ansible.rst b/docs/ansible.rst index 7cadd67e..6c8bde25 100644 --- a/docs/ansible.rst +++ b/docs/ansible.rst @@ -45,7 +45,7 @@ it can only ensure the module executes as quickly as possible. * **Fewer writes to the target filesystem occur**. In typical configurations, Ansible repeatedly rewrites and extracts ZIP files to multiple temporary - directories on the target. Security issues relating to temporarily files in + directories on the target. Security issues relating to temporary files in cross-account scenarios are entirely avoided. The effect is most potent on playbooks that execute many **short-lived From 745d72bb1d0fa288a323d0eae5e7e2b3cc671815 Mon Sep 17 00:00:00 2001 From: napkindrawing Date: Thu, 12 Jul 2018 14:54:41 -0400 Subject: [PATCH 02/14] core: support for "doas" become_method --- ansible_mitogen/connection.py | 34 ++++- .../plugins/connection/mitogen_doas.py | 43 +++++++ mitogen/core.py | 1 + mitogen/doas.py | 118 ++++++++++++++++++ mitogen/parent.py | 3 + 5 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 ansible_mitogen/plugins/connection/mitogen_doas.py create mode 100644 mitogen/doas.py diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index c8b1870c..2b5c4356 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -173,6 +173,20 @@ def _connect_sudo(spec): } +def _connect_doas(spec): + return { + 'method': 'doas', + 'enable_lru': True, + 'kwargs': { + 'username': spec['become_user'], + 'password': wrap_or_none(mitogen.core.Secret, spec['become_pass']), + 'python_path': spec['python_path'], + 'doas_path': spec['become_exe'], + 'connect_timeout': spec['timeout'], + } + } + + def _connect_mitogen_su(spec): # su as a first-class proxied connection, not a become method. return { @@ -202,6 +216,20 @@ def _connect_mitogen_sudo(spec): } +def _connect_mitogen_doas(spec): + # doas as a first-class proxied connection, not a become method. + return { + 'method': 'doas', + 'kwargs': { + 'username': spec['remote_user'], + 'password': wrap_or_none(mitogen.core.Secret, spec['password']), + 'python_path': spec['python_path'], + 'doas_path': spec['become_exe'], + 'connect_timeout': spec['timeout'], + } + } + + CONNECTION_METHOD = { 'docker': _connect_docker, 'jail': _connect_jail, @@ -213,8 +241,10 @@ CONNECTION_METHOD = { 'ssh': _connect_ssh, 'su': _connect_su, 'sudo': _connect_sudo, + 'doas': _connect_doas, 'mitogen_su': _connect_mitogen_su, 'mitogen_sudo': _connect_mitogen_sudo, + 'mitogen_doas': _connect_mitogen_doas, } @@ -314,8 +344,8 @@ class Connection(ansible.plugins.connection.ConnectionBase): #: target user account. fork_context = None - #: Only sudo and su are supported for now. - become_methods = ['sudo', 'su'] + #: Only sudo, su, and doas are supported for now. + become_methods = ['sudo', 'su', 'doas'] #: Set to 'ansible_python_interpreter' by on_action_run(). python_path = None diff --git a/ansible_mitogen/plugins/connection/mitogen_doas.py b/ansible_mitogen/plugins/connection/mitogen_doas.py new file mode 100644 index 00000000..7d60b482 --- /dev/null +++ b/ansible_mitogen/plugins/connection/mitogen_doas.py @@ -0,0 +1,43 @@ +# Copyright 2017, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import os.path +import sys + +try: + import ansible_mitogen.connection +except ImportError: + base_dir = os.path.dirname(__file__) + sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..'))) + del base_dir + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'mitogen_doas' diff --git a/mitogen/core.py b/mitogen/core.py index 53f32bb0..6f536ec8 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -607,6 +607,7 @@ class Importer(object): self._present = {'mitogen': [ 'compat', 'debug', + 'doas', 'docker', 'fakessh', 'fork', diff --git a/mitogen/doas.py b/mitogen/doas.py new file mode 100644 index 00000000..1d9d04eb --- /dev/null +++ b/mitogen/doas.py @@ -0,0 +1,118 @@ +# Copyright 2017, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import logging +import os + +import mitogen.core +import mitogen.parent +from mitogen.core import b + + +LOG = logging.getLogger(__name__) + + +class PasswordError(mitogen.core.StreamError): + pass + + +class Stream(mitogen.parent.Stream): + create_child = staticmethod(mitogen.parent.hybrid_tty_create_child) + child_is_immediate_subprocess = False + + #: Once connected, points to the corresponding TtyLogStream, allowing it to + #: be disconnected at the same time this stream is being torn down. + tty_stream = None + + username = 'root' + password = None + doas_path = 'doas' + password_prompt = b('Password:') + incorrect_prompts = ( + b('doas: authentication failed'), + ) + + def construct(self, username=None, password=None, doas_path=None, + password_prompt=None, incorrect_prompts=None, **kwargs): + super(Stream, self).construct(**kwargs) + if username is not None: + self.username = username + if password is not None: + self.password = password + if doas_path is not None: + self.doas_path = doas_path + if password_prompt is not None: + self.password_prompt = password_prompt.lower() + if incorrect_prompts is not None: + self.incorrect_prompts = map(str.lower, incorrect_prompts) + + def connect(self): + super(Stream, self).connect() + self.name = u'doas.' + mitogen.core.to_text(self.username) + + def on_disconnect(self, broker): + self.tty_stream.on_disconnect(broker) + super(Stream, self).on_disconnect(broker) + + def get_boot_command(self): + bits = [self.doas_path, '-u', self.username, '--'] + bits = bits + super(Stream, self).get_boot_command() + LOG.debug('doas command line: %r', bits) + return bits + + password_incorrect_msg = 'doas password is incorrect' + password_required_msg = 'doas password is required' + + def _connect_bootstrap(self, extra_fd): + self.tty_stream = mitogen.parent.TtyLogStream(extra_fd, self) + + password_sent = False + it = mitogen.parent.iter_read( + fds=[self.receive_side.fd, extra_fd], + deadline=self.connect_deadline, + ) + + for buf in it: + LOG.debug('%r: received %r', self, buf) + if buf.endswith(self.EC0_MARKER): + self._ec0_received() + return + if any(s in buf.lower() for s in self.incorrect_prompts): + if password_sent: + raise PasswordError(self.password_incorrect_msg) + elif self.password_prompt in buf.lower(): + if self.password is None: + raise PasswordError(self.password_required_msg) + if password_sent: + raise PasswordError(self.password_incorrect_msg) + LOG.debug('sending password') + self.tty_stream.transmit_side.write( + mitogen.core.to_text(self.password + '\n').encode('utf-8') + ) + password_sent = True + raise mitogen.core.StreamError('bootstrap failed') diff --git a/mitogen/parent.py b/mitogen/parent.py index 36d107f5..26796113 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1240,6 +1240,9 @@ class Router(mitogen.core.Router): self._context_by_id[context.context_id] = context return context + def doas(self, **kwargs): + return self.connect(u'doas', **kwargs) + def docker(self, **kwargs): return self.connect(u'docker', **kwargs) From 38799e22d251f2675383676963dd59f2992bae65 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 12 Jul 2018 20:16:44 +0100 Subject: [PATCH 03/14] examples: fix mitogen-fuse on 2.x. --- examples/mitogen-fuse.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/mitogen-fuse.py b/examples/mitogen-fuse.py index b086398d..7421a0e2 100644 --- a/examples/mitogen-fuse.py +++ b/examples/mitogen-fuse.py @@ -125,6 +125,8 @@ class Operations(fuse.Operations): # fuse.LoggingMixIn, self.host = host self.root = path self.ready = threading.Event() + if not hasattr(self, 'encoding'): + self.encoding = 'utf-8' def init(self, path): self.broker = mitogen.master.Broker(install_watcher=False) From f20274ea18d418fc6d1be308be6ed853ddd617fa Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 12 Jul 2018 21:36:33 +0100 Subject: [PATCH 04/14] docs: fix lock icon. --- docs/ansible.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ansible.rst b/docs/ansible.rst index 6c8bde25..3dd17c3a 100644 --- a/docs/ansible.rst +++ b/docs/ansible.rst @@ -90,7 +90,7 @@ concurrent to an equivalent run using the extension. .. raw:: html From b9c88d344b47520d929b3611f59f56c1b68e6326 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 12 Jul 2018 22:40:15 +0100 Subject: [PATCH 05/14] issue #299: ansible: fix PluginLoader.get() monkey-patch This prototype is broken for network_cli connections. --- ansible_mitogen/strategy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible_mitogen/strategy.py b/ansible_mitogen/strategy.py index 6dd8ec04..fbe23ef7 100644 --- a/ansible_mitogen/strategy.py +++ b/ansible_mitogen/strategy.py @@ -54,7 +54,7 @@ def wrap_action_loader__get(name, *args, **kwargs): return adorned_klass(*args, **kwargs) -def wrap_connection_loader__get(name, play_context, new_stdin, **kwargs): +def wrap_connection_loader__get(name, *args, **kwargs): """ While the strategy is active, rewrite connection_loader.get() calls for some transports into requests for a compatible Mitogen transport. @@ -62,7 +62,7 @@ def wrap_connection_loader__get(name, play_context, new_stdin, **kwargs): if name in ('docker', 'jail', 'local', 'lxc', 'lxd', 'machinectl', 'setns', 'ssh'): name = 'mitogen_' + name - return connection_loader__get(name, play_context, new_stdin, **kwargs) + return connection_loader__get(name, *args, **kwargs) class StrategyMixin(object): From 184104ce9204bee9c79852b2b5f046d69881a59d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 13 Jul 2018 15:05:51 +0100 Subject: [PATCH 06/14] issue #303: add doas to the docs --- docs/ansible.rst | 34 +++++++++++++++++++++++++++++----- docs/api.rst | 34 ++++++++++++++++++++++++++++------ docs/changelog.rst | 7 +++++++ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/docs/ansible.rst b/docs/ansible.rst index 3dd17c3a..a8f5df02 100644 --- a/docs/ansible.rst +++ b/docs/ansible.rst @@ -127,8 +127,8 @@ Noteworthy Differences precluding its use for installing Python on a target. This will be addressed soon. -* The ``su`` and ``sudo`` become methods are available. File bugs to register - interest in more. +* The ``doas``, ``su`` and ``sudo`` become methods are available. File bugs to + register interest in more. * The `docker `_, `jail `_, @@ -137,9 +137,9 @@ Noteworthy Differences `lxd `_, and `ssh `_ built-in connection types are supported, along with Mitogen-specific - :ref:`machinectl `, :ref:`mitogen_su `, :ref:`mitogen_sudo - `, and :ref:`setns ` types. File bugs to register interest in - others. + :ref:`machinectl `, :ref:`mitogen_doas< mitogen_doas>`, + :ref:`mitogen_su `, :ref:`mitogen_sudo `, and :ref:`setns ` + types. File bugs to register interest in others. * Local commands execute in a reuseable interpreter created identically to interpreters on targets. Presently one interpreter per ``become_user`` @@ -477,6 +477,30 @@ establishment of additional reuseable interpreters as necessary to match the configuration of each task. +.. _doas: + +Doas +~~~~ + +``doas`` can be used as a connection method that supports connection delegation, or +as a become method. + +When used as a become method: + +* ``ansible_python_interpreter`` +* ``ansible_become_exe``: path to ``doas`` binary. +* ``ansible_become_user`` (default: ``root``) +* ``ansible_become_pass`` (default: assume passwordless) +* ansible.cfg: ``timeout`` + +When used as the ``mitogen_doas`` connection method: + +* The inventory hostname has no special meaning. +* ``ansible_user``: username to use. +* ``ansible_password``: password to use. +* ``ansible_python_interpreter`` + + .. _method-docker: Docker diff --git a/docs/api.rst b/docs/api.rst index 0bb42ec2..bc88a622 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -523,6 +523,31 @@ Router Class # Use the SSH connection to create a sudo connection. remote_root = router.sudo(username='root', via=remote_machine) + .. method:: dos (username=None, password=None, su_path=None, password_prompt=None, incorrect_prompts=None, \**kwargs) + + Construct a context on the local machine over a ``su`` invocation. The + ``su`` process is started in a newly allocated pseudo-terminal, and + supports typing interactive passwords. + + Accepts all parameters accepted by :py:meth:`local`, in addition to: + + :param str username: + Username to use, defaults to ``root``. + :param str password: + The account password to use if requested. + :param str su_path: + Filename or complete path to the ``su`` binary. ``PATH`` will be + searched if given as a filename. Defaults to ``su``. + :param bytes password_prompt: + A string that indicates ``doas`` is requesting a password. Defaults + to ``Password:``. + :param list incorrect_prompts: + List of bytestrings indicating the password is incorrect. Defaults + to `(b"doas: authentication failed")`. + :raises mitogen.su.PasswordError: + A password was requested but none was provided, the supplied + password was incorrect, or the target account did not exist. + .. method:: docker (container=None, image=None, docker_path=None, \**kwargs) Construct a context on the local machine within an existing or @@ -616,12 +641,9 @@ Router Class :param str su_path: Filename or complete path to the ``su`` binary. ``PATH`` will be searched if given as a filename. Defaults to ``su``. - :param str password_prompt: - The string to wait to that signals ``su`` is requesting a password. - Defaults to ``Password:``. - :param str password_prompt: - The string that signal a request for the password. Defaults to - ``Password:``. + :param bytes password_prompt: + The string that indicates ``su`` is requesting a password. Defaults + to ``Password:``. :param str incorrect_prompts: Strings that signal the password is incorrect. Defaults to `("su: sorry", "su: authentication failure")`. diff --git a/docs/changelog.rst b/docs/changelog.rst index 9e85446e..b999ba4d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,13 @@ Release Notes +v0.2.2 (2018-07-??) +------------------- + +* `#303 `_: the ``doas`` become method + is now supported. Contributed by Mike Walker. + + v0.2.1 (2018-07-10) ------------------- From 8609fa5f445364c3a2b173a9dfde927a6ee9851c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 14 Jul 2018 05:50:27 +0100 Subject: [PATCH 07/14] docs: link to PyPI release, not GitHub archive URL. Now download counts are visible via PSF BigQuery. --- docs/ansible.rst | 6 +++--- docs/conf.py | 23 ++++++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/ansible.rst b/docs/ansible.rst index a8f5df02..e53ecf8e 100644 --- a/docs/ansible.rst +++ b/docs/ansible.rst @@ -60,13 +60,13 @@ Installation 1. Thoroughly review the :ref:`noteworthy_differences` and :ref:`changelog`. 2. Verify Ansible 2.3-2.5 and Python 2.6, 2.7 or 3.6 are listed in ``ansible --version`` output. -3. Download and extract https://github.com/dw/mitogen/archive/stable.zip +3. Download and extract |mitogen_url| from PyPI. 4. Modify ``ansible.cfg``: - .. code-block:: dosini + .. parsed-literal:: [defaults] - strategy_plugins = /path/to/mitogen-master/ansible_mitogen/plugins/strategy + strategy_plugins = /path/to/mitogen-|mitogen_version|/ansible_mitogen/plugins/strategy strategy = mitogen_linear The ``strategy`` key is optional. If omitted, the diff --git a/docs/conf.py b/docs/conf.py index 90c0f446..57adf597 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,15 +2,8 @@ import os import sys sys.path.append('..') - -def grep_version(): - path = os.path.join(os.path.dirname(__file__), '../mitogen/__init__.py') - with open(path) as fp: - for line in fp: - if line.startswith('__version__'): - _, _, s = line.partition('=') - return '.'.join(map(str, eval(s))) - +import mitogen +VERSION = '%s.%s.%s' % mitogen.__version__ author = u'David Wilson' copyright = u'2018, David Wilson' @@ -31,8 +24,16 @@ language = None master_doc = 'toc' project = u'Mitogen' pygments_style = 'sphinx' -release = grep_version() +release = VERSION source_suffix = '.rst' templates_path = ['_templates'] todo_include_todos = False -version = grep_version() +version = VERSION + +rst_epilog = """ + +.. |mitogen_version| replace:: %(VERSION)s + +.. |mitogen_url| replace:: `mitogen-%(VERSION)s.tar.gz `__ + +""" % locals() From 70d732d35beb7f496c597802de930255fcaf5e5c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 14 Jul 2018 18:50:12 +0100 Subject: [PATCH 08/14] docs: add "no route" to known issues. --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index b999ba4d..0d296fc6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -94,6 +94,10 @@ Mitogen for Ansible - initech_app - y2k_fix +* When running with ``-vvv``, log messages such as *mitogen: Router(Broker(0x7f5a48921590)): no route + for Message(..., 102, ...), my ID is ...* may be visible. These are due to a + minor race while initializing logging and can be ignored. + * Performance does not scale linearly with target count. This requires significant additional work, as major bottlenecks exist in the surrounding Ansible code. Performance-related bug reports for any scenario remain From a5fae0d0844975052e7e3e73246d5fa4a08c3dd0 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 15 Jul 2018 17:10:58 +0100 Subject: [PATCH 09/14] docs: add jgadling to Contributors --- docs/contributors.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributors.rst b/docs/contributors.rst index 2bf2173c..9eebaa95 100644 --- a/docs/contributors.rst +++ b/docs/contributors.rst @@ -106,6 +106,7 @@ sponsorship and outstanding future-thinking of its early adopters.
  • Epartment
  • Fidy Andrianaivonever let a human do an ansible job ;)
  • rkrzr
  • +
  • jgadling
  • John F Wall — Making Ansible Great with Massive Parallelism
  • KennethC
  • Lewis Bellwood — Happy to be apart of a great project.
  • From 336e90c5e30ccabe916248078920c886819abe68 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 15 Jul 2018 20:34:43 +0100 Subject: [PATCH 10/14] parent: avoid needless quoting in Argv. This just makes debug output a little more readable. --- mitogen/parent.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mitogen/parent.py b/mitogen/parent.py index 26796113..38484874 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -457,10 +457,16 @@ class Argv(object): def __init__(self, argv): self.argv = argv + must_escape = frozenset('\\$"`!') + must_escape_or_space = must_escape | frozenset(' ') + def escape(self, x): + if not self.must_escape_or_space.intersection(x): + return x + s = '"' for c in x: - if c in '\\$"`': + if c in self.must_escape: s += '\\' s += c s += '"' From 8ce51ec96cdfa4035c6bc762e3869b4d597caab6 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 17 Jul 2018 17:44:59 +0100 Subject: [PATCH 11/14] issue #307: add SSH login banner to Docker containers --- tests/build_docker_images.py | 2 ++ tests/data/docker/ssh_login_banner.txt | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/data/docker/ssh_login_banner.txt diff --git a/tests/build_docker_images.py b/tests/build_docker_images.py index 32e3384b..7f856b2b 100755 --- a/tests/build_docker_images.py +++ b/tests/build_docker_images.py @@ -45,10 +45,12 @@ RUN yum clean all && \ DOCKERFILE = r""" COPY data/001-mitogen.sudo /etc/sudoers.d/001-mitogen +COPY data/docker/ssh_login_banner.txt /etc/ssh/banner.txt RUN \ chsh -s /bin/bash && \ mkdir -p /var/run/sshd && \ echo i-am-mitogen-test-docker-image > /etc/sentinel && \ + echo "Banner /etc/ssh/banner.txt" >> /etc/ssh/sshd_config && \ groupadd mitogen__sudo_nopw && \ useradd -s /bin/bash -m mitogen__has_sudo -G SUDO_GROUP && \ useradd -s /bin/bash -m mitogen__has_sudo_pubkey -G SUDO_GROUP && \ diff --git a/tests/data/docker/ssh_login_banner.txt b/tests/data/docker/ssh_login_banner.txt new file mode 100644 index 00000000..1ae4cd03 --- /dev/null +++ b/tests/data/docker/ssh_login_banner.txt @@ -0,0 +1,21 @@ +This banner tests Mitogen's ability to differentiate the word 'password' +appearing in a login banner, and 'password' appearing in a password prompt. + +This system is for the use of authorized users only. Individuals using this +computer system without authority or in excess of their authority are subject +to having all of their activities on this system monitored and recorded by +system personnel. + +In the course of monitoring this system with regard to any unauthorized or +improper use or in the course of system maintenance the system personnel may +have insights into regular business activity. + +Anyone using this system expressly consents to such monitoring and is advised +that if such monitoring reveals possible evidence of improper activity, system +personnel may provide the evidence of such monitoring to internal Compliance +and Security Officers who will - in the case of criminal offences - relay such +incidents to law enforcement officials. + +************************************************************** +NOTE: This system is connected to DOMAIN.COM, +please use your password. From 54a93f3c46e726675009d9241c38ecabcc5a9da8 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 17 Jul 2018 17:52:20 +0100 Subject: [PATCH 12/14] issue #307: suppress SSH login banner when verbosity is disabled. Login banner may include the password prompt, and there is no race-free way to detect the difference between the banner and the prompt. --- mitogen/ssh.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mitogen/ssh.py b/mitogen/ssh.py index eae70a36..5263e7f3 100644 --- a/mitogen/ssh.py +++ b/mitogen/ssh.py @@ -161,6 +161,11 @@ class Stream(mitogen.parent.Stream): bits = [self.ssh_path] if self.ssh_debug_level: bits += ['-' + ('v' * min(3, self.ssh_debug_level))] + else: + # issue #307: suppress any login banner, as it may contain the + # password prompt, and there is no robust way to tell the + # difference. + bits += ['-o', 'LogLevel ERROR'] if self.username: bits += ['-l', self.username] if self.port is not None: From 830a133ad60331d0ea439f8a2b74b1195d0c9062 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 17 Jul 2018 20:41:00 +0100 Subject: [PATCH 13/14] issue #307: require partial line when matching interactive prompt. This is a best-effort attempt to avoid SSHd banner spam from breaking our password entry loop. Closes #307. --- mitogen/ssh.py | 17 ++++++++++++++--- tests/ssh_test.py | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/mitogen/ssh.py b/mitogen/ssh.py index 5263e7f3..25928b45 100644 --- a/mitogen/ssh.py +++ b/mitogen/ssh.py @@ -61,7 +61,18 @@ def filter_debug(stream, it): This contains the mess of dealing with both line-oriented input, and partial lines such as the password prompt. + + Yields `(line, partial)` tuples, where `line` is the line, `partial` is + :data:`True` if no terminating newline character was present and no more + data exists in the read buffer. Consuming code can use this to unreliably + detect the presence of an interactive prompt. """ + # The `partial` test is unreliable, but is only problematic when verbosity + # is enabled: it's possible for a combination of SSH banner, password + # prompt, verbose output, timing and OS buffering specifics to create a + # situation where an otherwise newline-terminated line appears to not be + # terminated, due to a partial read(). If something is broken when + # ssh_debug_level>0, this is the first place to look. state = 'start_of_line' buf = b('') for chunk in it: @@ -86,7 +97,7 @@ def filter_debug(stream, it): state = 'start_of_line' elif state == 'in_plain': line, nl, buf = buf.partition(b('\n')) - yield line + nl + yield line + nl, not (nl or buf) if nl: state = 'start_of_line' @@ -237,7 +248,7 @@ class Stream(mitogen.parent.Stream): deadline=self.connect_deadline ) - for buf in filter_debug(self, it): + for buf, partial in filter_debug(self, it): LOG.debug('%r: received %r', self, buf) if buf.endswith(self.EC0_MARKER): self._router.broker.start_receive(self.tty_stream) @@ -255,7 +266,7 @@ class Stream(mitogen.parent.Stream): raise PasswordError(self.password_incorrect_msg) else: raise PasswordError(self.auth_incorrect_msg) - elif PASSWORD_PROMPT in buf.lower(): + elif partial and PASSWORD_PROMPT in buf.lower(): if self.password is None: raise PasswordError(self.password_required_msg) LOG.debug('%r: sending password', self) diff --git a/tests/ssh_test.py b/tests/ssh_test.py index 35f88cc8..a514c8ea 100644 --- a/tests/ssh_test.py +++ b/tests/ssh_test.py @@ -105,5 +105,24 @@ class SshTest(testlib.DockerMixin, unittest2.TestCase): ) +class BannerTest(testlib.DockerMixin, unittest2.TestCase): + # Verify the ability to disambiguate random spam appearing in the SSHd's + # login banner from a legitimate password prompt. + stream_class = mitogen.ssh.Stream + + def test_verbose_enabled(self): + context = self.docker_ssh( + username='mitogen__has_sudo', + password='has_sudo_password', + ssh_debug_level=3, + ) + name = 'ssh.%s:%s' % ( + self.dockerized_ssh.get_host(), + self.dockerized_ssh.port, + ) + self.assertEquals(name, context.name) + + + if __name__ == '__main__': unittest2.main() From 09d077ebd7e9c058f677430a47b4e2813c0128bc Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 17 Jul 2018 21:33:50 +0100 Subject: [PATCH 14/14] docs: update release notes --- docs/changelog.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0d296fc6..e853f6e8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,9 +18,24 @@ Release Notes v0.2.2 (2018-07-??) ------------------- +Mitogen for Ansible +~~~~~~~~~~~~~~~~~~~ + +* `#299 `_: fix the ``network_cli`` + connection type when the Mitogen strategy is active. + +Core Library +~~~~~~~~~~~~ + * `#303 `_: the ``doas`` become method is now supported. Contributed by Mike Walker. +* `#307 `_: SSH login banner output + containing the word 'password' is no longer confused for a password prompt. + +* Debug logs containing command lines are printed with the minimal quoting and + escaping required. + v0.2.1 (2018-07-10) -------------------