Use setpriv to Drop Linux Privileges Without PAM

setpriv runs one child process with exactly the privilege settings you choose, no PAM stack required. You will verify what the child inherited, and leave the calling shell and persistent system configuration unchanged. The examples use the locally installed util-linux 2.39.3. Allow about 10 minutes if you are testing an existing command, or longer if you are designing a service boundary.

Prerequisites: a Linux system with setpriv from util-linux, a command that is safe to run, and permission to inspect or change the credentials you intend to use. Ordinary inspection needs no elevated privilege. Changing another user's UID, GID or capability sets normally needs root or a process with the relevant kernel capability.

1. Confirm the local version and current state

Check the executable before relying on an option. This matters because setpriv is a thin wrapper around execve, and its available options follow the installed util-linux release.

$ setpriv --version
setpriv from util-linux 2.39.3
$ setpriv --dump
uid: 1004
euid: 1004
gid: 1004
egid: 1004
Supplementary groups: 27,987,1000,1004,1009,1017
no_new_privs: 0

Your IDs, groups, capabilities and AppArmor profile will differ. The dump is information only: with --dump, setpriv does not execute a program. --list-caps is a separate inspection mode that prints the capability names known to the installation.

Checkpoint: write down the identity and groups you expect the child to have. If that baseline is surprising, stop and investigate the shell or service that launched it.

2. Add no_new_privs to a harmless child

The least invasive privilege control is often --no-new-privs. It sets a kernel bit inherited by descendants: later execve calls cannot use set-user-ID, set-group-ID or file capabilities to grant new privilege. It cannot be unset, so apply it only to a process tree for which that restriction is acceptable.

$ setpriv --no-new-privs /usr/bin/id
uid=1004(alice) gid=1004(users) groups=1004(users),27(sudo),987(docker),1000(bob),1009(carol),1017(ops)

This output confirms the child ran and shows its IDs and supplementary groups, but id does not display no_new_privs. Use a small shell child to inspect the flag directly:

$ setpriv --no-new-privs /bin/sh -c 'grep "^NoNewPrivs:" /proc/self/status'
NoNewPrivs: 1

There is nothing to undo here. The bit applies to the child and its descendants; your current shell keeps its original state after the child exits.

3. Choose how supplementary groups are handled

Changing a primary UID or GID without making an explicit group decision is rejected for safety. Choose one of these policies in the same command:

For a normal user transition, ask setpriv to initialise groups and then verify with id. This changes only the child launched by the command:

$ sudo setpriv --reuid=TARGET_USER --regid=TARGET_GROUP --init-groups -- /usr/bin/id
uid=TARGET_UID(TARGET_USER) gid=TARGET_GID(TARGET_GROUP) groups=TARGET_GID(TARGET_GROUP),OTHER_GROUPS

Replace the all-capitals values with real names from your host; the exact id output is host-specific. The sudo prefix is needed only when the current account cannot set the requested credentials.

Security checkpoint: do not use --keep-groups in a service wrapper unless the retained groups are part of the intended access model. To remove access, use --clear-groups or an explicit allow-list, then verify it before starting the real program.

4. Drop capabilities when changing UID

Changing a UID or GID does not itself change the process capability sets. A root-launched child can therefore retain more authority than its numeric UID suggests, until the final exec rules and your selected capability options are considered. A conservative pattern from the local manual is to change both IDs and remove inheritable capabilities:

$ sudo setpriv --reuid=TARGET_USER --regid=TARGET_GROUP --init-groups --inh-caps=-all -- /usr/bin/id
uid=TARGET_UID(TARGET_USER) gid=TARGET_GID(TARGET_GROUP) groups=TARGET_GID(TARGET_GROUP),OTHER_GROUPS

--inh-caps=-all changes the inheritable set. --ambient-caps and --bounding-set control different sets, so do not substitute them without understanding the capability model described by capabilities(7). The kernel does not allow capabilities to be added to the bounding set, and if you remove one from the bounding set while leaving it inheritable, later behaviour can be confusing.

Test the exact executable and workload, not just id. A capability or an access check may only be exercised after the program opens a file, binds a port or performs another privileged operation. Never treat a successful wrapper exit as proof that the intended security boundary is complete.

5. Reset inherited environment when it is part of the boundary

Credentials are not the only inherited input. --reset-env clears environment variables except TERM, creates HOME, SHELL, USER and LOGNAME from the target user's passwd entry, and sets a standard user or root PATH. The actual PATH can differ on systems with merged /usr directories.

$ env -i TERM=xterm setpriv --reset-env /usr/bin/env | sort
HOME=/home/alice
LOGNAME=alice
PATH=/usr/local/bin:/usr/bin
SHELL=/bin/bash
TERM=xterm
USER=alice

Do not assume this is a complete application sandbox. Files, open descriptors, working directory, namespaces and other process properties are outside setpriv's remit. If a program needs a particular environment variable, pass it deliberately after considering whether it is trusted.

6. Handle failures before the real command runs

If any requested setting cannot be applied, setpriv does not run the program and returns status 127. Test a proposed wrapper with /usr/bin/true or another harmless command first, so a missing group policy fails before a service starts rather than after it has partially initialised.

$ setpriv --regid=TARGET_GROUP /usr/bin/true
setpriv: --[re]gid requires --keep-groups, --clear-groups, --groups, or --init-groups
$ printf 'status=%s\n' "$?"
status=127

Use the command's diagnostic to correct the option combination. Do not hide it with a shell fallback that starts the program anyway. SELinux and AppArmor transitions are also specialised: --selinux-label and --apparmor-profile can fail when the relevant security module is absent, or can make the final exec fail according to policy.

Done means