Snappy and users and groups (obsolete)

The latest ideas on this subject are covered here.


Introduction

Today, snappy’s notion of users (and groups) is primitive:

  • daemons run as root
  • commands run as the user invoking the command

For an initial implementation this has worked very well. The (untrusted) snaps run under root-strong confinement and the access to various resources is mediated via security policy.

There are quite a few important use cases that are not covered by the current implementation however:

  1. adding system users/groups https://launchpad.net/bugs/1606510
  2. supporting device access via ACLs for granting access of devices to (non-root) users. https://launchpad.net/bugs/1646144
  3. per-snap opt-in users to support things such as privilege dropping/separation within snaps. https://launchpad.net/bugs/1619888
  4. Supporting uids/gids in chroot environment

‘1’ and ‘2’ deal with users accessing resources that have been granted to the snap via the snap’s interface security policy and highlight the fact that while the snap may have access to a resource, that doesn’t necessarily mean that the user of the snap should have access to that resource.

‘3’ and ‘4’ deal with supporting a wider range of existing software.

This is a complicated topic and while all 4 cases don’t necessarily need to be implemented at the same time, they all should be considered when designing how users and groups are supposed to work in a snappy world. This topic is intended to help people assigned to implementing any or all of these cases to have the necessary design.

Let’s first consider each use case in a little more detail.

System user and groups

Existing software often uses the concept of users and groups as a security mechanism to grant access to some application resource via traditional users and groups. Examples include lxd, docker and libvirt which all create UNIX sockets with 0660 permissions with ownership of root:<some group>. They all do this because the socket provides privileged access to the host system that should only be granted to trusted users. In order for a non-root user to be able to use these services, the user must be added to the system group for the service.

Today, users that want to use snaps that use this security mechanism must either:

  • always run the command under sudo (eg, sudo -H lxc ...)
  • after snap installation, perform additional steps to create the system group manually, then add users to that group (eg, sudo addgroup lxd && sudo adduser foo lxd)

Device access

For many devices such as audio, UNIX group memberships are not used and instead ACLs are set on devices. Eg:

$ getfacl /dev/snd/seq
# file: dev/snd/seq
# owner: root
# group: audio
user::rw-
user:jamie:rw-
group::rw-
mask::rw-
other::---

(note ‘user:jamie:rw-’ in the above).

In the distant past, all these devices were chmod 0660 with a group specific to that device (eg, ‘audio’) or a catchall group like ‘plugdev’. While some historic groups are still found on the system (eg, ‘audio’), adding users to these groups or creating new groups and using chgrp on the devices should be discouraged on modern systems. In the not so distant past (before systemd), device access is controlled via ACLs that were handled by consolekit via ‘udev-acl’. With systemd, this is handled by ‘uaccess’ via /lib/udev/rules.d/70-uaccess.rules, 71-seat.rules and 73-seat-late.rules where the “uaccess” builtin is used to set ACLs (and some historic groups for compatibility). Since the ‘acl’ package has been seeded in Core for some time (it wasn’t at first), anything that matches Core’s uaccess rules will just work today (eg, /dev/video*).

