System usernames

System usernames can be used by snap developers to enable them to run services and daemons as a user other than the default root.

Outside of snaps, applications traditionally adopt the concept of users and groups from the host operating system to use as a security mechanism that grants access to specific system and software resources.

Snap confinement prohibits a system’s users and groups from being used in this way within a snap but a snap_daemon user and group can alternatively be created within a snap to provide similar user and group level control outside of a snap’s confinement.

snap_daemon user and group

From version 2.41 onwards, snapd supports the creation of a snap_daemon user and group within a snap, exposed as user ID (UID) and group ID (GID) 584788 on the host system.

:warning: From snapd 2.61 onwards, snap_daemon is being deprecated and replaced with _daemon_ (with underscores), which now possesses a UID of 584792.

To create the snap_daemon user/group inside a snap, add the following system-usernames section to the snap’s snapcraft.yaml (or snap.yaml):

system-usernames:
  snap_daemon: shared

With the above added, the 584788 user and group can be used by a snap via standard APIs (eg, getpwent(), the getent command, etc) to perform various ownership (eg, chown() and chgrp()) and privilege separation operations (eg, setuid() and setgid()).

Usage considerations

In general, the snap may use the allocated system username as desired. However, due to snapd’s security sandbox, the snap may need to be adjusted to work reliably.

Using system-usernames allows a snap to drop privileges and transition to the specified user, but the daemon will still start as root and be started from systemd on boot.

Dropping privileges

Security best practice dictates that applications drop privileges as follows:

// Error messages, etc., omitted for clarity
char *user = "snap_daemon";
struct passwd *pwd = getpwnam(user);
if (pwd == NULL) {
    exit(1);
}
if (setgroups(0, NULL) < 0) {
    exit(1);
}
if (setgid(pwd->pw_gid) < 0) {
    exit(1);
}
if (setuid(pwd->pw_uid) < 0) {
    exit(1);
}

The observant reader will notice that the call to setgroups() uses the non-portable, Linux-specific, invocation size 0 and NULL list to clear the list completely. This contrasts with a more portable size 1 and a list consisting of the single primary group. This is due to current sandbox limitations. If desired, setgroups() may be overridden via LD_PRELOAD. See the test-setgroups snap for an example).

For those using base: core18 or higher, the setpriv command may be used to drop privileges like so:

$SNAP/usr/bin/setpriv --clear-groups --reuid snap_daemon \
  --regid snap_daemon -- <your command>

base: core18 snaps should stage setpriv while base: core20 should stage util-linux. On snapd < 2.45, LD_PRELOAD is still needed due to how the sandbox is configured and how setpriv --clear-groups is using setgroups(0, <NON_NULL>):

LD_PRELOAD=$SNAP_COMMON/wraplib.so $SNAP/usr/bin/setpriv \
  --clear-groups --reuid snap_daemon --regid snap_daemon -- <your command>

Some applications may try to drop privileges themselves using initgroups() which under the hood uses setgroups() in a way that is blocked by the sandbox. In this case, the snap would need to use the LD_PRELOAD method. See the test-setpriv snap for how to use setpriv with and without LD_PRELOAD.

:information_source: Future releases of snapd will support setting User=snap_daemon and Group=snap_daemon in the systemd unit file.

Process management via signals

By default, a snap’s security sandbox limits the sending and receiving of signals to processes in the snap where the sender and the receiver have the same owner, even when the process runs as root (unless the process has CAP_KILL).

When developing a snap, care must therefore be taken to drop privileges before sending a signal to a process that has privilege dropped.

For example, a management process that runs as root may fork off worker processes that drop privileges to the snap_daemon user. Whenever this management process wants to send a signal to its workers, it must drop privileges to the snap_daemon user first. Alternatively, the snap could use plugs: [ process-control ], which among other things, grants CAP_KILL.

Since the process-control interface grants considerable access for system-wide process management, best practice dictates that privileges must be dropped as needed when sending signals to other processes in the snap.

Ownership (discretionary access controls)

The security sandbox generally limits file access to where the object id of the file (ie, the owner) matches the uid of the running process. This is true even for root, unless the process has CAP_DAC_OVERRIDE or CAP_DAC_READ_SEARCH defined, which the security sandbox intentionally denies.

As such, care must be taken when creating files after dropping privileges if those files are intended to be accessed by other processes in the snap that have not had their privileges dropped.

While each snap’s requirements may differ, in general, a reasonable approach is to create files and directories with <dropped user>:root ownership, allowing owner and group read and write. This allows allows both the privilege dropped user and the root user within the snap to access to the files.

For example, if the snap utilises the snap_daemon user, the snap might (as part of a configure hook or a wrapper script, for instance) do:

if [ ! -d "$SNAP_DATA/dir" ]; then
    mkdir "$SNAP_DATA/dir"
    chmod 770 "$SNAP_DATA/dir" # must be before chown
    chown snap_daemon "$SNAP_DATA/dir"
    chgrp root "$SNAP_DATA/dir"  # not needed but shown for clarity
fi

Security best practice dictates file access should be performed with the minimal privileges necessarily, so the snap is of course free to privilege-drop prior to accessing the file instead.

subuid, subgid and other container technologies

