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.88k stars 378 forks source link

sudo password #305

Closed pathcl closed 4 years ago

pathcl commented 4 years ago

Is your feature request related to a problem? Please describe. So far I can't find examples/documentation if users with password for sudo can be used

Describe the solution you'd like I'd like to enter a password to use for sudo just like other cfmgt tools

Fizzadar commented 4 years ago

So this should be possible using the stdin (global) argument via https://github.com/Fizzadar/pyinfra/pull/231. I tested with sudo at the time and was working as expected.

Would be good to have an example of this and also perhaps an example in the docs/example deploys section, will leave this ticket open to track adding those.

Bystroushaak commented 4 years ago

Hi, I am having a same problem (I think); sudo with password doesn't work, even tho I can log to the user via ssh.

So, the sudo password is not implemented at the moment? Why it is not taken from the ssh_password?

Fizzadar commented 4 years ago

@Bystroushaak have you tried using the stdin argument when calling the operation?

The issue here is there's no sane way to detect when sudo asks for a password. The only real option is to blindly pipe the password into the underlying command (using stdin).

There was an old issue (https://github.com/Fizzadar/pyinfra/issues/40) that looks at some approaches. Unfortunately no sensible option aside from stdin was found :/

pathcl commented 4 years ago

I haven't looked how ansible implements this feature but we could get ideas from there

Bystroushaak commented 4 years ago

I didn't. I'll try that later, but for now I just disabled the sudo password.

The issue here is there's no sane way to detect when sudo asks for a password.

Why? I've read the #40, but I don't understand why there isn't any way how to detect this. It is possible to specify your own command prompt for password, so it should be parse-able:

     -p prompt, --prompt=prompt
                 Use a custom password prompt with optional escape sequences.
                 The following percent (‘%’) escape sequences are supported by
                 the sudoers policy:

                 %H  expanded to the host name including the domain name (on
                     if the machine's host name is fully qualified or the fqdn
                     option is set in sudoers(5))

                 %h  expanded to the local host name without the domain name

                 %p  expanded to the name of the user whose password is being
                     requested (respects the rootpw, targetpw, and runaspw
                     flags in sudoers(5))

                 %U  expanded to the login name of the user the command will
                     be run as (defaults to root unless the -u option is also
                     specified)

                 %u  expanded to the invoking user's login name

                 %%  two consecutive ‘%’ characters are collapsed into a sin‐
                     gle ‘%’ character

                 The custom prompt will override the default prompt specified
                 by either the security policy or the SUDO_PROMPT environment
                 variable.  On systems that use PAM, the custom prompt will
                 also override the prompt specified by a PAM module unless the
                 passprompt_override flag is disabled in sudoers.

Env vars:

     SUDO_PROMPT      Used as the default password prompt unless the -p option
                      was specified.

Or is there some kind of issue with stdin parsing?

It should be also possible to use --askpass to specify your own script as an answer for the password request.

     -A, --askpass
                 Normally, if sudo requires a password, it will read it from
                 the user's terminal.  If the -A (askpass) option is speci‐
                 fied, a (possibly graphical) helper program is executed to
                 read the user's password and output the password to the stan‐
                 dard output.  If the SUDO_ASKPASS environment variable is
                 set, it specifies the path to the helper program.  Otherwise,
                 if sudo.conf(5) contains a line specifying the askpass pro‐
                 gram, that value will be used.  For example:

                     # Path to askpass helper program
                     Path askpass /usr/X11R6/bin/ssh-askpass

                 If no askpass program is available, sudo will exit with an
                 error.

Env vars:

     SUDO_ASKPASS     Specifies the path to a helper program used to read the
                      password if no terminal is available or if the -A option
                      is specified.
Bystroushaak commented 4 years ago

I have a user in the virtualbox called b with password b. I've created a /home/b/test.sh with following content:

#! /usr/bin/env bash
echo "b"

Then I can run sudo and verify, that it will ask me for password:

b@virtual:~$ sudo ls
[sudo] password for b: 
b@virtual:~$

I just hit ctrl+c, so the password is not given and sudo shall ask for the password next time.

Then I add the env var and the -A parameter:

b@virtual:~$ SUDO_ASKPASS=/home/b/test.sh sudo -A ls
test.sh

test.sh is the listing of the directory.

So, it should be possible to just use something like this to provide the password.

Fizzadar commented 4 years ago

@Bystroushaak so the prompt issue is because pyinfra reads lines from the output of commands (rather than bytes). I reimplemented reading bytes (https://github.com/Fizzadar/pyinfra/commit/58852a53c8e7a82ba65adccbd45347e55f9de5e5) but it's messy/slow.

The SUDO_ASKPASS looks perfect, however! I was not aware of this feature. I can see this working as follows:

Will have the follow this process for each individual command, and add a global sudo_password argument to all operations.

Bystroushaak commented 4 years ago

The SUDO_ASKPASS looks perfect, however! I was not aware of this feature. I can see this working as follows:

* Use with the `SUDO_ASKPASS` environment variable

* Remove the file after

I would maybe add removal of the file to the shell file itself, so if the connection / parsing / whatever fails, there is not a file with password left on the device.

Bystroushaak commented 4 years ago

@Bystroushaak so the prompt issue is because pyinfra reads lines from the output of commands (rather than bytes). I reimplemented reading bytes (58852a5) but it's messy/slow.

@Fizzadar BTW, just for inspiration, you may be interested in the http://amoffat.github.io/sh/ module.

Bystroushaak commented 4 years ago

Will have the follow this process for each individual command, and add a global sudo_password argument to all operations.

Also if you have ssh password / user password, then this is same.

Fizzadar commented 4 years ago

OK so thought about this some more - uploading the password into a file and using ASKPASS is, IMO, too risky. The failure handling in case of error will make it all but impossible to guarantee the file is removed. Additionally I would like to avoid/prevent people simply putting passwords into deploy code.

(I also figured out why stdin doesn't work - pyinfra disables interactive input to sudo (https://github.com/Fizzadar/pyinfra/blob/master/pyinfra/api/connectors/util.py#L180, -n). Removing this fixes passing the password via stdin but this is a hack and presents the same problem above with writing passwords into deploys.)

So to make sure this is secure I think the best approach has to be getting the password interactively, for which I currently see two approaches:

  1. Add a global ask_sudo_password=True argument. When we reach the operation then prompt the user to enter the password and then echo $PWD | sudo ... into the shell command pyinfra executes.
    • This is simple and prevents people inputting the password in deploy code/CLI.
    • Problem here is if sudo does not require a password the password will be echoed into the underlying command. BUT perhaps that's really the responsibility of the person writing the deploy!
  2. Define a custom sudo prompt and read the first X characters from stderr stream of new commands, if we find the prompt then as the user for the password, and write it back into the command.
    • This should be possible and feels like the cleanest approach (only sends password when explicitly requested).
    • BUT this means fiddling with reading from subprocess pipes + paramiko channels, which can get messy (esp when we need line buffered output later on).

Further context, just dug into the sh library which takes the first approach (https://github.com/amoffat/sh/blob/develop/sh.py#L3388).

normoes commented 4 years ago

I follow this issue closely. This is a feature I am looking forward to.

Do you need some help implementing this or in general?

Bystroushaak commented 4 years ago

Another possibility would be to use SUDO_ASKPASS, but put password into env variable, and then let it delete itself.

Problem here is if sudo does not require a password the password will be echoed into the underlying command. BUT perhaps that's really the responsibility of the person writing the deploy!

I've tried it yesterday and this can be dangerous and almost unknowable for the user (depending on the sudo settings, it may or may not remember the password for a while, also when someone changes the setting so that the user won't need password anymore, this will put the password into stdin of every command).

Fizzadar commented 4 years ago

Another possibility would be to use SUDO_ASKPASS, but put password into env variable, and then let it delete itself.

I think we have a winner! Generic askpass function that uses and envvar - so we get the best of both appproaches, will confirm with tests ASAP.

I've tried it yesterday and this can be dangerous and almost unknowable for the user (depending on the sudo settings, it may or may not remember the password for a while, also when someone changes the setting so that the user won't need password anymore, this will put the password into stdin of every command).

There's a -k flag that can be passed to sudo to ignore previous attempts such that it always asks for the password. Still doesn't capture the case where some commands might be NOPASSWD-enabled while others not. Luckily the askpass approach should cover all cases :)

Fizzadar commented 4 years ago

Further notes, first implementation will be explicit:

Possible future work/implicit handling:

Fizzadar commented 4 years ago

First pass implementation: https://github.com/Fizzadar/pyinfra/commit/84d86446181265a4687830a52f30c201fe3da56e

Should be usable like so:

pyinfra @local server.shell 'echo hello' sudo=true use_sudo_password=true
Bystroushaak commented 4 years ago

I've tried it and it worked. Thanks.

Bystroushaak commented 4 years ago

Hah, but doesn't work with files.

Fizzadar commented 4 years ago

ha! https://github.com/Fizzadar/pyinfra/commit/60f6bf5670317acdd0e383a47fdaae1226c34c9b should fix that (and any future disparity between command and file operations).

Bystroushaak commented 4 years ago

Tested, works nicely.

Is there a possibility to specify sudo password in non-interactive way? I mean something like take it from the ssh_password variable, or something like that.

normoes commented 4 years ago

I tested it as well, works great

The only thing I noticed: The password is displayed in the logs:

env SUDO_ASKPASS=/tmp/pyinfra-pyinfra-sudo-askpass PYINFRA_SUDO_PASSWORD=*** sudo -H -A -k sh -c 'yum clean all'
Fizzadar commented 4 years ago

@Bystroushaak if you set use_sudo_password='top-secret-password' it will use it w/o prompting (will document this)!

@normoes it should remove the password from the log, showing *** instead! (related: https://github.com/Fizzadar/pyinfra/issues/319).

normoes commented 4 years ago

I can confirm use_sudo_password='top-secret-password' works.

And the password is indeed obfuscated when omitting -v. So, yes, related to #319.

Thanks for the hint. Good work.

Fizzadar commented 4 years ago

This (and #319) is now available in 0.15.dev0! I will release 0.15 in a few days.

Fizzadar commented 4 years ago

This has now been released in 0.15 :)

wookayin commented 4 years ago

So how exactly can we get sudo working via prompt?

Fizzadar commented 4 years ago

@wookayin if you pass use_sudo_password=True to the operations (or use USE_SUDO_PASSWORD config variable) it will prompt for the password. There's an issue to investigate automating this.

wookayin commented 4 years ago

I get Error: no such option: --use_sudo_password but USE_SUDO_PASSWORD works. use_sudo_password=1 gives me another error (pyinfra error: No such module: ...). Look forward to #318, thanks!

Fizzadar commented 4 years ago

I've added https://github.com/Fizzadar/pyinfra/issues/409 to track the addition of the --use-sudo-password flag.