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:
-
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 itCAP_SYS_ADMINinside that namespace and nowhere else. Nothing here needs the host’s root.On every kernel tier this now runs after
crate::network_namespacehas 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 asealedcapsule cannot reconfigure the network namespace it is confined to. -
Makes mount propagation private (
MS_REC | MS_PRIVATEon/), so nothing it does leaks back into the host’s mount table. -
Mounts a fresh
tmpfsover a base directory ([choose_root_base]) and composes the new root inside it: the host runtime directories (SEALED_RUNTIME_PATHS) and a fixed, narrow/etcset (SEALED_ETC_PATHS) bind-mounted read-only, the session workdir bind-mounted read-write at its own absolute path, a private/devtmpfs carrying the OCI default device set (SEALED_DEVICE_NODES), and a/proc— masked withhidepidwhere the kernel permits it, bound from the host where it does not ([PROC_HIDEPID_OPTIONS] documents which is which, and why). -
pivot_roots onto it, detaches the old root, remounts the new root read-only, andchdirs 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§
- Sealed
Device - One entry of the private
/devtmpfs. - Sealed
EtcPath - One entry of the fixed
/etcallowlist: the host path to bind, plus whether it is a directory.
Enums§
- Installed
Profile State - How
/etc/apparmor.d/mur-sealedon this host compares to the profile this build ships. - Profile
Attachment - Which AppArmor profile, if any, the kernel reports as confining this process.
- Sealed
Blocker - The one mechanism that stands between this host and
sealed, named specifically enough that the refusal carries a command the operator can run. - Synthetic
EtcFile - An
/etcfile a composed root gets in synthesised form instead of the host’s. - Userns
Grant - Where this host’s permission to create an unprivileged user namespace comes from.
Constants§
- SEALED_
APPARMOR_ ATTACHMENT_ PATHS - The paths the two shipped profiles in
packaging/apparmor/mur-sealedattach to, as their AppArmor path globs. - SEALED_
APPARMOR_ PROFILE_ NAME - Name of the AppArmor profile this runtime ships, as it appears in
/sys/kernel/security/apparmor/profilesand in/proc/self/attr/current. - SEALED_
APPARMOR_ PROFILE_ PATH - Where
scripts/install.shinstalls the shipped profile, and the path the refusal message tells an operator to load. - SEALED_
APPARMOR_ PROFILE_ SHA256 - SHA-256 (lowercase hex) of
packaging/apparmor/mur-sealedas this build ships it. - SEALED_
DEVICE_ NODES - The OCI runtime-spec default device set, which is what a
sealedcapsule’s/devcontains and all it contains. - SEALED_
DEVICE_ SYMLINKS - The OCI default
/devsymlinks,(link name under /dev, target). - SEALED_
ETC_ PATHS - The only
/etcentries that exist inside a composed root, each bind-mounted read-only and each silently skipped when the host does not have it. - SEALED_
ROOT_ BASE_ CANDIDATES - Base directories the new-root
tmpfsmay 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
/tmpin the composed root.
Functions§
- classify_
installed_ profile - Classifies a read of
SEALED_APPARMOR_PROFILE_PATHagainst the shipped digest. - classify_
profile_ attachment - Classifies the contents of
/proc/self/attr/currentinto aProfileAttachment. - inspect_
installed_ profile classify_installed_profileover the realSEALED_APPARMOR_PROFILE_PATH. Reads one file and needs no privilege.- inspect_
profile_ attachment classify_profile_attachmentover the real/proc/self/attr/current. Holds no logic: the file is missing on every non-Linux host and on a Linux host without AppArmor, and both read asProfileAttachment::NotReported.