Skip to main content

Module sealed

Module sealed 

Source
Expand description

The sealed containment mechanism: a private mount namespace pivoted onto a composed root.

scoped (Landlock + seccomp, see [crate::sandbox]) can deliver “nothing outside the workdir is readable or writable”. It can never deliver “nothing outside the workdir exists”, because Landlock is an access filter — it does not change what a process can see, name or traverse. A mount namespace can, and that is the whole of what this module adds.

§What the mechanism is

Inside the forked child’s pre_exec window — before fd hygiene, before the capability drop, before the seccomp filter, and before the Landlock ruleset — the child:

  1. unshare(CLONE_NEWUSER | CLONE_NEWNS), then maps its own uid/gid one-to-one into the new user namespace (/proc/self/uid_map), which is what gives it CAP_SYS_ADMIN inside that namespace and nowhere else. Nothing here needs the host’s root.

    On every kernel tier this now runs after crate::network_namespace has already created a user+network namespace, so the one built here is nested inside it. That is load-bearing rather than incidental: a task in a descendant user namespace holds no capability in the ancestor, so a sealed capsule cannot reconfigure the network namespace it is confined to.

  2. Makes mount propagation private (MS_REC | MS_PRIVATE on /), so nothing it does leaks back into the host’s mount table.

  3. Mounts a fresh tmpfs over a base directory ([choose_root_base]) and composes the new root inside it: the host runtime directories (SEALED_RUNTIME_PATHS) and a fixed, narrow /etc set (SEALED_ETC_PATHS) bind-mounted read-only, the session workdir bind-mounted read-write at its own absolute path, a private /dev tmpfs carrying the OCI default device set (SEALED_DEVICE_NODES), and a /proc — masked with hidepid where the kernel permits it, bound from the host where it does not ([PROC_HIDEPID_OPTIONS] documents which is which, and why).

  4. pivot_roots onto it, detaches the old root, remounts the new root read-only, and chdirs back into the workdir.

Everything outside that root is then not merely access-denied — it is absent. There is no pathname for /etc/shadow, for a Docker socket, or for /dev/sda, so the escape classes that scoped has to enumerate and refuse (pathname unix sockets, device nodes, metadata visibility, stdlib directory enumeration) stop being reachable rather than being individually denied.

/proc is the one exception to that sentence: mounting a private procfs unprivileged needs a PID namespace this module deliberately does not create, so on a bare host the composed root carries a bind of the host’s /proc and host process metadata stays visible there (opens through it are still Landlock’s to refuse). See [PROC_HIDEPID_OPTIONS].

§What it does not replace

Landlock and seccomp still install afterwards, now scoped to the private root. sealed is defence in depth over scoped’s mechanism, not a substitute for it: the syscall allowlist is still the only thing standing between a capsule and io_uring, bpf or ptrace, none of which a mount namespace has an opinion about.

§Why the plan is precomputed

Everything the child does is driven by [SealedRootSpec], a fully-resolved list of NUL-terminated paths built in the parent, before fork(). The pre_exec window permits only async-signal-safe work: allocating there (a format!, a PathBuf::join) can deadlock on the allocator lock a different thread of the parent held at fork time. Splitting the decision ([plan_composed_root], pure and unit-testable against a fake host layout) from the execution ([construct_composed_root], raw syscalls over already-built CStrings) is what keeps the child side allocation-free on its success path — and it is what makes the composed-root layout testable at all on a host that cannot create a namespace.

Structs§

SealedDevice
One entry of the private /dev tmpfs.
SealedEtcPath
One entry of the fixed /etc allowlist: the host path to bind, plus whether it is a directory.

Enums§

SealedBlocker
The one mechanism that stands between this host and sealed, named specifically enough that the refusal carries a command the operator can run.
SyntheticEtcFile
An /etc file a composed root gets in synthesised form instead of the host’s.

Constants§

SEALED_APPARMOR_PROFILE_NAME
Name of the AppArmor profile this runtime ships, as it appears in /sys/kernel/security/apparmor/profiles and in /proc/self/attr/current.
SEALED_APPARMOR_PROFILE_PATH
Where scripts/install.sh installs the shipped profile, and the path the refusal message tells an operator to load.
SEALED_DEVICE_NODES
The OCI runtime-spec default device set, which is what a sealed capsule’s /dev contains and all it contains.
SEALED_DEVICE_SYMLINKS
The OCI default /dev symlinks, (link name under /dev, target).
SEALED_ETC_PATHS
The only /etc entries that exist inside a composed root, each bind-mounted read-only and each silently skipped when the host does not have it.
SEALED_ETC_STAGING_DIR_NAME
Directory created inside the session workdir holding the synthetic /etc files, each of which the composed root binds read-only over its SEALED_ETC_PATHS target.
SEALED_ROOT_BASE_CANDIDATES
Base directories the new-root tmpfs may be mounted over, most preferred first.
SEALED_RUNTIME_PATHS
Host directories bind-mounted read-only into every composed root, in order.
SEALED_TMP_DIR_NAME
Directory created inside the session workdir and bind-mounted at /tmp in the composed root.