Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

--ask-become-pass not working when fingerprint is enabled (not MFA) #80883

Open
1 task done
DevilPepper opened this issue May 25, 2023 · 3 comments
Open
1 task done
Labels
affects_2.14 bug This issue/PR relates to a bug. P3 Priority 3 - Approved, No Time Limitation

Comments

@DevilPepper
Copy link

Summary

If you've enabled auth with a fingerprint reader, ansible-playbook just hangs when it needs to become root.

I found this issue that was dismissed as "Ansible does not support MFA": #73308

I looked through the mailing list and didn't see this person ask there.

But this is not about MFA. sudo asks you for a fingerprint and after a set timeout (10 seconds) falls back to password entry. The key here is that Ansible does not need to support fingerprint auth (although it would be nice), but it could wait for the timeout and then enter the password when prompted for it. But today, you can wait many times this timeout and ansible-playbook just hangs there. Below, I'll give very detailed repro steps.

Workaround

I can clone my provision repo into the target machine, physically log in to it, change the host field from all to localhost, and run the playbook from there. I'm actually doing that for all the output below. The same bug presents itself BUT when ansible-playbook hangs trying to elevate, I can touch the fingerprint reader and that unblocks it 😆. Ansible outputs a warning about it. Here is the output of this workaround:

ansible-playbook [core 2.14.6]
  config file = /home/stuff/code/provision/ansible.cfg
  configured module search path = ['/home/stuff/.config/ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/stuff/code/provision/.venv/lib/python3.9/site-packages/ansible
  ansible collection location = /home/stuff/.config/ansible/collections:/usr/share/ansible/collections
  executable location = /home/stuff/code/provision/.venv/bin/ansible-playbook
  python version = 3.9.2 (default, Feb 28 2021, 17:03:44) [GCC 10.2.1 20210110] (/home/stuff/code/provision/.venv/bin/python3)
  jinja version = 3.1.2
  libyaml = True
Using /home/stuff/code/provision/ansible.cfg as config file
BECOME password: 
setting up inventory plugins
host_list declined parsing /home/stuff/code/provision/inventory.yaml as it did not pass its verify_file() method
script declined parsing /home/stuff/code/provision/inventory.yaml as it did not pass its verify_file() method
Parsed /home/stuff/code/provision/inventory.yaml inventory source with yaml plugin
Loading callback plugin default of type stdout, v2.0 from /home/stuff/code/provision/.venv/lib/python3.9/site-packages/ansible/plugins/callback/default.py
Skipping callback 'default', as we already have a stdout callback.
Skipping callback 'minimal', as we already have a stdout callback.
Skipping callback 'oneline', as we already have a stdout callback.

PLAYBOOK: sudo_hang.yaml ****************************************************************************************************************
Positional arguments: playbooks/sudo_hang.yaml
verbosity: 4
remote_user: stuff
connection: smart
timeout: 10
become_method: sudo
become_ask_pass: True
tags: ('all',)
inventory: ('/home/stuff/code/provision/inventory.yaml',)
forks: 5
1 plays in playbooks/sudo_hang.yaml

PLAY [Repro sudo hang] ******************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************
task path: /home/stuff/code/provision/playbooks/sudo_hang.yaml:2
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: stuff
<127.0.0.1> EXEC /bin/sh -c 'echo ~stuff && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/stuff/.ansible/tmp `"&& mkdir "` echo /home/stuff/.ansible/tmp/ansible-tmp-1684983160.0777464-9783-91825797620863 `" && echo ansible-tmp-1684983160.0777464-9783-91825797620863="` echo /home/stuff/.ansible/tmp/ansible-tmp-1684983160.0777464-9783-91825797620863 `" ) && sleep 0'
Using module file /home/stuff/code/provision/.venv/lib/python3.9/site-packages/ansible/modules/setup.py
<127.0.0.1> PUT /home/stuff/.config/ansible/tmp/ansible-local-9779fz2uhwrs/tmpofwndx3m TO /home/stuff/.ansible/tmp/ansible-tmp-1684983160.0777464-9783-91825797620863/AnsiballZ_setup.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/stuff/.ansible/tmp/ansible-tmp-1684983160.0777464-9783-91825797620863/ /home/stuff/.ansible/tmp/ansible-tmp-1684983160.0777464-9783-91825797620863/AnsiballZ_setup.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/home/stuff/code/provision/.venv/bin/python3 /home/stuff/.ansible/tmp/ansible-tmp-1684983160.0777464-9783-91825797620863/AnsiballZ_setup.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'rm -f -r /home/stuff/.ansible/tmp/ansible-tmp-1684983160.0777464-9783-91825797620863/ > /dev/null 2>&1 && sleep 0'
ok: [localhost]

TASK [Create a file] ********************************************************************************************************************
task path: /home/stuff/code/provision/playbooks/sudo_hang.yaml:5
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: stuff
<127.0.0.1> EXEC /bin/sh -c 'echo ~stuff && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/stuff/.ansible/tmp `"&& mkdir "` echo /home/stuff/.ansible/tmp/ansible-tmp-1684983160.892622-9851-9053023515229 `" && echo ansible-tmp-1684983160.892622-9851-9053023515229="` echo /home/stuff/.ansible/tmp/ansible-tmp-1684983160.892622-9851-9053023515229 `" ) && sleep 0'
Using module file /home/stuff/code/provision/.venv/lib/python3.9/site-packages/ansible/modules/file.py
<127.0.0.1> PUT /home/stuff/.config/ansible/tmp/ansible-local-9779fz2uhwrs/tmpk3uoim_p TO /home/stuff/.ansible/tmp/ansible-tmp-1684983160.892622-9851-9053023515229/AnsiballZ_file.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/stuff/.ansible/tmp/ansible-tmp-1684983160.892622-9851-9053023515229/ /home/stuff/.ansible/tmp/ansible-tmp-1684983160.892622-9851-9053023515229/AnsiballZ_file.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'sudo -H -S -p "[sudo via ansible, key=rbdbbsgdapqaynwmzsbfjookiyxlshwr] password:" -u root /bin/sh -c '"'"'echo BECOME-SUCCESS-rbdbbsgdapqaynwmzsbfjookiyxlshwr ; /home/stuff/code/provision/.venv/bin/python3 /home/stuff/.ansible/tmp/ansible-tmp-1684983160.892622-9851-9053023515229/AnsiballZ_file.py'"'"' && sleep 0'
[WARNING]: Module invocation had junk after the JSON data: Place your finger on the fingerprint reader
<127.0.0.1> EXEC /bin/sh -c 'rm -f -r /home/stuff/.ansible/tmp/ansible-tmp-1684983160.892622-9851-9053023515229/ > /dev/null 2>&1 && sleep 0'
changed: [localhost] => {
    "changed": true,
    "dest": "/root/DELETE_ME",
    "diff": {
        "after": {
            "atime": 1684983163.059274,
            "mtime": 1684983163.059274,
            "path": "/root/DELETE_ME",
            "state": "touch"
        },
        "before": {
            "atime": 1684983163.0549607,
            "mtime": 1684983163.0549607,
            "path": "/root/DELETE_ME",
            "state": "absent"
        }
    },
    "gid": 0,
    "group": "root",
    "invocation": {
        "module_args": {
            "_diff_peek": null,
            "_original_basename": null,
            "access_time": null,
            "access_time_format": "%Y%m%d%H%M.%S",
            "attributes": null,
            "follow": true,
            "force": false,
            "group": null,
            "mode": null,
            "modification_time": null,
            "modification_time_format": "%Y%m%d%H%M.%S",
            "owner": null,
            "path": "/root/DELETE_ME",
            "recurse": false,
            "selevel": null,
            "serole": null,
            "setype": null,
            "seuser": null,
            "src": null,
            "state": "touch",
            "unsafe_writes": false
        }
    },
    "mode": "0644",
    "owner": "root",
    "size": 0,
    "state": "file",
    "uid": 0
}

PLAY RECAP ******************************************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[WARNING]: Module invocation had junk after the JSON data: Place your finger on the fingerprint reader

Issue Type

Bug Report

Component Name

sudo? become: true

Ansible Version

$ ansible --version
ansible [core 2.14.6]
  config file = /home/stuff/code/provision/ansible.cfg
  configured module search path = ['/home/stuff/.config/ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/stuff/code/provision/.venv/lib/python3.9/site-packages/ansible
  ansible collection location = /home/stuff/.config/ansible/collections:/usr/share/ansible/collections
  executable location = /home/stuff/code/provision/.venv/bin/ansible
  python version = 3.9.2 (default, Feb 28 2021, 17:03:44) [GCC 10.2.1 20210110] (/home/stuff/code/provision/.venv/bin/python3)
  jinja version = 3.1.2
  libyaml = True

Configuration

# if using a version older than ansible-core 2.12 you should omit the '-t all'
$ ansible-config dump --only-changed -t all
ANSIBLE_HOME(env: ANSIBLE_HOME) = /home/stuff/.config/ansible
ANSIBLE_NOCOWS(/home/stuff/code/provision/ansible.cfg) = True
CONFIG_FILE() = /home/stuff/code/provision/ansible.cfg
DEFAULT_HOST_LIST(/home/stuff/code/provision/ansible.cfg) = ['/home/stuff/code/provision/inventory.yaml']
DEFAULT_ROLES_PATH(/home/stuff/code/provision/ansible.cfg) = ['/home/stuff/code/provision/roles']

OS / Environment

  • Debian Bullseye
  • Lenovo ThinkPad X1 Yoga Gen 7 (Not that I think this is relevant. It just has a fingerprint reader)
  • fprintd is enabled for auth, but so is password

Steps to Reproduce

If you have a fingerprint reader (required) and haven't enabled it yet, on Debian you can do this:

sudo apt install fprintd libpam-fprintd
sudo pam-auth-update --package --enable fprintd
sudo fprintd-enroll $USER

In /etc/pam.d/common-auth, pam-auth-update would have added a line like this:

auth	[success=2 default=ignore]	pam_fprintd.so max_tries=1 timeout=10 # debug

Now you can log in and sudo with your fingerprint. Here's a playbook that does something with elevated permissions:

---
- name: Repro sudo hang
  hosts: all
  tasks:
  - name: Create a file
    become: true
    ansible.builtin.file:
      path: /root/DELETE_ME
      state: touch

I would run this like:

TARGET_HOST="targethost.lan"
ansible-playbook -u $USER playbooks/sudo_hang.yaml -K --limit $TARGET_HOST

Expected Results

I expect the playbook to run to completion. Once the fingerprint entry times out, it asks for the password. That's when Ansible can entry the provided password.

Actual Results

ansible-playbook [core 2.14.6]
  config file = /home/stuff/code/provision/ansible.cfg
  configured module search path = ['/home/stuff/.config/ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/stuff/code/provision/.venv/lib/python3.9/site-packages/ansible
  ansible collection location = /home/stuff/.config/ansible/collections:/usr/share/ansible/collections
  executable location = /home/stuff/code/provision/.venv/bin/ansible-playbook
  python version = 3.9.2 (default, Feb 28 2021, 17:03:44) [GCC 10.2.1 20210110] (/home/stuff/code/provision/.venv/bin/python3)
  jinja version = 3.1.2
  libyaml = True
Using /home/stuff/code/provision/ansible.cfg as config file
BECOME password: 
setting up inventory plugins
host_list declined parsing /home/stuff/code/provision/inventory.yaml as it did not pass its verify_file() method
script declined parsing /home/stuff/code/provision/inventory.yaml as it did not pass its verify_file() method
Parsed /home/stuff/code/provision/inventory.yaml inventory source with yaml plugin
Loading callback plugin default of type stdout, v2.0 from /home/stuff/code/provision/.venv/lib/python3.9/site-packages/ansible/plugins/callback/default.py
Skipping callback 'default', as we already have a stdout callback.
Skipping callback 'minimal', as we already have a stdout callback.
Skipping callback 'oneline', as we already have a stdout callback.

PLAYBOOK: sudo_hang.yaml ****************************************************************************************************************
Positional arguments: playbooks/sudo_hang.yaml
verbosity: 4
remote_user: stuff
connection: smart
timeout: 10
become_method: sudo
become_ask_pass: True
tags: ('all',)
inventory: ('/home/stuff/code/provision/inventory.yaml',)
forks: 5
1 plays in playbooks/sudo_hang.yaml

PLAY [Repro sudo hang] ******************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************
task path: /home/stuff/code/provision/playbooks/sudo_hang.yaml:2
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: stuff
<127.0.0.1> EXEC /bin/sh -c 'echo ~stuff && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/stuff/.ansible/tmp `"&& mkdir "` echo /home/stuff/.ansible/tmp/ansible-tmp-1684983071.0286577-9619-247928224759300 `" && echo ansible-tmp-1684983071.0286577-9619-247928224759300="` echo /home/stuff/.ansible/tmp/ansible-tmp-1684983071.0286577-9619-247928224759300 `" ) && sleep 0'
Using module file /home/stuff/code/provision/.venv/lib/python3.9/site-packages/ansible/modules/setup.py
<127.0.0.1> PUT /home/stuff/.config/ansible/tmp/ansible-local-9615uv3gb8yw/tmp6iy014gs TO /home/stuff/.ansible/tmp/ansible-tmp-1684983071.0286577-9619-247928224759300/AnsiballZ_setup.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/stuff/.ansible/tmp/ansible-tmp-1684983071.0286577-9619-247928224759300/ /home/stuff/.ansible/tmp/ansible-tmp-1684983071.0286577-9619-247928224759300/AnsiballZ_setup.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/home/stuff/code/provision/.venv/bin/python3 /home/stuff/.ansible/tmp/ansible-tmp-1684983071.0286577-9619-247928224759300/AnsiballZ_setup.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'rm -f -r /home/stuff/.ansible/tmp/ansible-tmp-1684983071.0286577-9619-247928224759300/ > /dev/null 2>&1 && sleep 0'
ok: [localhost]

TASK [Create a file] ********************************************************************************************************************
task path: /home/stuff/code/provision/playbooks/sudo_hang.yaml:5
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: stuff
<127.0.0.1> EXEC /bin/sh -c 'echo ~stuff && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/stuff/.ansible/tmp `"&& mkdir "` echo /home/stuff/.ansible/tmp/ansible-tmp-1684983071.8399367-9686-230194161258648 `" && echo ansible-tmp-1684983071.8399367-9686-230194161258648="` echo /home/stuff/.ansible/tmp/ansible-tmp-1684983071.8399367-9686-230194161258648 `" ) && sleep 0'
Using module file /home/stuff/code/provision/.venv/lib/python3.9/site-packages/ansible/modules/file.py
<127.0.0.1> PUT /home/stuff/.config/ansible/tmp/ansible-local-9615uv3gb8yw/tmp928oek4o TO /home/stuff/.ansible/tmp/ansible-tmp-1684983071.8399367-9686-230194161258648/AnsiballZ_file.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/stuff/.ansible/tmp/ansible-tmp-1684983071.8399367-9686-230194161258648/ /home/stuff/.ansible/tmp/ansible-tmp-1684983071.8399367-9686-230194161258648/AnsiballZ_file.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'sudo -H -S -p "[sudo via ansible, key=jbjszufckjypwrbozwjzefymkllkcmrp] password:" -u root /bin/sh -c '"'"'echo BECOME-SUCCESS-jbjszufckjypwrbozwjzefymkllkcmrp ; /home/stuff/code/provision/.venv/bin/python3 /home/stuff/.ansible/tmp/ansible-tmp-1684983071.8399367-9686-230194161258648/AnsiballZ_file.py'"'"' && sleep 0'

Code of Conduct

  • I agree to follow the Ansible Code of Conduct
@ansibot
Copy link
Contributor

ansibot commented May 25, 2023

Files identified in the description:

  • lib/ansible/playbook/become.py

If these files are incorrect, please update the component name section of the description or use the !component bot command.

click here for bot help

@ansibot ansibot added affects_2.14 bug This issue/PR relates to a bug. needs_triage Needs a first human triage before being processed. labels May 25, 2023
@sivel
Copy link
Member

sivel commented May 25, 2023

I've done some investigation, since I was pretty sure the ssh connection plugin could handle this, but I see you are using localhost, and thus likely the local connection plugin.

As such, I created a fake sudo exe that I could use to test:

#!/bin/bash
echo "Place your finger on the fingerprint reader"
sleep 10
exec sudo "$@"

And this works as I expected with the ssh plugin. Testing the local connection plugin I ran into the described hang. Looking at the local connection plugin I see why it doesn't work.

The local plugin effectively will ever only attempt to read from stdout/stderr 1 time, and then will switch back to Popen.communciate which won't then be used for become prompt matching.

Here is a diff that seems to resolve the issue:

diff --git a/lib/ansible/plugins/connection/local.py b/lib/ansible/plugins/connection/local.py
index 0c769a4b2838e7..18ca7252164322 100644
--- a/lib/ansible/plugins/connection/local.py
+++ b/lib/ansible/plugins/connection/local.py
@@ -24,6 +24,7 @@
 import pty
 import shutil
 import subprocess
+import time
 
 import ansible.constants as C
 from ansible.errors import AnsibleError, AnsibleFileNotFound
@@ -123,22 +124,18 @@ def exec_command(self, cmd, in_data=None, sudoable=True):
 
             become_output = b''
             try:
+                start = time.time()
                 while not self.become.check_success(become_output) and not self.become.check_password_prompt(become_output):
-                    events = selector.select(self._play_context.timeout)
-                    if not events:
-                        stdout, stderr = p.communicate()
+                    if time.time() - start >= self._play_context.timeout:
                         raise AnsibleError('timeout waiting for privilege escalation password prompt:\n' + to_native(become_output))
+                    events = selector.select(1)
 
                     for key, event in events:
-                        if key.fileobj == p.stdout:
-                            chunk = p.stdout.read()
-                        elif key.fileobj == p.stderr:
-                            chunk = p.stderr.read()
-
-                    if not chunk:
-                        stdout, stderr = p.communicate()
-                        raise AnsibleError('privilege output closed while waiting for password prompt:\n' + to_native(become_output))
-                    become_output += chunk
+                        chunk = key.fileobj.read()
+                        if not chunk:
+                            selector.unregister(key.fileobj)
+                        become_output += chunk
+                        display.debug("output chunk:\n>>>%s<<<\n" % to_text(chunk))
             finally:
                 selector.close()
 

I'm not immediately planning on working on this further right now, so this can be picked up by anyone who wants to test further, and put together a proper PR.

@nitzmahone nitzmahone added P3 Priority 3 - Approved, No Time Limitation and removed needs_triage Needs a first human triage before being processed. labels May 25, 2023
@DevilPepper
Copy link
Author

DevilPepper commented May 26, 2023

Thanks so much for looking into it so fast! I could try to find some time next week to test it and open a PR.

So about ssh: it must be the demo gods or something... I just ran the playbook over ssh to copy the output... and it worked. 10 seconds passed and the playbook was able to continue. I promise this wasn't working all week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
affects_2.14 bug This issue/PR relates to a bug. P3 Priority 3 - Approved, No Time Limitation
Projects
None yet
Development

No branches or pull requests

4 participants