pyinfra-dev / pyinfra

pyinfra turns Python code into shell commands and runs them on your servers. Execute ad-hoc commands and write declarative operations. Target SSH servers, local machine and Docker containers. Fast and scales from one server to thousands.
https://pyinfra.com
MIT License
3.91k stars 382 forks source link

Can't configure the location of `pyinfra-sudo-askpass-XXXXXXX` file, sudo failing in environments with restricted /tmp script execution #880

Closed Renerick closed 2 years ago

Renerick commented 2 years ago

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

    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)

Nix (nix-shell to be specific), built from 2.4 source

--> 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)

Fizzadar commented 2 years ago

Thank you for raising this @Renerick! I think the updated askpass handling from #852 should indeed respect the temp dir config.

jaysoffian commented 2 years ago

Oh no, since my PR broke this I'll submit a PR to fix it.

Renerick commented 2 years ago

@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.

jaysoffian commented 2 years ago

@Renerick please check whether #905 fixes the issue for you.

Renerick commented 2 years ago

Thanks for the heads up! I'll have a look on this weekend

Renerick commented 2 years ago

@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.
jaysoffian commented 2 years ago

@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 ...
Renerick commented 2 years ago

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

jaysoffian commented 2 years ago

Thank you for the confirmation.