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.
16
  • 3
    Why do you need to use exec there? Just sudo -u www-data php -r ... should be enough.
    – muru
    Commented Jul 10, 2019 at 8:31
  • 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
    Commented Jul 10, 2019 at 9:35
  • 1
    Is it an option to use some alternative to sudo? sudo.ws/other.html Commented Jul 10, 2019 at 9: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
    Commented Jul 10, 2019 at 9: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
    Commented Jul 10, 2019 at 9:48

4 Answers 4

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.

4
  • 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
    Commented Jul 10, 2019 at 9: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).. Commented Jul 10, 2019 at 9:53
  • The given example doesn't work on my system. The intermediate "sudo" process is still there.
    – famzah
    Commented Aug 12, 2020 at 18:48
  • Ok I added an UPDATE to the answer, please check if it helps! Commented Aug 13, 2020 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).

8
  • 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
    Commented Jul 7, 2021 at 0:28
  • Well, you are kind of right, but I am not wrong and my answer is not "very wrong" either. :D.
    – etosan
    Commented Jul 16, 2021 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
    Commented Jul 16, 2021 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
    Commented Jul 16, 2021 at 19:46
  • 1
    This only proves how complex tool sudo really is.
    – etosan
    Commented Jul 16, 2021 at 19:51
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..

3
  • Need a solution without backgrounding or a solution with full restore (like the parent process etc.).
    – mvorisek
    Commented Jul 10, 2019 at 9:37
  • Ok. You could improve the question with these details (and the need to minimize process count). Commented Jul 10, 2019 at 9:41
  • added to the question
    – mvorisek
    Commented Jul 10, 2019 at 9:46
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.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .