setpriv is a neat little known tool included in util-linux. It's a workalike of chpst -u/s6-setuidgid that also includes Linux capability support, enabling more granular privilege granting. This also allows unprivileged cronjobs without using su/runuser, (which are not the right answer to this problem) and without tying your script to a specific service management toolkit.
Usage
setpriv has many options, and we can go on a lot. This is only a fraction of what it can do, read the manual for more details.
Setting user and groups
This is similar to the functionality provided by chpst -u and s6-setuidgid. The relevant options for setting user and group in setpriv are:
- --reuid - set the user (note this doesn't imply any groups!);
- --regid - set the group;
Ok, there are also individual --{r,e}{u,g}id options, but --re{u,g}id is enough for most purposes.
In addition to that, the manual recommends specifying one of these options related to supplementary groups (otherwise, they are inherited from root, which is not safe).
- --init-groups: initialize the user's supplementary groups as specified in /etc/groups (like s6-setuidgid).
- --clear-groups: removes any groups other than what is specified in the --?{u,g}id options (like runit's chpst);
- --groups: manually sets the additional groups, like you can with s6-envuidgid + s6-applyuidgid. However, unlike in their case, --groups uses group names rather than GIDs.
Note that while more convoluted, setpriv is more granular than, and can emulate, both s6-setuidgid and chpst, which is nice.
An additional option I'd like to mention is --reset-env, which removes the environment and sets HOME properly (which you must do manually with these other tools).
For a very simple example, let's say you want to reload your newsboat RSS feeds as a cronjob. This can be accomplised with a setpriv invocation:
setpriv --reuid capezotte --regid capezotte --init-groups --reset-env -- newsboat -x reload
For a more "traditional" example, a runit service using the built-in chpst, vs one using setpriv:
# With runit
exec chpst -u http:http darkhttpd /server
# With setpriv
exec setpriv --reuid http --regid http --clear-groups -- darkhttpd /server
setpriv can work as a drop-in replacement to chpst, as it directly executes the process after dropping privileges.
Setting capabilities
However, the killer feature of setpriv is using it to add capability support without changing the file itself (and, as users of this distro like, without changing PID1).
Let's say we want the previous darkhttpd example to be able to bind to root-only ports even though it starts a non-root user.* This can be accomplished with the net_bind_service capability.
setpriv --reuid http --regid http --clear-groups --ambient-caps +net_bind_service --inh-caps +net_bind_service -- darkhttpd /server --port 80
This essentially adds net_bind_service to the set of ambient and inheritable capabilities, ensuring darkhttpd, after execution, will have it even if the file itself doesn't have the capability. For a more detailed description of how Linux capabilities work in our case, read Transformation of capabilities during execve() from man 7 capabilities.
In the meantime, you can copy-paste a systemd unit file and write -all,+some_cap,+other_cap, etc. for each specified capability set.
*Darkhttpd doesn't actually need this since it can drop privileges on its own after starting as root, but this can be used to make services that would otherwise unavoidably require root access possible to run as unprivileged users. For instance, the stubby DNS-to-DoH translator expects to be launched either as root, or as an unprivleged user with the aforementioned capability.
Inb4
Why not su and runuser?
setpriv is explicitly intended to drop privileges and directly executes the program after performing the requested changes, nothing more, nothing less.
su and runuser will perform a lot of not-really-needed setup (controlled by their files in /etc/pam.d) and will spawn the program as an additional process, which is less efficient on a cronjob, and unacceptable for service monitors like runit, s6, dinit, etc., which need direct access to the process to work as intended. Read more here.
Why not chpst/s6-setuidgid?
chpst and s6-setuidgid are bundled as part of a toolset with runit and s6, respectively, which means you'd need to install these entire toolkits for this one piece of functionality (despite the tools themselves being standlone).
In addition to that, since their goal is universal compatibility with POSIX-compliant OSes, Linux-specific options (such as capabilities) are out-of-scope for them.
However, if you already use the rest of the toolset, they are a natural fit. In addition, their usage is much less verbose than setpriv.