Snapd takes great care to avoid overlapping with other container technologies (or in the case of systemd, working with systemd-nspawn's collision detection). It uses the 524288-589823 UID/GID range, for instance, to help avoid the default ranges for LXD, Docker and other container systems.

Some administrators may adjust their non-snap container runtimes to use non-default values (eg, via /etc/subuid, /etc/subgid, etc). While it is non-fatal for other container ranges to overlap with snapd’s range, best practice dictates that a different range should always be used to ensure a clean separation between snapd and other container ranges in the kernel on the system.

References

3 Likes

FYI, some have reported that gosu works well for privilege dropping.

1 Like

I’m trying this feature out, but I’m getting weird permission denied errors.

The root account inside of the snap cannot access files owned by snap_daemon in $SNAP_USER_DATA, regardless on the permissions of those files.

root@howard:/home/merlijn# ls -al /root/snap/easy-openvpn-server/x8/openvpn-server1-status.log
-rw-rw-rw- 1 snap_daemon snap_daemon 232 Jan 25 21:30 /root/snap/easy-openvpn-server/x8/openvpn-server1-status.log
root@howard:/home/merlijn# cat /root/snap/easy-openvpn-server/x8/openvpn-server1-status.log
cat: /root/snap/easy-openvpn-server/x8/openvpn-server1-status.log: Permission denied

It’s the same issue the other way around. Inside of the snap, the snap_daemon user does not seem to be able to access files owned by root, regardless of their permissions.

Outside of the snap, everything appears normal; root account can access snap_daemon files.

For my OpenVPN snap, I need the status file to be accessible to both root and snap_daemon, since the former opens the file and, after OpenVPN drops privileges, the latter truncates the file.

This is expected for most paths as many AppArmor rules for snaps specify owner which means that only the owner of the file/path can access that file. What specific paths are you trying to access?

I’m trying to access $SNAP_USER_DATA/openvpn-server1-status.log (the snap should run as root). However, I can configure OpenVPN to use any path, as long as it is writeable by both root and snap_daemon. It’s a status file that the daemon uses to show how many users are connected to the VPN etc… The daemon opens it before dropping privileges and writes/truncates the file after dropping privileges.

I would expect a snap to have full access to its $SNAP_USER_DATA. Is there another path I should be using?

Well it does, for a particular user. snap_daemon has full access to its $SNAP_USER_DATA, and root has full access to its $SNAP_USER_DATA, just they don’t have access to each others. Looking at the default AppArmor profile for a snap, we can see that $SNAP_USER_DATA has an owner rule:

while $SNAP_DATA does not have this owner rule:

So what I would recommend you to do is to use $SNAP_DATA, but as root (i.e. before dropping privs to snap_damon), chown that directory as snap_daemon so that it can write to $SNAP_DATA, as normally only root can write to $SNAP_DATA.

1 Like

Actually, reading this page, this is explained under the section:

1 Like

I have existing snaps that install services with $SNAP_COMMON set up as their base directory for config files, state, etc. In order to migrate these existing installs to a newer version of the snap that uses the snap_daemon user, would it be enough to chown -R snap_daemon $SNAP_COMMON – or will I need to use a subdirectory of $SNAP_COMMON?

FYI, with snapd 2.45 you can use the setpriv command from the util-linux package to drop privileges without needing a separate LD_PRELOAD. See the updated “Dropping Privileges” (above) for more details.

1 Like

Snaps are not allowed to modify the ownership of SNAP_COMMON (or SNAP_DATA, etc), so no, this wouldn’t work. You should be able to use chown -R ... $SNAP_COMMON/* though (perhaps in a refresh hook).

I see that there is a postgres snap example that makes the use of gosu command. After installing and running this on an Ubuntu Core, the gosu command fails when attempting to run as snap_daemon:

error: failed switching to "snap_daemon": unable to find user snap_daemon: no matching entries in passwd file

This is using snapd 2.47.1

I can chown of a certain directory/file to snap_daemon but cannot run any scripts/binaries as the snap_daemon user. Has anyone have luck doing this?

Hi, I need to update this example snap to use setpriv instead which works on all systems and doesn’t have the same problem as gosu, if you wouldn’t mind could you file an issue on the github repo for me to fix that snap?

Independently the docs here should probably not reference gosu due to the fact it doesn’t work on Ubuntu Core.

Done. It appears that the setpriv works as an alternative. Thanks.

1 Like

I don’t understand why this number is here. Maybe it was meant to be “snap_daemon” instead?

… (they are synonymous)

1 Like

Oh, I see, I missed that, thanks :slight_smile:

snapcraft test utility seems to suggest that there is an additional valid value for system-usernames:

@pytest.mark.parametrize("username", ["snap_daemon", "snap_microk8s"])
def test_yaml_valid_system_usernames_long(data, username):
    data["system-usernames"] = {username: {"scope": "shared"}}
    Validator(data).validate()

So is “snap_microk8s” now a valid system username?

snap_microk8s doesn’t appear in the microk8s snap rev 3052 now on 1.23/stable, so I suspect this is prep work for future use and that only the microk8s snap would be able to use it. snap_daemon has a more generic use case.

That’s correct; for the time being we still lack a way for snaps to add arbitrary usernames, and those few taht we have defined so far are targeted for specific use-cases; that’s why, indeed, only the microk8s snap is allowed to declare the snap_microk8s user.