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 — 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).

  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 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§

ProfileGapReadings
The host readings classify_profile_gap decides on.
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.
SealedShortfall
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§

InstalledProfileState
How /etc/apparmor.d/mur-sealed on this host compares to the profile this build ships.
ProfileAttachment
Which AppArmor profile, if any, the kernel reports as confining this process.
ProfileFile
Whether SEALED_APPARMOR_PROFILE_PATH is on disk, which decides whether a remedy can load it or has to install it first.
ProfileGap
Why no mur-sealed profile confines an unconfined mur: the case a refusal’s remedy is worded for.
ProfiledPath
Which shipped profile names a binary path: the three kinds of path a mur-sealed profile attaches to.
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.
UsernsGrant
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-sealed attach 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/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_APPARMOR_PROFILE_SHA256
SHA-256 (lowercase hex) of packaging/apparmor/mur-sealed as this build ships it.
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_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.

Functions§

classify_installed_profile
Classifies a read of SEALED_APPARMOR_PROFILE_PATH against the shipped digest.
classify_profile_attachment
Classifies the contents of /proc/self/attr/current into a ProfileAttachment.
classify_profile_gap
The ProfileGap the readings describe. Pure, so every case is unit-testable on any OS; inspect_profile_gap is the reader.
inspect_installed_profile
classify_installed_profile over the real SEALED_APPARMOR_PROFILE_PATH. Reads one file and needs no privilege.
inspect_profile_attachment
classify_profile_attachment over 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 as ProfileAttachment::NotReported.
inspect_profile_gap
classify_profile_gap over this host and this process. Holds no logic of its own; off Linux it is ProfileGap::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, or None when that directory cannot be read. An entry whose name cannot be read is left out.
profile_gap_remedy
The remedy an unconfined mur under a withheld grant is told: the one text E-CAP-005, E-CAP-003 for sealed and mur doctor name a ProfileGap with. version is 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 the LAST RESORT sentence: the fix for the gap’s own case first and, for a ProfileGap::PathNotProfiled with no profile loaded, the load step second. mur doctor prints them as separate lines.
profiled_path_for
Which kind of path a shipped profile names path as, or None when no shipped profile names it.