5

I need to use sudo to run process as a different user. But how to use sudo with exec like:

sudo -u www-data exec php -r 'sleep(2); echo 5;'

to replace the sudo process?

Why and requirements?

  • minimize total processes count
  • commands are started continously, there is no possibility to start them in one sudo
  • the solution needs to behave like without exec, i.e. no backgrounding etc.
mvorisek
  • 209
  • 3
    Why do you need to use exec there? Just sudo -u www-data php -r ... should be enough. – muru Jul 10 '19 at 08:31
  • To safe linux processes and prevent to show it in ps output. – mvorisek Jul 10 '19 at 08:34
  • By definition of exec it will replace the sudo process with the php process. Correct me if I am wrong. – mvorisek Jul 10 '19 at 08:51
  • @Mvorisek The PHP process would still show in the process list. Please update your question with some context and what it is you are actually trying to achieve. – Kusalananda Jul 10 '19 at 08:52
  • That's true of the exec builtins of a shell like bash, but you aren't running a shell. Even if you were, how would it help you hide anything? – muru Jul 10 '19 at 08:52
  • It will remove (replace by php) process and that is my goal/question. – mvorisek Jul 10 '19 at 08:53
  • 2
    Of course, but without the sudo process. If this is started 1000x times, it makes a difference to have a 1000 vs 2000 processes. – mvorisek Jul 10 '19 at 09:35
  • 1
    Is it an option to use some alternative to sudo? https://www.sudo.ws/other.html – Jonas Berlin Jul 10 '19 at 09:40
  • 7
    If you are forking 1000 PHP processes with sudo, why do you need 1000 sudo calls? Isn't one sudo with a shell that does that loop enough? Changing the user identity only needs to happen once. – Kusalananda Jul 10 '19 at 09:46
  • 1
    @Kusalananda Processes are not started in one batch. Jonas Berlin has a good solution, maybe the child process can be backgrounded and then attached back but not to the sudo process, but the parent of it. The forking by sudo itself is no issue. – mvorisek Jul 10 '19 at 09:48
  • 1
    Can't you setuid in PHP? https://www.php.net/manual/en/function.posix-setuid.php – muru Jul 10 '19 at 10:03
  • @muru Good idea, but the user is passed as a variable. One possible solution, but I would be happy to find a one without the need to create a script file for every run. – mvorisek Jul 10 '19 at 10:05
  • Oh, sorry, you meant setuid directly in PHP. That is not possible, I can not modify the PHP script. Firstly I read/meant setuid flag to the php file/executable. – mvorisek Jul 10 '19 at 10:13
  • What about something like php -r 'posix_setuid(...); include "actual_script.php"'? – muru Jul 10 '19 at 10:31
  • I understand the functionality, but I am looking for universal solution (i.e. usable also for other interpreter like python) for sudo + exec or some elegant solution like Jonas Berlin has outlined. – mvorisek Jul 10 '19 at 12:23
  • I'm pretty sure setuid and functionality for executing other scripts exist in most major languages. – muru Jul 10 '19 at 13:57

4 Answers4

4

From the manual page of sudo(8):

As a special case, if the policy plugin does not define a close function and no pty is required, sudo will execute the command directly instead of calling fork(2) first. The sudoers policy plugin will only define a close function when I/O logging is enabled, a pty is required, or the pam_session or pam_setcred options are enabled.

In normal cases this means that if you ensure your process is disconnected from the TTY, it will not use fork. So for example:

sudo -u www-data php -r 'sleep(2); echo 5;' < /dev/null > /dev/null 2>&1

will avoid the extra process. If you need some of the outputs, direct them to a file instead or similar that is not a TTY and leave the rest to /dev/null as shown above.

I noticed that there is still a side effect of seeing the original sudo command in the process listing, but maybe that is okay with you. Alternatively there might be some php function or similar to call to change how the process shows up in the process listing to remediate this need.

UPDATE:

