Home / Alt manpages / runuser(1)

  • runuser(1)
  • User command
  • linux

Run a Command as Another User with runuser

runuser launches one command as another user or group, with no password prompt at all, built for root and service scripts rather than everyday logins. You will finish with a repeatable way to run a single command under another identity. The examples use runuser from util-linux 2.39.3, as installed on this machine. It changes only the child process unless that child writes files or restarts a service itself.

  • You need: a root shell, or a sudo rule that permits the exact command.
  • Time: about ten minutes.
  • Not covered here: a full PAM session; the manual recommends setpriv if you do not need one.

1. Check the installed command

Start with read-only checks. These confirm the binary and version without changing anything:

$ command -v runuser
/usr/sbin/runuser
$ runuser --version
runuser from util-linux 2.39.3

The useful form is runuser -u USER -- COMMAND ARGUMENTS. The -- marker is not required every time, but it draws a clear line between runuser's own options and the child command.

Checkpoint

If your shell is not root, expect a flat refusal rather than a password prompt:

$ runuser -u nobody -- id
runuser: may not be used by non-root users

Run the rest of the examples from a root shell, or prefix them with an approved sudo invocation. Do not grant broad passwordless root access just to make a test convenient.

2. Run one harmless command as a target user

Pick an existing account and verify its identity inside the child. This uses a local account called alice; swap in the service account that actually owns the files or process you are testing:

# runuser -u alice -- id
uid=1000(alice) gid=1000(alice) groups=1000(alice),27(sudo),987(docker),1009(bob),1017(carol)
  • Group membership is host-specific. The check that matters is that the reported user and primary group match your intended target.
  • Need numeric values for a script? Use id -u USER and id -g USER first.
  • This is not a login. The command starts, runs, and waits for the child to finish; it does not create a persistent session or touch the account's configuration.
  • Files the child writes are still real. Start with id, printf, or another read-only probe before anything that changes state.

3. Pass arguments without losing the boundary

Everything after the program belongs to that program. Quote values that might contain spaces or shell metacharacters:

# runuser -u alice -- /usr/bin/printf '%s\n' 'runuser-child-ok'
runuser-child-ok
# runuser -u alice -- /usr/bin/sh -c 'printf "uid="; id -u; printf "\n"'
uid=1000

The outer shell parses the command before runuser ever sees it. The single quotes around the inner shell command specifically stop the root shell from expanding its variables early. Never build this line by concatenating untrusted input: a user-controlled argument can rewrite the command or make the child read or overwrite a path you did not intend.

Checkpoint

Confirm the child's exit status whenever the result matters:

# runuser -u alice -- /usr/bin/sh -c 'exit 7'
# printf 'exit status: %s\n' "$?"
exit status: 7

Normally runuser returns the exit status of the command it ran, and that is the quirk worth remembering:

  • Killed by a signal: reported as the signal number plus 128.
  • Status 1: runuser itself hit a generic error.
  • Status 126: the command could not be executed.
  • Status 127: the command was not found.

4. Set the primary and supplementary groups

Root can pick a primary group with -g and supplementary groups with -G. If you skip -g, the first -G group also becomes the primary group. Use numeric or named groups that actually exist on the host:

# runuser -u alice -g alice -G alice -- id
uid=1000(alice) gid=1000(alice) groups=1000(alice)

Warning

-G replaces the child's whole supplementary-group set; it is not additive on top of the caller's groups. That can quietly remove access the target normally has, which is fine for a controlled test but surprising in a service wrapper. Verify with id before starting anything long-running. These options are also root-only: a failure here is a privilege or account problem, not a hint to try a different spelling of the command.

5. Choose the environment deliberately

Without --login, runuser keeps the current directory and, for compatibility, changes only the core identity variables: HOME and SHELL, plus USER and LOGNAME for a non-root target. PAM can make further changes on top, which is why a child can end up with settings you did not expect.

Use --login for a login-like environment. It clears the environment bar TERM and an allowed whitelist, sets the target's home, shell, user, logname and path, changes directory, and starts the shell as a login shell:

# runuser --login alice -- -c 'printf "home=%s\npwd=" "$HOME"; pwd; printf "path=%s\n" "$PATH"'
home=/home/alice
pwd=/home/alice
path=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/snap/bin:/opt/bin

The exact path comes from /etc/default/runuser, /etc/login.defs and PAM, so do not hard-code this output across machines. The same files also carry settings such as ENV_PATH, ENV_ROOTPATH, ENV_SUPATH and ALWAYS_SET_PATH.

--preserve-environment, also spelled -m or -p, keeps the caller's current environment instead, and is ignored with --login. Treat it as a deliberate compatibility choice rather than a service default: inherited variables can steer configuration, credentials or executable paths.

6. Use interactive sessions and pseudo-terminals carefully

Omit -u and runuser falls back to su-compatible semantics, defaulting to root when no user is given. The more explicit interactive form is:

# runuser --login alice
alice@host:~$ id -un
alice
alice@host:~$ exit

The prompt and hostname will vary. The shell ends when you type exit or press its end-of-file key; do not leave an unbounded root shell inside a script.

--pty creates an independent pseudo-terminal and proxies input and output. It is mainly for interactive sessions and stops the child sharing the original terminal's file descriptor. Test it with a harmless command first: it does not make an unsafe command safe, and it grants no extra permissions.

7. Diagnose failures without changing policy

Check the target account, command path and groups before reaching for an elevated change:

$ getent passwd TARGET_USER
$ command -v COMMAND
$ getent group TARGET_GROUP
  • Replace the uppercase placeholders with real values before running anything.
  • Command reported as not found? Use an absolute path, or check the login and non-login PATH values.
  • Target has a restricted shell not listed in /etc/shells? runuser ignores --shell and the SHELL variable unless the caller is root.
  • Account like nobody has a non-interactive shell? Test with a command rather than assuming an interactive login will work.

Since util-linux 2.38, runuser resets several resource limits on the child, including nice priority, real-time priority, file size, address space and open-file limits. A program that behaves fine launched directly can see different limits under runuser, so measure the child with the relevant tool and set limits in the service manager or wrapper that is meant to own that policy.

Recovery

There is no general undo for runuser, because it normally changes only the child process. If that child created files, altered permissions or restarted a service, recover those effects through that operation's own documented backup or rollback procedure. Stop before testing a destructive command as root.

Done means

  • Version confirmed and you have established that root is required.
  • Identity checked: a harmless command reported the intended user and groups.
  • Boundary kept: you used -- and careful quoting to separate runuser options from child arguments.
  • Environment chosen: login, preserved or ordinary behaviour, picked deliberately rather than by default.
  • Exit status understood: you know how runuser reports the command's own status versus its own errors.
  • Nothing assumed harmless: a root shell, inherited environment or target account was never treated as safe by default.