Files
systemd/man/systemd-nsresourced.service.xml
T
Daan De Meyer e50ec930c2 nsresourced: refuse writable overlayfs whose upper outlives the namespace
A write through an overlay mount does not create the object on the overlay
itself: overlayfs creates it on the upper layer via vfs_create(), which
reaches only the *inode* LSM hooks, not the *path* hooks the transient
range policy attaches to. The one path hook that does fire sees the overlay
mount, whose superblock belongs to the (managed) mounting namespace and so
looks like it dies with it -- while the upper may be a persistent file
system on which the transient IDs then persist.

  Regular file system -- caught by the path hook:

    openat(dir_on_persistent_fs, "f", O_CREAT)
      path_openat -> ... -> security_path_mknod(path{mnt = persistent mount})   << path hook (attached)
        userns_restrict_path_mknod -> validate_mount()
          sb->s_user_ns = init   -> not below managed   (outlives the ns)
          mnt_idmap does not remap the transient id      (reaches disk)
        => -EPERM                                        BLOCKED

  Overlay write -- slips past the path hook (this is the hole):

    openat(overlay_mnt/"f", O_CREAT)
      path_openat -> ... -> security_path_mknod(path{mnt = OVERLAY mount})       << path hook (attached)
        validate_mount(): overlay sb->s_user_ns == managed
        => "dies with the namespace"                     ALLOW
      ovl_create_or_link -> vfs_create(UPPER dentry)
        security_inode_create(UPPER inode)               << inode hook (NOT attached)
        => no check
      inode created on the UPPER fs, owned by the transient uid   PERSISTS

Judge the upper at mount time instead, in lsm/sb_kern_mount:

    mount("overlay", ..., upperdir=<X>, workdir=<Y>)
      do_new_mount -> vfs_get_tree
        ovl_fill_super
          stamps work/index onto the upper via vfs_mkdir, owned by the
          transient mounter -> security_inode_mkdir only (not a path hook)
        security_sb_kern_mount(sb)                        << hook (attached)
          userns_restrict_sb_kern_mount:
            sb->s_magic != OVERLAYFS_SUPER_MAGIC       -> return 0   (not an overlay)
            find_managed_userns(sb->s_user_ns) == none -> return 0   (unmanaged mounter)
            upper = ((struct ovl_fs*)sb->s_fs_info)->layers[0].mnt
            upper == NULL                              -> return 0   (read-only, no upper)
            fs_userns = upper->mnt_sb->s_user_ns
              upper dies with the managed ns (e.g. a tmpfs)         => return 0  ALLOW
              upper persistent, idmap records no transient id       => return 0  ALLOW
              upper persistent, an idmap extent records a transient => -EPERM   REFUSE

The upper has to be judged at mount time because overlayfs performs the
actual upper creations (both the mount-time work/index dirs and every later
copy-up) with internal vfs_create()/vfs_mkdir() calls that reach only the
inode hooks, on the upper's own superblock, where neither the overlay's
ovl_fs nor the mount idmapping is available. Refusing the mount stops every
such write. It cannot undo the work/index dirs ovl_fill_super() already
stamped onto the upper before this hook runs. Reaching those takes an
upperdir and a workdir that already exist on the same persistent,
non-idmapped file system and that the transient IDs may write to: the client
cannot create them itself, since that is what the path hooks deny.

The idmap check walks the upper mount's uid_map and gid_map and permits the
overlay only when no extent records a transient id on disk (ids the idmap
does not map fail the write, so they are fine too). This is what lets a
client back a writable overlay with a persistent but id-mapped upper, e.g.
one handed out by mountfsd, while a plain persistent upper stays refused.

The check keys on whether a writable upper exists (layers[0].mnt is
non-NULL), which is fixed for the superblock's lifetime, not on the mutable
SB_RDONLY flag: an overlay mounted read-only but with an upper can be
remounted read-write later without re-entering sb_kern_mount (remount uses a
different LSM hook), so it must be judged now. A genuinely read-only overlay
has no upper and is left alone. Reading the upper layer needs overlayfs'
private struct ovl_fs/ovl_layer, which are absent from vmlinux.h when
overlayfs is built as a module; they are declared as CO-RE type flavors and
guarded by bpf_core_type_exists(), and the service now orders itself after
modprobe@overlay.service so the module's BTF is present by the time the
program is loaded.

A managed (nsresourced-provisioned) user namespace can therefore only mount
a writable overlay whose upper dies with it (e.g. a tmpfs it set up
itself). Callers that need a writable, persistent (non-tmpfs) upperdir --
mkosi, for one -- should use a regular unprivileged user namespace for that
rather than a transient nsresourced range, since such an overlay is refused
for a managed namespace by design.

test-userns-restrict now mounts real overlays: a read-only overlay, a
writable overlay with a tmpfs upper, and a writable overlay whose upper is
an init-owned but idmapped mount are all allowed, while a writable overlay
with a plain init-owned upper is refused with EPERM. The kernel permits that
last mount without the LSM loaded, so the EPERM is the policy's doing.
2026-08-12 10:49:34 +02:00