Many systems seem to have the pam options enabled by default. It's possible to disable them but please be ware that there are some side effects like "resource limits may not be updated for the command being run". See sudoers(5) for details. To disable them globally, you can adding the following line in /etc/sudoers:

Defaults        !pam_setcred
Defaults        !pam_session

Check the sudoers(5) man page to see how to restrict the settings to e.g. specific users or similar; look for "Default_Type" in the man page.

Jonas Berlin
  • 1,124
  • Interesting. I need standard pipes, i.e. stdin, stdout, ... But pty and others is not required. Do you know why there is the I/O logging condition? – mvorisek Jul 10 '19 at 09:51
  • 1
    I don't know. A wild guess would be that it might need to clean up some TTY permissions or similar after the process exits (which it cannot do if it doesn't stick around as a separate process).. – Jonas Berlin Jul 10 '19 at 09:53
  • The given example doesn't work on my system. The intermediate "sudo" process is still there. – famzah Aug 12 '20 at 18:48
  • Ok I added an UPDATE to the answer, please check if it helps! – Jonas Berlin Aug 13 '20 at 20:30
3

EDITED: This was my original answer (quoted because SO doesn't support strikethrough):

ORIGINAL:

Proper answer is that you simply can't. At least not reliably and easily.

FIXED: Proper answer is that you can. Very very carefully. See addenum on the bottom.

Despite what manual page says in section referenced by other commenters, the default sudo process model is to fork() on great majority (99.9%) of systems most of the time.

You can verify the truth easily yourself with process monitor like htop or ps --forest.

Even suggested answer:

<?php
exec("exec sudo -u www-data php -r 'sleep(2); echo 5;' < /dev/null > /dev/null 2>&1");

will leave lingering sudo process around:

php spawn-test.php
 └─ sudo -u www-data php -r 'sleep(2); echo 5;'
     └─ php -r 'sleep(2); echo 5;'

as long as php invoking the sudo driving script is somehow tied to some controlling terminal (edited: or script child process to a pty).

The reason for this is probably 'modern' sudo setup. sudo uses tty identification for ticketing and pam plugins that can do anything they want with your session as well. In ~some~ most software often simply compiling-in pam disables execve() pathways. Then you have 'modern' stuff interacting like systemd-logind etc.

sudo is very complex piece of software, and making it do what you want is not at all easy,

ORIGINAL:

and in your situation, it is essentially impossible.

EDIT: and depending on your situation, might be still possible.

However having multiple sudo instances wait for their children is not a problem in general. On *nix systems processes are very cheap entities, even these days, when they are dynamically linked with miliards of .so libraries.

You can also be assured that sudo lingers around "properly", that means it is consuming minimum computation resources (almost zero) and os is smart enough to share as much memory as possible among all the running sudo instances. So you should not see it as problem at all.

Now the question becomes, why do you want get rid of the sudo processes in-between?

If reason is that you don't "like" sudo processes there, without proper justification, that's the wrong reason.

Still, it can be done, of course, by means of SUID root and dropping privileges respectively, but it is an approach which is not beginner friendly, it is hard to get right, and all that makes it extremely dangerous.

So much, it is generally frowned upon even by experts, so we won't go into how to achieve that here.

Still if you are persistent enough, there are answers even here, on stackoverflow network, how to switch back to root by the means of SUID.

Just keep in mind, that you cannot trust anything in that process state (not even env vars) and must drop privileges properly into your target user as soon as possible (and do that correctly as well).

More over, depending on your deployment platform, you might need to take into account things like selinux or other security frameworks too.

Finally once your child is of different user than it's driving parent process, it becomes impossible to send it a signal to change it's state, without jumping through further hoops.

Because it's so hard and error prone, it is the sole reason sudo even exists, but I agree with you sudo is not always fit for every use case.

EDIT:

See user11658273's answer on how to disable all the advanced features: pam integration, pty creation and log servers - which all must be disabled, it seems, for this to work. It should make sudo go into forkless mode, even when you have PAM locally configured.

Be sufficiently careful.

Disabling pam_session will probably break things depending on PAM session being setup, while I (only imagine) pam_setcred could break IPA, Kerberos or something else that relies on credentials tickets being issued through PAM. log_servers are not that often used and with !use_pty your target script will inherit first 3 file descriptors that parent setup for it. I cannot test in this environment, whether this actually prevents sudo from authenticating "remote" accounts or not.

Better, to minimize potential breakage for all other users of the machine, you should give these special permissions only to user in question: Defaults: yourscriptuser !pam_session,!pam_setcred,!use_pty,!log_servers

Understand what you are doing and that sending signals from the parent script (to terminate child for example) might not work:

$ sudo cat /etc/sudoers | grep \!use_pty
Defaults: testuser !pam_session,!pam_setcred,!use_pty,!log_servers

in one terminal:

$ sudo sh -c 'echo $$ && exec sleep 88888' 27749

in other terminal:

$ kill -TERM 27749
kill: kill 27749 failed: operation not permitted

Make sure your child script always exits after it does it's work or when it reads zero bytes from stdin handle (ie read it from time to time and exit on empty read) - which is standard unix indication the file was closed (in this case pipe from the parent), this should make all the lingering children exit properly should they still remain running, when parent terminates. Otherwise they might end up lingering around indefinitely depending on their code (forking sudo handles of that for you).

etosan
  • 1,054
  • This answer is very long and very wrong. Forking can be trivially disabled by adding Defaults !pam_session,!pam_setcred,!use_pty,!log_servers to the /etc/sudoers file, as clearly documented in sudoers(5). use_pty and log_servers are disabled by default, and the documented security benefits of pam_* are minimal. See my answer for more information. I do not have enough reputation to downvote this answer. – 11_22_33 Jul 07 '21 at 00:28
  • Well, you are kind of right, but I am not wrong and my answer is not "very wrong" either. :D. – etosan Jul 16 '21 at 19:22
  • Huh character limit? Sucks.

    Well point is when I read "...policy plugin will only define a close function when..." in manual I understood that that behaviour is controlled by conditional compilation, which you (2 years later) and several others pointed out is not the case. Anyway wording is really poor in sudo manual, I would not use "define" in this context. ...

    – etosan Jul 16 '21 at 19:41
  • 1
    ... and while my reply is simplified for the asker, your solution, however, has brutally beautiful potential to actually lock out asking user and everybody on their team from their machine (really interesting find anyway): especially on systems reliant on some IPA, LDAP or other pam based integrated authentication solutions (which is the norm in bigger organizations). – etosan Jul 16 '21 at 19:46
  • Please fix your reply with disclaimer, that it can potentially break integration authorization services (when set as default) and add example that specifically does not use Defaults ..., but is tied to specific, preferably machine local, group (or user perhaps?), and I will properly upvote your answer. – etosan Jul 16 '21 at 19:50
  • 1
    This only proves how complex tool sudo really is. – etosan Jul 16 '21 at 19:51
  • Hi, thanks for responding, and I am sorry for being overly aggressive. What I would say is -- whoa, definitely don't use my solution on some important multi-user system without care, but I should have made that clear. The sudo documentation is very unreadable. I made these changes only because having over 5 years worth of sudo processes cluttering my process lists was annoying and unnecessary. Not being able to kill the process as the same user is a great catch, but that is actually a noted security improvement in my case. I updated my answer to include your information as well. – 11_22_33 Jul 24 '21 at 09:25
  • No problem 11_22_33 I understand the frustration! On occasion I fight with sudo few times a year too. Since then I found few other solutions on rpm based oses, but usually easiest way is simply to let permissions trickle down from root and some secure and safe state. Let service manager handle it (ie systemd): it has all the tools to do it safely. In OPs case the red herring is that, that their whole architecture is probably flawed. – etosan Dec 19 '21 at 05:08
2

Sudo forks (edit: not always, see my other answer) before running the command you specify. You cannot use exec to undo the fork. When in bash you use exec, it avoids the fork altogether and that's why it works in bash - sudo otoh doesn't have this feature. But, you can work around the problem like this instead, forking your program to the background and then exiting the intermediate process:

sudo -u www-data bash -c "php -r 'sleep(2); echo 5;' &"

This has some other side-effects that might or might not be a problem for you, like losing the original parent process information and not waiting for the process to complete..

Jonas Berlin
  • 1,124
2

This can be achieved by adding the following line to your sudoers file (edit with sudo visudo or sudo -e /etc/sudoers):

# Run commands for all users with the exec system call directly.
# This may break some authentication systems or scripts.                              
Defaults !pam_session,!pam_setcred,!use_pty,!log_servers

Run commands by "user" with the exec system call directly.

This may also break authentication, but only for one user.

Defaults:user !pam_session,!pam_setcred,!use_pty,!log_servers

The following is a process list of a program ran with sudo before making the above changes:

$ sudo sleep 500 &
[1] 4453
$ ps -f T
UID        PID  PPID  C STIME TTY      STAT   TIME CMD
root      4453  3478  0 00:16 pts/3    S      0:00 sudo sleep 500
root      4454  4453  0 00:16 pts/3    S      0:00 sleep 500

After making the above changes:

$ sudo sleep 500 &
[1] 4479
$ ps -f T
UID        PID  PPID  C STIME TTY      STAT   TIME CMD
root      4479  3478  0 00:18 pts/3    S      0:00 sleep 500

If desired this can be made to "affect all users on any host, all users on a specific host, a specific user, a specific command, or commands being run as a specific user," by using a different "Defaults" type. Refer to the sudoers(5) manual for more information.

As mentioned by @etosan, disabling PAM will break authentication through PAM and potentially lock users and administrators out, in particular on multi-user and remote systems. Another good catch by @etosan is that it is no longer possible to kill a program started with sudo as the user that started it, because the original sudo process is not running and cannot forward a signal on behalf of the user. This may be considered a security improvement in some situations, but it may also break programs relying on this functionality.

The sudo documentation is not always comprehensive and is often difficult to understand, so if it matters I would recommend trying to read the source.

On my system (Gentoo Linux profile default/linux/amd64/17.1/no-multilib/hardened/selinux) the distribution default configuration disables use_pty and log_servers by default, making the later two options unnecessary.

This configuration may also degrade the security of sudo, referring to sudoers(5) again:

pam_session  On systems that use PAM for authentication, sudo will
             create a new PAM session for the command to be run in.
             Unless sudo is given the -i or -s options, PAM session
             modules are run with the “silent” flag enabled.  This
             prevents last login information from being displayed
             for every command on some systems.  Disabling
             pam_session may be needed on older PAM implementations
             or on operating systems where opening a PAM session
             changes the utmp or wtmp files.  If PAM session support
             is disabled, resource limits may not be updated for the
             command being run.  If pam_session, pam_setcred, and
             use_pty are disabled, log_servers has not been set and
             I/O logging has not been configured, sudo will execute
             the command directly instead of running it as a child
             process.  This flag is on by default.
         This setting is only supported by version 1.8.7 or
         higher.

pam_setcred On systems that use PAM for authentication, sudo will attempt to establish credentials for the target user by default, if supported by the underlying authentication system. One example of a credential is a Kerberos ticket. If pam_session, pam_setcred, and use_pty are disabled, log_servers has not been set and I/O logging has not been configured, sudo will execute the command directly instead of running it as a child process. This flag is on by default.

         This setting is only supported by version 1.8.8 or
         higher.

For use_pty (which is likely to be disabled by default):

use_pty      If set, and sudo is running in a terminal, the command
             will be run in a pseudo-terminal (even if no I/O log‐
             ging is being done).  If the sudo process is not at‐
             tached to a terminal, use_pty has no effect.
         A malicious program run under sudo may be capable of
         injecting commands into the user's terminal or running
         a background process that retains access to the user's
         terminal device even after the main program has fin‐
         ished executing.  By running the command in a separate
         pseudo-terminal, this attack is no longer possible.
         This flag is off by default.

11_22_33
  • 141
  • 1
  • 9