pyinfra
pyinfra copied to clipboard
Can't configure the location of `pyinfra-sudo-askpass-XXXXXXX` file, sudo failing in environments with restricted /tmp script execution
Describe the bug
When running a task with _sudo=True, _ask_sudo_password=<password>
in restricted environments where script execution from /tmp
is prohibited, sudo password prompt appears. Setting config.TEMP_DIR
in an attempt to place the file in other directory does not affect this behavior.
Related to #852
To Reproduce
server.shell(f'cd {tmp_dest} && docker-compose up -d', _sudo=True, _use_sudo_password=host.data._use_sudo_password)
In my particular case, this happens on a Synology NAS with DSM 7. There is only a single reference to this restriction in the entire Internet (https://community.synology.com/enu/forum/1/post/153704), and I have yet to find how to configure that. Nevertheless, this would happen on hosts with noexec
tmp mounts, so probably worth checking this out.
Expected behavior
Being able to configure the location of the password script with TEMP_DIR
config value or a new config value (like ASK_SUDO_PASSWORD_EXE_LOCATION
)
Meta
- Include output of
pyinfra --support
.
System: Linux
Platform: Linux-5.15.62-x86_64-with-glibc2.34
Release: 5.15.62
Machine: x86_64
pyinfra: v2.4
Executable: /nix/store/722hn0z9f2mrmw2lba3ayjk2f3pz592g-python3.9-pyinfra-2.1/bin/pyinfra
Python: 3.9.13 (CPython, GCC 11.3.0)
- How was pyinfra installed (source/pip)?
Nix (nix-shell to be specific), built from 2.4 source
- Include pyinfra-debug.log (if one was created)
- Consider including output with
-vv
and--debug
.
--> Loading config...
--> Loading inventory...
[pyinfra_cli.inventory] Creating fake inventory...
[pyinfra_cli.inventory] Checking possible group_data directory: /homelab
[pyinfra_cli.inventory] Checking possible group_data directory: /homelab/inventory
--> Connecting to hosts...
[pyinfra.connectors.ssh] Connecting to: synology ({'allow_agent': True, 'look_for_keys': True, 'hostname': '[REDACTED]', '_pyinfra_ssh_forward_agent': None, '_pyinfra_ssh_config_file': None, '_pyinfra_ssh_known_hosts_file': None, '_pyinfra_ssh_strict_host_key_checking': None, '_pyinfra_ssh_paramiko_connect_kwargs': None, 'username': '[REDACTED]', 'port': [REDACTED], 'timeout': 10, 'password': '[REDACTED]'})
[pyinfra.connectors.sshuserclient.client] Loading SSH config: None
[synology] Connected
[pyinfra.api.state] Activating host: synology
--> Preparing operations...
Loading: deploys/synology/[REDACTED].py
[pyinfra.api.host] Starting deploy Deploy docker compose file (args={'sudo': False, 'sudo_user': None, 'use_sudo_login': False, 'use_sudo_password': '[REDACTED]', 'preserve_sudo_env': False, 'su_user': None, 'use_su_login': False, 'preserve_su_env': False, 'su_shell': None, 'doas': None, 'doas_user': None, 'shell_executable': 'sh', 'chdir': None, 'env': {}, 'success_exit_codes': [0], 'timeout': None, 'get_pty': None, 'stdin': None, 'name': None, 'ignore_errors': False, 'continue_on_error': False, 'precondition': None, 'postcondition': None, 'on_success': None, 'on_error': None, 'parallel': 2, 'run_once': False, 'serial': False}, data=None)
[pyinfra.api.operation] Adding operation, {'Deploy docker compose file | Files/Template'}, opOrder=(0, 7, 9), opHash=ce735ef7f0d2bdf463e7b274d1af100e588930c8
[pyinfra.api.facts] Getting fact: files.File (path=./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml) (ensure_hosts: None)
[pyinfra.connectors.ssh] Running command on synology: (pty=False) sh -c '
temp=$(mktemp /tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX)
cat >"$temp"<<'"'"'__EOF__'"'"'
#!/bin/sh
printf '"'"'%s\n'"'"' "$PYINFRA_SUDO_PASSWORD"
__EOF__
chmod 755 "$temp"
echo "$temp"
'
[pyinfra.connectors.ssh] Waiting for exit status...
[pyinfra.connectors.ssh] Command exit status: 0
[pyinfra.connectors.ssh] Running command on synology: (pty=None) env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-gCs6HOfttNgK *** sh -c '! (test -e ./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml || test -L ./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml ) || ( stat -c '"'"'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"'"' ./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml 2> /dev/null || stat -f '"'"'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"'"' ./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml )'
[pyinfra.connectors.ssh] Waiting for exit status...
[pyinfra.connectors.ssh] Command exit status: 0
[pyinfra.api.facts] [synology] Loaded fact files.File (path=./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml)
[pyinfra.api.facts] Getting fact: files.Directory (path=./tmp/[REDACTED]/docker-compose.yml) (ensure_hosts: None)
[pyinfra.connectors.ssh] Running command on synology: (pty=None) env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-gCs6HOfttNgK *** sh -c '! (test -e ./tmp/[REDACTED]/docker-compose.yml || test -L ./tmp/[REDACTED]/docker-compose.yml ) || ( stat
-c '"'"'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"'"' ./tmp/[REDACTED]/docker-compose.yml 2> /dev/null || stat -f '"'"'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"'"' ./tmp/[REDACTED]/docker-compose.yml )'
[pyinfra.connectors.ssh] Waiting for exit status...
[pyinfra.connectors.ssh] Command exit status: 0
[pyinfra.api.facts] [synology] Loaded fact files.Directory (path=./tmp/[REDACTED]/docker-compose.yml)
[pyinfra.api.facts] Getting fact: files.Sha1File (path=./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml) (ensure_hosts: None)
[pyinfra.connectors.ssh] Running command on synology: (pty=None) env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-gCs6HOfttNgK *** sh -c 'test -e ./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml && ( sha1sum ./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml 2> /dev/null || shasum ./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml 2> /dev/null || sha1 ./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml ) || true'
[pyinfra.connectors.ssh] Waiting for exit status...
[pyinfra.connectors.ssh] Command exit status: 0
[pyinfra.api.facts] [synology] Loaded fact files.Sha1File (path=./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml)
[pyinfra.api.host] [synology] noop: file ./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml is already uploaded
[pyinfra.api.operation] Adding operation, {'Deploy docker compose file | Server/Shell'}, opOrder=(0, 7, 16), opHash=4613229f58e989b5caf7c3eb0b4b6a2674276978
[pyinfra.api.operation] Adding operation, {'Deploy docker compose file | Files/File'}, opOrder=(0, 7, 18), opHash=9eb8fef4b14176505e5ff37f9abe8592215b262a
[pyinfra.api.host] Reset deploy to None (args=None, data=None)
[synology] Ready: deploys/synology/[REDACTED].py
--> Proposed changes:
Groups: inventory / infra
[synology] Operations: 3 Change: 2 No change: 1
--> Beginning operation run...
--> Starting operation: Deploy docker compose file | Files/Template (deploys/synology/docker-compose/[REDACTED].yml, ./tmp/[REDACTED]/docker-compose.yml/docker-compose.yml, create_remote_dir=True)
[pyinfra.api.operations] Starting operation Deploy docker compose file | Files/Template on synology
[synology] No changes
--> Starting operation: Deploy docker compose file | Server/Shell (cd ./tmp/[REDACTED]/docker-compose.yml && docker-compose up -d)
[pyinfra.api.operations] Starting operation Deploy docker compose file | Server/Shell on synology
[pyinfra.connectors.ssh] Running command on synology: (pty=None) env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-TF51VUsf7Fqy *** sudo -H -A -k sh -c 'cd ./tmp/[REDACTED]/docker-compose.yml && docker-compose up -d'
[pyinfra.connectors.ssh] Waiting for exit status...
[pyinfra.connectors.ssh] Command exit status: 1
[synology] sudo password:
[pyinfra.connectors.ssh] Running command on synology: (pty=None) env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-TF51VUsf7Fqy *** sudo -H -A -k sh -c 'cd ./tmp/[REDACTED]/docker-compose.yml && docker-compose up -d'
[pyinfra.connectors.ssh] Waiting for exit status...
[pyinfra.connectors.ssh] Command exit status: 1
[synology] sudo: unable to run /tmp/pyinfra-sudo-askpass-TF51VUsf7Fqy: Permission denied
[synology] sudo: no password was provided
[synology] sudo: a password is required
[synology] Error: executed 0/1 commands
[pyinfra.api.state] Failing hosts: synology
[pyinfra.connectors.ssh] Running command on synology: (pty=False) sh -c 'rm -f /tmp/pyinfra-sudo-askpass-TF51VUsf7Fqy' [pyinfra.connectors.ssh] Waiting for exit status...
[pyinfra.connectors.ssh] Command exit status: 0
--> pyinfra error: No hosts remaining!
(there are some weird directory structure in the logs above, please disregard, most likely a bug in my scripts)
Thank you for raising this @Renerick! I think the updated askpass handling from #852 should indeed respect the temp dir config.
Oh no, since my PR broke this I'll submit a PR to fix it.
@jaysoffian Thanks! For the record, you didn't "break" it necessarily (in fact, your PR was 110% reasonable), more like an unintended consequence that only appears in VERY specific circumstances.
@Renerick please check whether #905 fixes the issue for you.
Thanks for the heads up! I'll have a look on this weekend
@jaysoffian Unfortunately, this change breaks sudo completely as Synology default shell does not seem to support these string substituions
[REDACTED]@synology-1:~$ mktemp "${{TMPDIR:=/tmp}}/pyinfra-sudo-askpass-XXXXXXXXXXXX"
-sh: ${{TMPDIR:=/tmp}}/pyinfra-sudo-askpass-XXXXXXXXXXXX: bad substitution
[REDACTED]@synology-1:~$ echo "${{HOME}}"
-sh: ${{HOME}}: bad substitution
[REDACTED]@synology-1:~$ echo $HOME
/[REDACTED] # correct directory
$ sh --version
GNU bash, version 4.4.23(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
@Renerick it looks like you're copy/pasting the code from the diff instead of running the new code.
The issue there is that the double braces are a Python escape mechanism for formatting strings, so the correct code is actually this:
mktemp "${TMPDIR:=/tmp}/pyinfra-sudo-askpass-XXXXXXXXXXXX"
I know that the new code works correctly with bash (even when run in POSIX mode as sh) because I tested it.
Please install pyinfra from my PR and test it that way. e.g.
python3 -m venv pyinfra-test
pyinfra-test/bin/python -m pip install 'git+https://github.com/jaysoffian/pyinfra@make-sudo-askpass-command-respect-tmpdir-env-var'
pyinfra-test/bin/pyinfra ...
yes, you are absolutely correct, my bad.
I did install pyifnra from your commit via nix-shell, so there was no problem on this side
let pkgs = import <nixpkgs> {
overlays = [
(self: super: {
pyinfra = super.pyinfra.overrideAttrs (old: {
src = super.fetchFromGitHub {
owner = "Fizzadar";
repo = old.pname;
rev = "7ce69211d840a5712969e55abcf16576db258e05";
hash = "sha256-p8goMspSC+XuuPyXM5SmV4LpW+JYQ8TA/HuOEV7BgCc=";
};
});
})
];
};
What happened is, after installation it did not work. sudo password prompt still appeared and I tried to debug the installation. In the process, as you correctly noted, I erroneously tried to run the command by copying it from the diff and came to the wrong conclusion about this patch being broken.
I just retested it again and It works! I'm not quite sure why it did not work the first time, but the fix is 100% working for me right now, so I will blame this on some environment related fluke.
Huge thanks for your help!
cc: @Fizzadar
Thank you for the confirmation.