[systemd-devel] Design patterns for privilege separating systemd services?

Kumar Kartikeya Dwivedi memxor at gmail.com
Sun Feb 21 20:14:18 UTC 2021


On Fri, Feb 19, 2021 at 03:31:10AM IST, Colin Walters wrote:
> 
> The thing I mainly like about socketpair() is that I know it's *only* accessible via the fd - it's https://en.wikipedia.org/wiki/Capability-based_security - except Linux has kind of broken that by creating /proc/N/fd anyways.  So relying on unlinking the socket seems fine, it's easy code to write and verify.
>

Hi,
I have a few ideas on how this can be extended in a way that allows for other neat features on top
(but still requires some integration with PID1). This is orthogonal to Lennart's idea,
which I very much like (and would like to see in systemd).

systemd already supports storing named file descriptors in the fd store. Currently, these are only
acquired on a restart, passed down by inheritance. To support a capability like scheme, systemd
could support pinning such fds that survive the service lifecycle. This way you have a service local
fd store keyed by the fd name. (Even not pinning would work just fine, if there are concerns
regarding resource usage). These entries for the service could also come from a .socket that systemd
keeps open.

systemd could then allow other services to inherit named fds from a service. If the named fd was
not pinned, an invalid fd (a dummy or just FOO=-1) could be supplied to the service.

AcquireFd=foobar.service:name (in a .socket?)

Such a scheme has a few benefits, but the most important of these is revocation. Suppose that you
shared one socketpair end and a few other named fds that act as a capability to do something by
sending them to systemd, effectively publishing them. (Note that the fds representing some
capability may just be dummies, e.g. O_TMPFILE).

A service requesting some privileged action may send these to you over the socketpair for
authentication. You can kcmp(2) the the two fds to establish they came from systemd (this also
allows transitive 'capability' inheritance for subprocesses of the client which may be sending the
message instead, allowing for privilege separation in the client too).

The final benefit is that you can just close your cached fd representing a capability, and then
anyone who receives the named fd pinned in systemd further will not kcmp equal to yours. To optimize
PID1's fd storage, it can receive a revocation message from the service and replace the named entry
with an invalid fd. To minimize named entries, all absent named values can be treated as one
containing an invalid fd, so systemd need not remember the revoked named entry any further, and a
service requesting one gets an invalid fd.

Revocation allows for a simple implemention of timed authorization, where you can just close your copy
after the timer fires, and a client can do nothing anymore to make the two fds compare equal.

This allows you to get something close to capabilities, and enforce server specific rights by
requesting that they be passed over whenever sending a message over a socket (so this works
with D-Bus too, afaics) that requests the server to perform a privileged operation.

Since these are all dup'd fds, they all point to the same 'open file description', which is what
allows kcmp(2) to work, but more importantly keeps the memory usage constant, as they all point to
the same struct file instance in the kernel.

You can probably draw some parallels with Bus1 here (where it used handles instead, and allowed merging
them on retrieval for equality comparison). IIRC, they also had similar ideas where permissions
needed would be declared in a manifest and systemd would pass in these bus handles to the service
fora it to be able to use them to refer to some object and pass messages back and forth.

TLDR; (Made up option names, but you'll get the idea)

# rpm-ostreed.service
FDStoreEntry=zincati-fd (from the socketpair)
FDStoreEntry=rpm-ostree-foo-op-fd (representing foo-op)
...

# zincati.service
AcquireFd=rpm-ostreed:zincati-fd
AcquireFd=rpm-ostree-foo-op-fd

You fill these in from rpm-ostreed, and zincati uses the foo-op-fd it received to request foo-op
over the socketpair (ofcourse, if you bind the right to invoke foo-op to the socketpair end, just
sharing it alone works, but I just demonstrated that you can get as granular as you want...)

-- 
Kartikeya


More information about the systemd-devel mailing list