Handling of the "cups" plug by snapd, especially auto-connection

Can you detail why this is too complex? I think that this would be a great solution, is to have the cups snap operate in two modes, one where there is no other cupsd on the system in which case the cupsd from the cups snap does everything, and two where cupsd from the cups snap detects another cups on the system and instead runs itself in the cupsd firewall/proxy mode, forwarding all requests to the classic cupsd the user has installed.

This setup would ensure that we can have the cups snap always be installed even on other distros where there is a classic cupsd and optionally classic drivers as well but still have a good user experience, and also would let us safely make it so that installing a snap which plugs the cups interface ends up also installing the cups snap.

This is not enough, since any kind of action the snapcraft-runner script to prevent access to cups is done after the confinement has been setup, so someone could just manually craft a snap which sidesteps the snapcraft-runner and thus is able to escalate privileges to lpadmin. Any kind of group dropping operation would need to be done by snap-confine inside snapd, and I don’t think we have any precedence for always dropping a group from the currently executing process, it feels a bit user-hostile to do that IMHO…

Edit: actually I see you came to that realization in your followup post…

Proposal for a new direction

I don’t want to disregard all the existing work that has been done on this, but I do want to propose what I view as a fundamentally better setup than what we have today, albeit a slightly more complicated one, that has IMHO the best user experience and security properties. Here is my proposal:

  1. The cups snap exposes a content interface slot which provides the cupsd socket from somewhere in $SNAP_DATA, etc. and not the global location - this content interface slot can be made to auto-connect globally to any snap wishing to connect a plug to it via store assertions

  2. Application snaps wishing to print can plug a content interface which auto-connects to the above content interface and uses default-provider to ensure that the cups snap always gets installed and can always provide the cupsd socket

  3. A snapcraft generated wrapper script from one of the extensions sets the CUPS_SERVER env var to the location of the cupsd socket that is shared over the content interface, and the extension also ensures that the content interface plug is added to any snap that is desktopy. Or maybe all this cups stuff becomes it’s own separate, orthogonal extension (that might be better since there are probably a lot of desktop snaps that don’t need to print and thus don’t need to have the the cups snap installed on the users system).

  4. The cups snap auto-detects whether there is already a global cups socket from a classically installed cupsd, and if that exists, then it operates in the firewall/proxy mode. If there is no such socket already being listened on, then cupsd from the cups snap either listens on the global cups socket in addition to the one in $SNAP_DATA (I know that this kind of thing can be done via Go to listen on multiple unix sockets at the same time, I dunno how easy it is to do in cupsd source code), or it creates a symlink at the global location pointing to the one in $SNAP_DATA, etc. (then unconfined apps not run as snaps just end up talking directly to the socket in $SNAP_DATA by following the symlink). This ensures that when there is a classic cupsd, classic apps talk to it without need to care that the other side is a snap, and snap apps always talk to cupsd over the content interface shared from the cups snap and thus are always mediated.

  5. We could then simplify the cupsd snap implementation to one that checks if the client process connecting to it’s socket is a snap or not, if it is a snap, then we always provide it permission to print, and we deny it ability to do admin tasks if the client process does not have the cups-control interface plugged.

  6. All of the above means we can actually just get rid of the cups interface as it exists, and simply make the claim that snaps can by default always print if they are doing so via the cups snap content interface which is sharing the socket (and a user wanting to deny access to printing can disconnect the content interface, then the app snap does not have access to the socket at all).

  7. Due to 6, we can simplify the policy in snapd to only allow an application slotting cups-control to read/write to the global socket in /run, and this global socket access is not allowed anywhere else. The cups interface can go away entirely, and the plug side of the cups-control interface also can just be empty, effectively only existing for the mediating version of cupsd to inspect and grant admin access to a snap plugging cups-control. If we really wanted we could even introduce a new interface cups-support which allows doing all the things that cupsd needs to do from inside a snap, but that’s probably unnecessary at this point and we can just put all of those accesses to the permanent slot side of cups-control.

The above setup has the following nice properties:

  • app snaps never are provided access to the global cups socket (which leads to confusing policy in snapd where it’s unclear if the cups interface is providing access to a global socket from another snap or access to a socket that is coming from the classic host world)
  • app snaps always are able to print by talking to a version of cups that does mediation to deny admin actions, since the only way they can talk to a cupsd socket is through the content interface shared socket
  • classic apps wanting to print can continue to do so using the normal location of the cups socket
  • users who already have cupsd classically installed can continue to use it and manage queues and drivers and such, and it will appear to them that their snap apps printing things are connecting directly to the classic cupsd instance (when in fact they are being proxied via cupsd)
  • we have simpler interface policies in snapd, and less complicated auto-connect rules (we just have the global auto-connect for the cups content interface slot from the cups snap)
  • a snap that wants to print will be able to do so without the user granting any special privileges to it, on any distro with or without classic cupsd installed on it.

Note that this entirely depends on the cups snap being able to nicely forward/proxy requests from snaps wishing to print, which if that doesn’t work well or is not maintainable then causes the whole plan to fall apart…

Now all of that is basically a total left turn from the current plan, so maybe it doesn’t make sense to do this just because it is a lot of work to suddenly do this instead of the existing plan, but I think that at the very least we should consider following this sort of paradigm for future classic applications that we want to start mediating more finely by moving them to snaps, and the only requirements for that classic application/service are:

  • that it can peacefully co-exist with a classically installed version of itself, effectively becoming a firewall/proxy if the classic version exists on the system
  • that it can expose both the global resources that classic non-snap applications need to use/interact with it (in the case that the snap version of this software is the only one on the system), as well as very finely controlled resources that are entirely sharable via the content interface

I’m really curious if @jdstrand has time to think about this, as someone who initially worked on the current plan if there are things I am missing here.

1 Like