SELinux interface support

I’ve been taking a look at what would be involved in writing SELinux policy for covering the existing interfaces. My current impression is that this is going to be (a) difficult, (b) fragile and © difficult, but let’s start with a quick introduction to SELinux and some fundamental differences between it and Apparmor.

SELinux is based around processes running in contexts and interacting with resources that have labels. The SELinux policy describes how these interact - for example, a process running in the user_t context could be given permission to read files labelled user_home_t by providing

allow user_t user_home_t:file read;

Any files that are labelled as user_home_t would then be readable by any process in the user_t context (assuming that it also satisfied traditional Unix permissions). All access is mediated by labels - it’s not possible to grant access to a specific path.

Like Apparmor, policy isn’t restricted to the kernel - dbus and systemd (for example) are SELinux aware and can interpret policy to determine whether a process running in a specific context should have access to a resource they control. That side of things seems pretty straightforward.

So, conceptually, a lot of the this works identically on SELinux and Apparmor - a policy module is synthesised for each Snap and loaded into the system-wide policy. However, SELinux policy can’t exist per-process - instead, we need to generate a unique context for each app in a Snap and grant permissions only to that specific context. Something along the lines of SVirt (http://danwalsh.livejournal.com/30565.html) would work here, using a generic context with an MLS extension that’s unique to the app.

This is where things get more difficult. As previously mentioned, SELinux is label-based rather than path-based. So, where an existing interface might do:

/dev/tpm0 rw

an SELinux policy would have to do:

allow snap_t:s4:c123 tpm_dev_t:chr_file rw_chr_file_pmerissions;

The problem is that there’s no inherent way to know in advance what hte label of /dev/tpm0 is - this is something that may differ between distributions. Some distributions may provide a specific label for /dev/tpm*, others may have it labelled with a generic device type. In the latter case, granting access to that will also grant access to any other device nodes with the same label.

Dbus has the same problem. The policy for permitting a task to speak to dnsmasq over DBus would look something like:

allow snap_t:s4:c123 dnsmasq_t:dbus send_msg;

Where, again, dnsmasq_t may be entirely different between distributions.

In the majority of cases this probably doesn’t provide an impediment to writing a policy that works for a specific distribution - Fedora has sufficiently fine-grained labels that each existing interface can probably be ported to that. My concern is that doing so in a way that’s entirely consistent with the Apparmor implementation may not be possible, and that it may be differently inconsistent across different distributions. This may result in the same interface giving access to different functionality on different distributions.

The other concern I have here is that there’s no good way to keep all of this in sync. Distributions may provide policy updates during a release, and Snapd would have to be updated in lockstep with that. Worse, if any changes are made to an interface, the same change will need to be made to every implementation (Apparmor, Fedora SELinux, Debian SELinux, Suse SELinux and so on) and it’ll need to be validated against every distribution that has support. Realistically, this isn’t possible - mistakes will be made, and things will end up broken as a result. The test matrix is just going to be too large.

I don’t think per-distribution SELinux policy is going to be viable, which I think means there’s two real choices here:

  1. Use SELinux to confine Snaps from each other, but for no other purpose. All other resource access would then be via Portals, as in Flatpak.
  2. Add kernel support for stacking monolithic LSMs and have Snaps running in an environment where Apparmor policy is validated before SELinux. Per-Snap access control would remain in the form of Apparmor policy, while snapd itself would be confined by SELinux. This would also require modification to dbus and systemd to support validating requests against multiple policies.

To be fair, I’m leaning to the pessimistic here - I’ve maintained SELinux policy at two separate companies, and I can’t claim that the experience has been a bag of fun in either case. Does anyone see an easier way of doing this that doesn’t run into these problems?

5 Likes

Practically speaking, there are really only two variants of the SELinux policy out there: the one based on Fedora (refpolicy + improvements) and the Android one. Last I checked, SUSE’s policy is also derived from Fedora’s, as are the ones for other distributions that offer SELinux as an option. Fedora’s own SELinux policy code has knobs for supporting the different FHS for Debian and Gentoo, and I’m aware of some collaboration between Arch Hardened and Fedora for SELinux stuff.

Additionally, at least for now, there is a consistent set of labels for Snappy code defined in snapd-selinux (shipped in the Fedora builds of snapd, part of the source in data/selinux).

This was certainly considered as a starting point at the summer sprint a year ago (the sVirt style approach). Migrating most resource access to Portals is not necessarily a bad move anyway, as it further generalizes the sandboxing/confinement mechanisms and reduces the reliance on MAC. However, back then, it was mostly thrown out as the idea of reusing Portals wasn’t appealing. I don’t know if that has changed now.

That would mean that every distribution that prefers SELinux would need to maintain double LSM infrastructure. I don’t think that’s a realistic option, especially with the two oldest MACs (AppArmor and SELinux) overlapping quite a lot in terms of capabilities. In the past, there’s been quite a lot of resistance to the idea, and even if it were supported in the kernel (as it is not now), then the kernel teams of distributions doing this need to enable it.

In my case, I would have to somehow convince the Fedora kernel team to enable it, and get someone to package and maintain AppArmor stuff for Fedora. On top of it, I’d need to manage both systems for host confinement and snap confinement. That’s going to be really hard for me, and I’ve done at least some of these things before. At this level of complexity, I think most people would just give up.

That work is getting started for the desktop portals like file access. But that will only solve a small part of the use-cases that Snapd interfaces cover.

Hi Matthew,

Thanks for joining this conversation. We’ve been investigating and gradually working our way through SELinux support, so it’ll be great to have more collaboration with someone that knows it inside out.

Although I’m much less experienced in SELinux than you are, my feeling is that the situation is brighter than it may feel – perhaps a side effect of being less bounded to the AppArmor details than you might imagine we are. In reality, the interface mechanism in snapd is meant to represent high-level intent, rather than file-level access information.

As a good example, we have that interface labeled network-control. That says nothing about which specific files will be accessible to that interface on a particular distribution. What’s represented there is the intent of handing to that application the needed access for manipulating the network stack of the system, whatever that turns out to mean locally.

As another good example, we have a serial-port interface which may be defined purely in terms of USB vendor and product IDs, and if you look purely from the apparmor perspective it allows quite a few options under /dev/tty*. But in fact this is restricted to a particular device via udev tagging and process cgroups, which resembles quite a bit the reality you’re describing where the process and the filesystem are tagged.

With those ideas in mind and as a preliminary suggestion of where to start, instead of trying to mimic perfectly the apparmor implementation, I’d suggest taking a higher level view of what each of these interface means and how the proper representation of them would look like in a SELinux universe. Yes, roles, domains, etc, may be different from distribution to distribution, but we should be able to easily map that out when really necessary, and in some cases we can use introspection techniques or other confinement mechanisms.

What gives me some assurance that this is not an unsurmountable amount of work is that we only need to that once for each interface, and the number of interfaces is orders of magnitude smaller than the number of applications. So, if is possible to map out application by application in the several distributions, we should be able to map out those interfaces too.

About it being fragile, yes, we need to be careful, and the infrastructure we have in place nowadays reflects that. Every single one of our PRs fires tests against 8 severs at the moment, on a matrix of Ubuntu 14.04, 16.04, 32bit, 64bit, and Ubuntu Core. We’re working right now to add Debian and Fedora to that set. Our interfaces have tests ensuring their high-level expectation is working across that matrix.

We can only claim SELinux support works on a particular distribution if that distribution is included in that suite, as that ensures we can keep them working over time both in terms of snapd changes and in terms of their own release cycles.

As a practical way forward, instead of bracing the world I suggest taking one interface at a time, and looking at how we might represent it in a particular distribution. It’s okay for it to not be a universal solution… we’ll get there eventually.

Practically speaking, there are really only two variants of the SELinux policy out there: the one based on Fedora (refpolicy + improvements) and the Android one

There’s others - not everybody who runs a distribution uses the SELinux policy that they ship with, and there are various places running internal distributions who may have their own policy for various reasons. Not having a reasonable avenue of support for them limits the reach of Snap.

Relying on refpolicy also ends up being a restriction on snapd. If a new interface is added that can’t be expressed with existing refpolicy, refpolicy needs to be extended. That change then needs to be backported to every distribution (presumably including things like RHEL) before snapd can make it available everywhere. Until that happens, you have a situation where some Snaps require interfaces that aren’t available and user confusion is generated.

We already have this problem with AppArmor anyway. I consider this a fairly weak argument, and perhaps this is an avenue where snapd simply needs to be smarter. How exactly, not sure. Maybe runtime detection of supported confinement features, and degrade gracefully?

We already do this today :slight_smile: We’re planning to get better at some aspects of it though (having snapd tell snap-confine about facts more).

I’m planning a longer reply to the original post, just got into coding in the morning and afternoon.

What’s represented there is the intent of handing to that application the needed access for manipulating the network stack of the system, whatever that turns out to mean locally.

The problem I see with not providing strong guarantees about exactly what an interface provides is that apps are inevitably going to end up depending on the behaviour of a particular implementation of an interface and fail to work on other distributions, which is exactly what Snaps are intended to avoid.

However, based on Neal’s feedback and some conversation with Joshua Brindle, I think the closest thing to a workable approach is going to be to ensure that refpolicy has interfaces (in the refpolicy sense) that match each interface (in the Snap sense). The generated policy then ends up looking like:

snap_network_access(snap_app_whatever_t)
snap_x11_access(snap_app_whatever_t)

with the system policy being responsible for ensuring that those map to something appropriate for the distribution. This takes responsibility out of the hands of the Snap developers and pushes it back onto the distribution, which feels like the only supportable way of handling this. However, we’re still left with a few problems:

  1. My original suggestion that each app run in a different MLS level doesn’t work - MLS restrictions are imposed via constraints, not in allow rules, which means you can’t grant permissions to a single level. I think that means having to generate a new attribute for each snap and a new type for each app and then granting those types the attribute, and using the attribute to gate access to the filesystem resources.

  2. As far as I can tell, dbus only supports using selinux at the name level, not the path or interface level. This means that any interfaces that rely on being able to grant access to a subset of the functionality under a name can’t currently be represented in selinux.

  3. This still makes it impossible to add new interfaces to snapd without the cooperation of the target distributions, and figuring out the practicalities and the politics of that don’t sound fun.

  4. It still doesn’t work in places using SELinux but not using refpolicy

  5. It’s still going to result in differences in what’s restricted depending on the underlying distribution

(5) is the one that scares me most - if an interface ends up being more permissive on one distribution than on others, apps tested on the stricter distribution may end up with the assumption that they don’t have to handle a specific security case because the snapd policy will block it for them. If that assumption turns out to be false somewhere else, we suddenly have a security issue. That’s arguably the fault of the distribution in question, but it’s the sort of thing that’s likely to happen in the absence of a strict definition of what permissions an interface is intended to grant. The inverse case of one distribution being stricter than others is less scary in one sense (apps fail rather than apps suddenly have security issues) but worse in another sense (users get annoyed that apps don’t work, start running everything in dev mode).

We already have this problem with AppArmor anyway.

Do we? The policy expression in the existing interfaces is pretty self-contained (with the exception of stuff like expressing dbus permissions) and ought to work pretty much regardless of what the distribution’s Apparmor policy looks like .

The AppArmor policy descriptions are invalid for nearly all Linux distributions that even offer AppArmor, so it doesn’t work anyway. It requires patches that exist only in the Ubuntu kernel at this time. There is ongoing work to upstream the changes, but they’re still not there yet.

The AppArmor policy descriptions are invalid for nearly all Linux distributions that even offer AppArmor, so it doesn’t work anyway. It requires patches that exist only in the Ubuntu kernel at this time. There is ongoing work to upstream the changes, but they’re still not there yet.

I don’t think they’re equivalent cases - the expression of policy still doesn’t heavily rely on external components. There’s only really 8 includes in the existing interfaces, whereas with this approach every snap interface would have an equivalent dependency.

That’s correct. However I think generally this won’t be an unsurmountable problem. The AppArmor policy tends to go fine-grained with things that weren’t designed with confinement in mind but it was somehow important to allow access to the service (eg, trying to limit the attack surface of so called ‘transitional’ interfaces). Something like Portals or Ubuntu’s trusted helpers (eg, locationd, etc) don’t really need to go fine-grained like this cause they are designed with application isolation in mind and we can simply consider those services trusted. Things like network-manager or udisks2 that aren’t designed with application isolation in mind, we just require manual connection. I’m not saying that the existing policy perfectly represents this POV (eg, we might’ve gone a bit stricter with AppArmor confinement because we could) and there will be places where the policy differs, but I think if we consider the base declaration (eg, if the interface is manually or auto-connected), we can come to something reasonable.

I also want to reiterate Gustavo’s recommendation of picking one interface at a time and I’ll suggest that there is nothing saying that snapd on an SELinux enabled system must offer the full range of interfaces. There is a concept in snapd (that probably needs to be more fully realized in design and code) where the system advertises the available interfaces and snaps that use these interfaces are installable and ones that use other interfaces are not. In this manner, we can pick the most useful and/or easiest ones first and iterate on the others in the fullness of time. For example, Fedora (maybe) could offer network, network-bind, wayland (in progress), gnome3 (in progress) and Portals in the earliest iterations (I just pulled those out of the air, obviously others could be added/substituted).

This is correct, as I recall form the kdbus discussions this is also unlikely to change. The good part of this is that for new services this is likely sufficient. The problem is that for the vast majority of current desktop software (where confined snap talks to an unconfined system or user service) this is still relevant.

This is true, but note that quite a large patchset went up already and more is coming.

To your larger point, thus far we’ve said that distributions that don’t offer the full AppArmor mediation should have snapd installed in forced devmode. I think enough time has gone by to understand the problem and perhaps we can reconsider this stance and consider a more dynamic approach. OTOH, we could introspect the kernel to see what it supports (AppArmor has an easy way to do this) and on systems with AppArmor enabled, only offer interfaces that use the features that the kernels support. Eg, many interfaces are only concerned with file rules, so we could let those interfaces through. DBus isn’t supported? Don’t offer network-manager. Don’t have mount rules? Don’t offer udisks2. Maybe this isn’t the direction we want to go, but this sort of thing is possible.

I think @mjg59 is right, we rely on the base apparmor templates but the “meat” is in what snapd defines. Once Linux 4.13 is released any distribution can choose to enable apparmor and get complete confinement without rebuilding most of snapd (just snap-confine but that distinction is gradually going to be removed with all-runtime decisions)

That’s an unsolvable problem either way, because despite the divergences between distributions, there will be divergences across time between the release cycles of each of those distributions. So small variations will exist, and we’ll accommodate the real world in the best way possible.

Exactly, and requiring every single distribution to cooperate towards a perfectly synchronized world is simply not realistic.

Instead, let’s map the intent of those interfaces into SELinux and have a great testing suite that ensures the fundamental properties of each interface is preserved across time and across distributions. As @jdstrand points out, we don’t need to do all at once. Even with a single interface we can already unblock strict snaps on that particular distribution and then continue to progress towards the full range.