109 lines
6.9 KiB
XML

<?xml version='1.0'?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="systemd-nsresourced.service" conditional='ENABLE_NSRESOURCED'>
<refentryinfo>
<title>systemd-nsresourced.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-nsresourced.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-nsresourced.service</refname>
<refname>systemd-nsresourced</refname>
<refpurpose>User Namespace Resource Delegation Service</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-nsresourced.service</filename></para>
<para><filename>/usr/lib/systemd/systemd-nsresourced</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>systemd-nsresourced</command> is a system service that permits transient delegation of a
UID/GID range to a user namespace (see <citerefentry
project='man-pages'><refentrytitle>user_namespaces</refentrytitle><manvolnum>7</manvolnum></citerefentry>)
allocated by a client, via a Varlink IPC API.</para>
<para>Unprivileged clients may allocate a user namespace, and then request a UID/GID range to be assigned
to it via this service. The user namespace may then be used to run containers and other sandboxes, and/or
apply it to an id-mapped mount.</para>
<para>Allocations of UIDs/GIDs this way are transient: when a user namespace goes away, its UID/GID range
is returned to the pool of available ranges. In order to ensure that clients cannot gain persistency in
their transient UID/GID range a BPF-LSM based policy is enforced that refuses to create inodes — or
change ownership of them — that would end up owned by a transient UID/GID on a file system outliving the
user namespace the range was assigned to. Writes to file systems the user namespace allocated itself are
hence permitted, as those are torn down together with it, and so are writes through id-mapped mounts that
translate the transient range into something else before it reaches the disk. As a stacking file system
such as <literal>overlayfs</literal> writes to a separate upper layer that would not be covered by the
checks on the overlay mount itself, mounting a writable <literal>overlayfs</literal> instance is refused
for these user namespaces unless its upper layer, too, is torn down together with the user namespace, or
is an id-mapped mount that translates the transient range away before it reaches the disk.</para>
<para><command>systemd-nsresourced</command> automatically ensures that any registered UID ranges show up
in the system's NSS database via the <ulink url="https://systemd.io/USER_GROUP_API">User/Group Record
Lookup API via Varlink</ulink>.</para>
<para>Currently, only UID/GID ranges consisting of either exactly 1 or exactly 65536 UIDs/GIDs can be
registered with this service. Moreover, UIDs and GIDs are always allocated together, and
symmetrically.</para>
<para>The allocation API supports <emphasis>delegated ranges</emphasis>: additional UID/GID ranges that
are mapped 1:1 into the user namespace rather than being translated to a target UID/GID. These delegated
ranges enable nested user namespace scenarios where a container needs to create child user namespaces
with their own transient UID ranges. Normally, the kernel restricts which UIDs can be mapped into a user
namespace to those that are also mapped in the parent. Delegated ranges solve this by pre-allocating
additional ranges that are visible inside the user namespace and can be used by nested
<function>AllocateUserRange()</function> calls. Up to 16 delegated ranges can be requested per user
namespace, each of size 65536. The ranges are allocated from the container UID ranges as per
<ulink url="https://systemd.io/UIDS-GIDS">Users, Groups, UIDs and GIDs on systemd Systems</ulink>.</para>
<para>The allocation API also supports <emphasis>identity mappings</emphasis>: instead of allocating a
transient UID/GID range, the user namespace can be configured to map the caller's UID/GID to root (UID
0) inside the namespace, or to itself. Identity mappings can be combined with delegated ranges to enter
a privileged user namespace from which the container can be set up after which the container can run in
one of the delegated ranges. Identity mapped users are not subject to BPF-LSM write restrictions unlike
the transient ranges.</para>
<para>Additionally, the allocation API supports mapping the <emphasis>foreign UID range</emphasis> into
the user namespace. When this option is enabled, the foreign UID range is mapped 1:1 into the user
namespace, allowing processes inside to access and manipulate files owned by the foreign UID range.</para>
<para>The service provides API calls to delegate mounts (referenced via their mount file descriptors as
per Linux <function>fsmount()</function> API), to pass ownership of a cgroup subtree to the user
namespace and to delegate a virtual Ethernet device pair to the user namespace. When used in combination
this is sufficient to implement fully unprivileged container environments, as implemented by
<citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>, fully
unprivileged <varname>RootImage=</varname> (see
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>) or
fully unprivileged disk image tools such as
<citerefentry><refentrytitle>systemd-dissect</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
<para>This service provides one <ulink url="https://varlink.org/">Varlink</ulink> service:
<constant>io.systemd.NamespaceResource</constant> allows registering user namespaces, and assign mounts,
cgroups and network interfaces to it.</para>
</refsect1>
<refsect1>
<title>See Also</title>
<para><simplelist type="inline">
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-mountfsd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-dissect</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry project='man-pages'><refentrytitle>user_namespaces</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
</simplelist></para>
</refsect1>
</refentry>