Unfortunately there are quite a few devices that are not covered either because there isn’t a uaccess rule for it (eg, a serial-port) or because the rules don’t apply for some reason (logging in via ssh and accessing /dev/video* or /dev/snd/* (seat rules don’t apply cause not local)).

Opt-in per-snap users/groups

A lot of existing applications are designed with the notion of privilege separation and/or permanently dropping privileges to secure their code. For example, postgresql, mysql, apache, nginx, etc. Some want to start as root to bind to a port and immediately permanently drop privileges and others want to fork processes under another user to (for example) handle untrusted input.

Today, all these daemon applications must run as root and are not allowed to drop privileges. While the security policy will keep the system safe and will keep the applications isolated, the security stance of the applications themselves is lessened because their security mechanisms can’t be used under snappy (eg, consider an application that handles untrusted input that would normally run a process under a separate user-- under snappy it is the same user so if there is a bug in processing the untrusted input, that process is able to attack the main application).

Soon (now that snap-confine is re-exec’d and seccomp arg filtering work can recommence) we’ll allow snaps to use the ‘daemon’ system user/group to drop privileges. While a shared user/group, this is no worse than the shared ‘root’ user/group and will allow applications to leverage their security mechanisms for up to one group.

Snappy should allow applications to use multiple users and groups. For example, it would be desirable for a complex snap that uses the LAMP stack to be able to let apache drop to a different user than mysql is dropping to so that successful attacks against the apache process don’t give access to mysql’s resources.

Chroot environments

Sometimes it is useful to take an existing chroot setup, put it into a snap and then have the snap chroot into the directory. There are several issues with this because processes running inside the chroot will use the chroot’s /etc/passwd, /etc/group, etc. If an application within the chroot tries to privilege drop or start as a user in the chroot’s /etc/passwd, it may not map in expected ways (eg, the security policy might allow dropping to the ‘daemon’ uid on the host, but ‘daemon’ in the chroot is a different uid so it is blocked).

Classic distro vs Core

An important consideration when designing users and groups for snappy is that:

  • Classic distro systems have writable passwd/group/shadow/etc databases
  • Core systems have read-only passwd/group/shadow/etc databases and writable ‘extrauser’ databases via libnss-extrausers
  • Classic distro systems don’t usually have libnss-extrausers installed
  • extrausers cannot be used to add users to system groups (ie, it may only be
    used to add users in the extrausers db to groups in the extrausers db)

Implementation ideas

Global namespace

For ‘2-4’ (and sometimes ‘1’), the overarching idea is that snappy manages users and groups in the global namespace in some manner by prefacing ‘snap_’ to the user/group (‘snap.’ is prettier, but see https://bugs.launchpad.net/snappy/+bug/1606510/comments/16) and new interface backends will be used to implement this. Using the global namespace preserves the snappy design principle that snaps should integrate fully into the system and keeps existing interface connection mechanisms clean. If done correctly, snaps get however many users and groups they want, device access is consistent with standard practices (adduser/usermod), chroots work easily and adding system groups is lightweight.

For ‘1’ (system users and groups), an idea has been put forth that creating system users and groups should be granted via snap declarations. In order to support common users and groups across distributions, these users are not required to be prefixed with ‘snap_’. Whether or not the snap.yaml declares the system users and groups to add is TBD (to be designed). Snappy gains a ‘user’ backend to, upon install of the snap, create the user/group and updates the seccomp policy to allow chown/setuid/etc to this user/group.

For ‘2’ (device access), for things not covered by udev and systemd automatically via shipped configuration, we use setfacl to tag devices via udev rules. Eg, today serial devices are root:dialout 0660 and have no udev ACLs on them. The new ‘user’ backend is used by the serial-port interface to specify that all serial ports can be accessed via the ‘snap_dialout’ group. In addition to creating the ‘snap_dialout’ group, the backend adds a udev rule to setfacl the serial-port devices (or maybe use the uaccess mechanism) to ‘snap_dialout’ so that sudo adduser <user> snap_dialout would grant access to the serial ports for that user.

For ‘3’ (per-snap opt-in users), snap.yaml has declarations for the users/groups to add. Eg:

users: [ foo ]
groups: [ bar ]

This is similar to ‘1’ but the backend creates the snap_${SNAP_NAME}_foo user and snap_${SNAP_NAME}_bar group and updates the seccomp policy to allow chown/setuid/etc to these users/groups. Snap developers update their application’s configuration to use the snappy-created users and groups (snap run could also export these to the environment if that is deemed helpful). Importantly, snap declarations are not required for this because the users and groups are snap-specific (though we might limit this to some reasonable number, eg, ‘16’, ‘32’, something less than 65535 :)).

To support ‘4’ (uids and gids in chroots), it isn’t substantially different than ‘3’ in that the snap requests users and groups and the user backend adds them and the seccomp backend allows chown/setuid/etc to these users/groups. Snappy could stop here and let the snap’s wrapper script getent passwd snap_$SNAP_NAME_foo for anything it is interested in and tack that onto the end of its chroot/etc/passwd file (piping to ‘sed’ if desired) and the uids/gids match with the host so security policy works as expected.

Snappy could be more helpful and generate an /etc/passwd, /etc/group, etc that contains the important users/groups from the hosts passwd and group databases, tacks on the snap-specific entries at the end, then bind mount these files into the chroot. A potentially nice extension of this idea is to have snappy generate passwd and group databases that use friendly names that the chroot expects, but have the uids/gids map to the hosts uids/gids. Eg, suppose we have a snap that wants ‘www-data’, ‘mysql’ and ‘discourse’ users and groups and to use those exact names within the chroot:

users: [ www-data, mysql, discourse ]
groups: [ www-data, mysql, discourse ]
apps:
  baz:
    daemon: simple
    bind-users-and-groups: $SNAP/path/to/chroot

Here, the snap_${SNAP_NAME}_www-data, snap_${SNAP_NAME}_mysql and snap_${SNAP_NAME}_discourse are all added to the system and security policy of the snap, and snap-confine will bind mount this (etc/group omitted for brevity):

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
www-data:x:30000:30000:www-data:/:/usr/sbin/nologin
mysql:x:30001:30001:mysql:/:/usr/sbin/nologin
discourse:x:30002:30002:discourse:/:/usr/sbin/nologin

(‘root’ and ‘daemon’ are taken from host, www-data, mysql and discourse uid and gid taken from the host, with ‘/’ set as home (ie, the chroot)).

It is important to note that for ‘4’, existing files within the chroot may not map predictably from the applications point of view, so some additional design is needed. Eg, if the squashfs already had ‘$SNAP/chroot/var/www/something’ chowned to the uid of ‘www-data’ from ‘$SNAP/chroot/etc/passwd’, that won’t map to snap_${SNAP_NAME}_www-data on the system. One way to handle this would be to request specific uids, but this would be problematic at scale. Other options exist (eg modifying squashfs mount options, http://bindfs.org/, …) and would have to be explored (UPDATE: see comments below for how to do this with a uid mapping approach).

Open questions:

  • what should snap.yaml look like? Should system users be declared in the snap.yaml? If so, how does that declaration work with opt-in per-snap users?
  • when to use extrauser db vs system db. Seems plausible to have the users backend detect OnClassic and operate accordingly. Since the users/groups use ‘snap_…’ we don’t have to worry about collisions (practically speaking; we have to code for them of course)
  • should snappy call ‘setfacl’ directly or add ‘uaccess’ rules (probably setfacl to support systems where uaccess isn’t used)
  • should users-and-groups not be toplevel and be per command instead? (probably not because the chroot scenario might want all of the users in there)
  • should we support setting ‘home’ and ‘shell’ in the passwd db? Sane defaults might be /snap/$SNAP_NAME/current for ‘home’ and ‘/bin/sh’ for shell.
  • is bind mounting /etc/passwd and /etc/group what we want to do for chroots? If so, what about supporting ‘home’ and ‘shell’ in the chroot’s passwd db? (it might be handy to have 'www-data’s home be ‘/var/www’ in the chrrot
  • what is the UX for adding users to device ACLs? snap connect <snap>:serial-port --user=<user>? The adduser command? Something else?
  • ???

###User namespaces
It is conceivable to use user namespaces to support users and groups. With this approach snaps get a range of uids/gids that they can use how they want. This is potentially friendly for chroots but there are a number of considerations:

  • user namespaces does not address system users and groups or device ACLs
  • interface connections would become more complicated with snaps running in different user namespaces. Snap command vs snap daemon interactions would have to be carefully designed to work correctly
  • need at least kernel 3.12 for user namespace support (man user_namespaces) but there have been a lot of CVE fixes and newer kernels are better. Porting user namespaces to older kernels is not feasible so relying on this feature in snappy for such a core proposition as users and groups would be problematic
  • the intersection of security policy and user namespaces could become very complex
  • user namespaces goes against the design principle that snaps should integrate fully into the system since separate commands need to be used for administering them and things such as ‘ps’ output is less clear

While user namespaces are great for system or application containers, snappy is different from those container technologies and I do not recommend the use of user namespaces as the primary means of management of users and groups. It goes against current snappy design principles, is not backward compatible with older kernels, doesn’t address all use cases, and it complicates the codebase and security policy. Futhermore, for device ACLs and system groups we need to work within the global namespace anyway and we can support opt-in per-snap users (and potentially chroots. UPDATE: see comments below for uid mappings) well within the global namespace.

2 Likes

This is interesting to think about because the scenario where the snappy developer has files in the squashfs that are not root owned whose ownership is intended to map to values within the system is quite different from the current implementation. Importantly, today, snapcraft specifically uses ‘-all-root’ with mksquashfs and the public store verifies ownership of files to root:root to avoid any assumptions from the developer’s environment and problems that might occur within the snap. For similar reasons, we also require snapcraft to use ‘-no-xattrs’ (allowing xattrs would immensely complicate acls and inode-based security labels (ie, selinux)).

The ‘global namespaces’ solution for ‘4’ would support the scenario where everything in the squashfs has root:root ownership and a wrapper copies/symlinks out to $SNAP_DATA files that are owned by snap_$SNAP_NAME_foo (with the wrapper updating its chroot/etc/passwd on the fly based on getent or perhaps snappy bind mounts a snap-specific chroot/etc/passwd). We could simply say that this is the extent of the supported solution.

If we want to take this a step further and support snaps with non-root:root ownership in the squashfs to map properly, this would need very careful design to work properly with strict confinement. Some thoughts:

  1. squashfs could grow mount options to map uids in the squash to uids on the host. eg mount -t squashfs -o uidmap=1000:30000,1001:30001,1002:30003 ... such that uid ‘1000’ in the squashfs shows up as ‘30000’ when mounted
  2. use http://bindfs.org/. Bindfs is a fuse filesystem “for mirroring the contents of a directory to another directory. Additionally, one can change the permissions of files in the mirrored directory.” Eg:
$ sudo addgroup snap_hello-bindfs_group0 --gid 30000
$ sudo addgroup snap_hello-bindfs_group1 --gid 30001
$ sudo adduser --system --no-create-home snap_hello-bindfs_user0 --uid 30000 --gid 30000
$ sudo adduser --system --no-create-home snap_hello-bindfs_user1 --uid 30001 --gid 30001
$ hello-bindfs.sh
bash-4.3$ ls -l $SNAP/files
total 0
-rw-r--r-- 1 jamie    jamie    0 Apr 20 07:46 1000-owned
-rw-r--r-- 1 someuser someuser 0 Apr 20 07:46 1001-owned
-rw-r--r-- 1 root     root     0 Apr 20 07:46 root-owned
bash-4.3$ exit
$ sudo bindfs --map=jamie/snap_hello-bindfs_user0:@jamie/@snap_hello-bindfs_group0:someuser/snap_hello-bindfs_user1:@someuser/@snap_hello-bindfs_group1 /snap/hello-bindfs/x1/ /snap/hello-bindfs/x1/
$ hello-bindfs.sh
bash-4.3$ ls -l $SNAP/files
total 0
-rw-r--r-- 1 snap_hello-bindfs_user0 snap_hello-bindfs_group0 0 Apr 20 07:46 1000-owned
-rw-r--r-- 1 snap_hello-bindfs_user1 snap_hello-bindfs_group1 0 Apr 20 07:46 1001-owned
-rw-r--r-- 1 root                    root                     0 Apr 20 07:46 root-owned
bash-4.3$ stat $SNAP/files/1000-owned 
  File: '/snap/hello-bindfs/x1/files/1000-owned'
  Size: 0         	Blocks: 0          IO Block: 1024   regular empty file
Device: 2dh/45d	Inode: 7           Links: 1
Access: (0644/-rw-r--r--)  Uid: (30000/snap_hello-bindfs_user0)   Gid: (30000/snap_hello-bindfs_group0)
Access: 2017-04-20 07:46:26.000000000 -0500
Modify: 2017-04-20 07:46:26.000000000 -0500
Change: 2017-04-20 07:46:26.000000000 -0500
 Birth: -

With appropriate changes to seccomp policy we can copy from $SNAP and get expected ownership:

bash-4.3# cp -a $SNAP/files/1000-owned $SNAP_DATA/was-1000-owned
bash-4.3# ls -l $SNAP_DATA
total 0
-rw-r--r-- 1 snap_hello-bindfs_user0 snap_hello-bindfs_group0 0 Apr 20 07:46 was-1000-owned
bash-4.3# exit
$ ls -l /var/snap/hello-bindfs/x1
total 0
-rw-r--r-- 1 snap_hello-bindfs_user0 snap_hello-bindfs_group0 0 Apr 20 07:46 was-1000-owned

What’s interesting about bindfs (which is doing in userspace what modifying squashfs in the kernel would do) is that we have the technical ability to map uids from the developer to snappy assigned uids/gids, but how that is expressed in the yaml needs TBD. Perhaps:

# Normal "don't need a mapping" case
users: [ www-data, mysql, discourse ]
groups: [ www-data, mysql, discourse ]

# Chroot "need a mapping" case
users: [ www-data:10000, mysql:10001, discourse:10002 ]
groups: [ www-data:10000, mysql:10001, discourse:10002 ]

If we express how the internal uids (ie, www-data in the chroot has 10000) in the snap.yaml, only then does the mapping kick in. In addition, snapcraft and the review tools have enough information to enforce everything is root-owned except these few things. With the bindfs/squashfs mount options, we shouldn’t have to perform any bind mounts of etc/passwd either.

@niemeyer, @mvo, @tyhicks, @ratliff - the is the forum post for our meeting today.

That’s what I had in mind when we discussed today, but now that you ask, maybe we should create a new topic with the current proposal alone, so we can get feedback on that and inform without people getting distracted with the brainstorms that made us reach the proposal. From there we can link to this topic so some of the background is available for those interested.

What do you think?

I can do that. I’ll write it up when I have a few moments.

@niemeyer - new topic is Multiple users and groups in snaps