Rootless ntpd (and other daemons) for Alpine Linux using capabilities

On Linux systems ntpd is typically the process responsible for synchronizing the clock. I'm using Alpine Linux with it's default busybox ntpd, so I will focus on that. By default, the busybox ntpd needs to run as root in order to be allowed to change the system time.

Note:
Most setups will already have a dedicated ntp user. Furthermore, ntpd implementations are typically heavily tested so there is no problem with them running as root; this is meant to be a general tutorial on how to run rootless daemons.

Here is the standard ntpd service file:

#!/sbin/openrc-run

name="busybox $SVCNAME"
command="/usr/sbin/$SVCNAME"
command_args="${NTPD_OPTS:--N -p pool.ntp.org}"
pidfile="/var/run/$SVCNAME.pid"

depend() {
	need net
	provide ntp-client
	use dns
}

In order to replace the need for root permissions, we can use Linux capabilities, specifically – CAP_SYS_TIME. If you want to make some other daemon rootless, you will need to check which capabilities it requires. Using strace to find which syscalls fail can be useful here. See capabilities(7). There are two ways to gain permissions – either by setting file attributes or using the C api, possibly through a program like capsh which comes with libcap. I prefer to use capsh since it does not affect other invocations of the ntpd program. For convenience, I will use a wrapper script, called custom_ntpd:

#!/bin/sh
exec capsh --keep=1 --user=nobody --inh=cap_sys_time --addamb=cap_sys_time --drop=all -- -c 'busybox ntpd -n -N -p pool.ntp.org'

To summarize – it runs the ntpd command as user "nobody" (a dedicated ntpd user may be preferable), adds the CAP_SYS_TIME capability to the inheritable set (allowed in child processes) as well as the ambient set (automatically activated in child processes). Finally, it drops all other capabilities which are not needed and runs the command.

To use this new ntpd wrapper script, create a new service /etc/init.d/custom_ntpd which is similar to the original ntpd service but uses the custom_ntpd script instead:

#!/sbin/openrc-run

name="customized busybox $SVCNAME"
command="/usr/local/bin/custom_ntpd"
command_args=""
pidfile="/var/run/$SVCNAME.pid"
command_background=true

depend() {
	need net
	provide ntp-client
	use dns
}