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— a bind of the session’s private procfs, which the command mounted before this step (crate::proc_view), or of the host’s where none could be mounted ([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 holds to that sentence too: every command runs in its session’s PID namespace and
mounts a private procfs of it over /proc before this module runs, so the composed root’s
/proc is a bind of that mount, listing only the session’s own processes, and Landlock grants
it read and list. Only on a host whose probe could not mount a private procfs does the bind
carry the host’s /proc, where host process metadata stays visible (opens through it are
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§
- Profile
GapReadings - The host readings
classify_profile_gapdecides on. - 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. - Sealed
Shortfall - Why this host cannot back
sealed: the blocker, and, when a probe attempt failed, the call it saw fail and the readings the refusal is worded with.
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.
- Profile
File - Whether
SEALED_APPARMOR_PROFILE_PATHis on disk, which decides whether a remedy can load it or has to install it first. - Profile
Gap - Why no
mur-sealedprofile confines an unconfinedmur: the case a refusal’s remedy is worded for. - Profiled
Path - Which shipped profile names a binary path: the three kinds of path a
mur-sealedprofile attaches to. - 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.
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. - classify_
profile_ gap - The
ProfileGapthe readings describe. Pure, so every case is unit-testable on any OS;inspect_profile_gapis the reader. - 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.- inspect_
profile_ gap classify_profile_gapover this host and this process. Holds no logic of its own; off Linux it isProfileGap::Undetermined.- loaded_
apparmor_ profiles - The names of the profiles the kernel has loaded, from the world-readable
/sys/kernel/security/apparmor/policy/profiles/<entry>/name, orNonewhen that directory cannot be read. An entry whosenamecannot be read is left out. - profile_
gap_ remedy - The remedy an unconfined
murunder a withheld grant is told: the one textE-CAP-005,E-CAP-003forsealedandmur doctorname aProfileGapwith.versionis the running build’s version, which picks the release tag the profile is downloaded from; a pre-release version (one with a-) has no tag and is told to install from a checkout. - profile_
gap_ remedy_ parts profile_gap_remedy’s parts, without theLAST RESORTsentence: the fix for the gap’s own case first and, for aProfileGap::PathNotProfiledwith no profile loaded, the load step second.mur doctorprints them as separate lines.- profiled_
path_ for - Which kind of path a shipped profile names
pathas, orNonewhen no shipped profile names it.