Run a Command as Another User with runuser
You will finish with a repeatable way to launch one command under another user and, when needed, another group. The examples use runuser from util-linux 2.39.3, as installed on this machine. They change only the child process unless the command you launch changes files or services itself.
The route
Jump straight to the step you need, or tick off Done means at the end.
- 1. Check the installed command
- 2. Run one harmless command as a target user
- 3. Pass arguments without losing the boundary
- 4. Set the primary and supplementary groups
- 5. Choose the environment deliberately
- 6. Use interactive sessions and pseudo-terminals carefully
- 7. Diagnose failures without changing policy
Allow about ten minutes. You need a root shell, or a working sudo rule that permits the particular command. runuser does not ask for a password, so it is intended for root. If you do not need a PAM session, the manual recommends considering setpriv instead.
1. Check the installed command
Start with read-only checks. This confirms the binary and version without changing anything:
$ command -v runuser
/usr/sbin/runuser
$ runuser --version
runuser from util-linux 2.39.3
The useful command form is runuser -u USER -- COMMAND ARGUMENTS. The -- marker is not required in every case, but it makes it clear where runuser options end and the child command begins.
Checkpoint
If your shell is not root, expect a refusal rather than a password prompt:
$ runuser -u nobody -- id
runuser: may not be used by non-root users
Run the remaining examples from a root shell, or prefix them with an approved sudo invocation. Do not grant broad passwordless root access merely to make a test convenient.
2. Run one harmless command as a target user
Choose an existing account and verify its identity inside the child. The following uses the local andy account, but you should replace it with the service account that owns the files or process you are testing:
# runuser -u andy -- id
uid=1004(andy) gid=1004(dixon) groups=1004(dixon),27(sudo),987(docker),1000(andy),1009(owendixon),1017(direktor)
Group membership is host-specific. The important check is that the reported user and primary group match your intended target. Use id -u USER and id -g USER first if you need numeric values for a script.
This invocation starts a command and waits for it to finish. It does not create a persistent login or change the account's configuration. Any files the child writes still have real consequences, so begin with id, printf or another read-only probe.
3. Pass arguments without losing the boundary
Arguments after the program belong to that program. Quote values that may contain spaces or shell metacharacters:
# runuser -u andy -- /usr/bin/printf '%s\n' 'runuser-child-ok'
runuser-child-ok
# runuser -u andy -- /usr/bin/sh -c 'printf "uid="; id -u; printf "\n"'
uid=1004
The shell parses the outer command before runuser sees it. In particular, the single quotes around the inner shell command prevent the root shell from expanding its variables. Do not build this section by concatenating untrusted input. A user-controlled argument can alter the command or make the child read or overwrite an unintended path.
Checkpoint
Confirm the child status when the command's result matters:
# runuser -u andy -- /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. A command killed by a signal is reported as the signal number plus 128. Status 1 means runuser itself encountered a generic error; 126 means the command could not be executed; 127 means it was not found.
4. Set the primary and supplementary groups
Root can select a primary group with -g and supplementary groups with -G. The first -G group also becomes the primary group when -g is absent. Use numeric or named groups that exist on the host:
# runuser -u andy -g dixon -G dixon -- id
uid=1004(andy) gid=1004(dixon) groups=1004(dixon)
Specifying -G replaces the supplementary-group set for the child; it is not an additive request to keep every group from the caller. That can remove access the target normally has, which is useful for a controlled test but surprising in a service wrapper. Verify with id before starting a long-running process.
These options are restricted to root. A failure here is a privilege or account configuration problem, not a prompt 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 principal identity variables such as HOME and SHELL, plus USER and LOGNAME for a non-root target. PAM can make final environment changes. This is why a child may inherit settings you did not expect.
Use --login when you need a login-like environment. It clears the environment except for TERM and an allowed whitelist, sets the target's home, shell, user, logname and path values, changes directory, and starts the shell as a login shell:
# runuser --login andy -- -c 'printf "home=%s\npwd=" "$HOME"; pwd; printf "path=%s\n" "$PATH"'
home=/home/andy
pwd=/home/andy
path=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/snap/bin:/opt/bin
The exact path is controlled by /etc/default/runuser, /etc/login.defs and PAM, so do not hard-code this output across machines. The local command also reads those files for settings such as ENV_PATH, ENV_ROOTPATH, ENV_SUPATH and ALWAYS_SET_PATH.
--preserve-environment, also written -m or -p, keeps the current environment instead. It is ignored with --login. Treat it as an explicit compatibility choice, not a default for services: inherited variables can select configuration, credentials or executable paths.
6. Use interactive sessions and pseudo-terminals carefully
If you omit -u, runuser uses su-compatible semantics and starts a shell, defaulting to root when no user is supplied. A more explicit interactive switch is:
# runuser --login andy
andy@host:~$ id -un
andy
andy@host:~$ exit
The prompt and host name vary. The shell ends when you type exit or press its end-of-file key. Do not use an unbounded root shell in a script.
--pty creates an independent pseudo-terminal and proxies input and output. It is mainly for interactive sessions and helps prevent the child from sharing the original terminal file descriptor. Test it with a harmless command before using it in a wrapper. It does not make an unsafe command safe, and it does not grant permissions.
7. Diagnose failures without changing policy
Check the target account, command path and groups before reaching for elevated changes:
$ getent passwd TARGET_USER
$ command -v COMMAND
$ getent group TARGET_GROUP
Replace the uppercase placeholders with real values. If the command is reported as not found, use an absolute path or inspect the login and non-login PATH values. If the target has a restricted shell that is not listed in /etc/shells, runuser ignores --shell and the SHELL environment variable unless the caller is root. An account such as nobody may also have a non-interactive shell, so test a command rather than assuming an interactive login will work.
Since util-linux 2.38, runuser resets several resource limits, including nice priority, real-time priority, file size, address space and open-file limits. A program that works when launched directly may therefore see different limits under runuser. Measure the child with the relevant tool and set limits in the service manager or wrapper designed to own that policy.
There is no general undo command because runuser normally changes only the child process. If your child created files, altered permissions or restarted a service, recover those effects using that operation's documented backup or rollback procedure. Stop before testing a destructive command as root.
Done means
- You confirmed the local runuser version and established that root is required.
- A harmless command reported the intended user and groups.
- You used
--and careful quoting to separate runuser options from child arguments. - You chose login, preserved or ordinary environment behaviour deliberately.
- You checked the child's exit status and know how runuser reports execution errors.
- You have not treated a root shell, inherited environment or target account as harmless by default.