This isn’t the way to do this IMO. Instead, figure out how to make something akin to ‘confinement: classic’ work for shells on Ubuntu Core.
Classic mode was specifically designed to address things like shells and multiplexers for ‘general snaps’ (eg, tmux by itself as opposed to tmux shipped in the snap). In fact, there is a tmux snap in the store right now that uses classic confinement (I realize it isn’t available on Core). Also, the premise of having ‘generalized snaps that interact in arbitrary ways’ goes against snappy’s design-- snaps are very intentionally meant to bundle and to only go outside their application area in very controlled ways.
Letting snaps execute other snaps binaries is a difficult proposition to have the expected usability experience (eg, how do you share files? what about IPC? seccomp? cgroups?) and it needs to be very carefully designed because interfaces are a contract between providers (slots) and consumers (plugs). If we allow something to ‘plugs’ binaries in /snap/bin from another snap, what is that saying? To me with the way we’ve currently defined interfaces is that it means that the plugging side can count on the providing side not changing in terms of invocation or output. I’m not sure that is tractable in the most general sense. This interface could be a special case and the contract redefined, but to what (ie, this needs design)? How will we make sure that this doesn’t degrade the experience? (eg, today, snaps are free to change how their /snap/bin binaries work because nothing (by design) depends on them)
Furthermore, I think we are probably going the wrong direction if we let users dictate in arbitrary, uncontrolled ways ‘this snap is allowed to execute this (sub)set of binaries from this other snap’. Presenting options for interface connections (‘this snap wants to use the network-manager interface’) is one thing (and already a difficult proposition for the user to understand the security implications or the trust being given to the snap), it is quite another to have them enter commands for letting snap ‘foo’ run ‘/snap/bin/bar.baz’, or similar.
If we achieved a proper design, there are several concerns with your idea such as:
- ‘snap-confine would check if it is invoked from a snap’ - how would it do that securely? A secure design would have to leverage LSM labeling, but that wouldn’t be cross-platform atm
- IMO this crosses a line on exec() mediation I don’t think we want to cross. Up until now, snap-confine consumes files to setup a sandbox, but your idea puts snap-confine right at the heart of exec() mediation (ie, ‘should I allow exec of this or that?’). If the the LSM is already involved for the security label, the snap-confine logic could be greatly simplified to make an LSM query on if the access should be allowed or not (granted, this is still snap-confine performing mediation, but in a more robust manner). For example, if this were going to be implemented when AppArmor is the LSM, the allowed /snap/bin accesses each get a ‘Px rule’ in the security policy of the caller, snap-confine uses the libapparmor API to ask if /snap/bin is allowed, if not, EPERM, then exec
- how do snaps transition from one snap’s mount namespace to another? Ie, if foo exec()s bar.baz, bar.baz must execute within the mount namespace of ‘bar’, not ‘foo’ for security and because ‘bar’ has no knowledge of ‘foo’ (how to work with it, etc, etc). However, this presents a usability issue because ‘foo’ may want to give ‘bar’ a file it owns. How would it do that? To put in more concrete terms, say the tmux snap has the ‘home’ interface and is allowed to execute ‘bar’, but ‘bar’ does not have the ‘home’ interface. tmux tries to give ‘bar’ a file, but it cannot. /tmp is in a different namespace, ‘bar’ confinement doesn’t allow sharing, etc. I very strongly feel it would be a mistake to expand 'bar’s confinement to include whatever 'tmux’s might allow
- we’d have to consider the usability and security issues surrounding exec transitions for each LSM mediation type (ie, file, capability, dbus, network, mount, ptrace, signal and unix)
- this design just will not work with seccomp because with seccomp you can only go more restrictive. As such, the tmux snap would really need to have unrestricted seccomp to be able to execute arbitrary /snap/bin commands, because the commands in /snap/bin might plugs anything (eg, running docker under tmux)
- this design also won’t work with device cgroups. If tmux plugs something (eg, a serial-port, something quite reasonable for tmux to do), then tries to run something in /snap/bin that plugs something else, then the /snap/bin/binary will fail because the device isn’t in the cgroup for tmux
- there are probably other things I’m missing since this is all OTOH
You could try to fix the seccomp and cgroup issues by trying:
- if the command is in /snap/bin, snap run asks snapd to execute it on its behalf
- snapd takes the security label of snap run and performs an LSM query to see if it is allowed, and if so, executes it
The security implications for getting snapd involved in this way are pretty scary cause it means this API for snapd has to be bullet-proof to not allow privilege escalation or confinement escape. Furthermore, it completely breaks the parent/child relationship of the caller/callee, breaks process groups, breaks process sessions, etc. It doesn’t solve the issues surrounding sharing files and would just be a very poor experience.
But classic confinement doesn’t suffer from any of these issues (sharing files isn’t totally transparent but at least possible). The big problem with classic confinement is that it doesn’t exist on Ubuntu Core. IMHO, the best solution would be figuring out how to address that. Perhaps that is simply a ‘confinement: shell’ that operates very much like ‘classic’ in terms of sandbox setup except that it is allowed on core, and we restrict its use. This allows tmux, screen, byobu, zsh, csh, etc, etc to work.