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§

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.
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_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.

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