Skip to content

[Feature]: Add optional Landlock FS isolation to restricted-host sandbox backend #818

@dmitriikeler

Description

@dmitriikeler

Preflight Checklist

  • I have searched existing requests and this hasn't been proposed yet
  • If this request came from a chat session issue, I included relevant session context and redacted secrets

Problem statement

Operators deploying MOLTIS without a container runtime (Fly.io Firecracker VMs, bare metal without Docker/Podman, corporate environments forbidding DinD) land on restricted-host, which provides env-clearing and ulimits but zero filesystem isolation (crates/tools/src/sandbox/platform.rs:209-214). Commands like cat /data/secrets/api_key or sqlite3 /data/home/memory.db .dump run unrestricted. The container sandbox solves this via namespaces but requires a runtime that many deployments don't have.

This interacts with — and complements — the other exec-layer gaps we've reported:

Argv-level path deny (#816) is userspace static analysis. Landlock is kernel-layer VFS enforcement. Together they form real defense-in-depth for sandbox-less deployments.

Proposed solution

Extend RestrictedHostSandbox with optional Landlock FS rules applied via pre_exec. The Linux landlock crate (v0.4.4, maintained by the kernel LSM author Mickaël Salaün, MIT/Apache-2.0, ~6.7M downloads) provides the Rust API.

Config API is an open design question — happy to iterate with maintainers. Two candidate interfaces:

Option A — fs_allow_paths (matches Landlock native semantics):

[sandbox]
backend = \"restricted-host\"
fs_allow_paths = [\"/usr\", \"/bin\", \"/lib\", \"/tmp\", \"/data/home/workspace\"]
# Everything outside = denied by Landlock at VFS layer

Operator expresses positive intent. Matches Landlock's allowlist model exactly. Simpler implementation.

Option B — fs_deny_paths (matches operator intuition):

[sandbox]
fs_deny_paths = [\"/data/secrets\", \"/data/home/memory.db\", \"/etc/shadow\", \"/root/.ssh\"]

Operator expresses negative intent. Implementation constructs implicit safe-default allow set and omits the denied paths. More intuitive but requires carefully-chosen defaults.

Option C — both with rules-based composition.

Option A is implementation-simpler and matches kernel semantics. Option B matches what operators actually want to express. Preference depends on whether maintainers want to invest in the allow-set default curation. Would value your input before committing to either.

Insertion point: RestrictedHostSandbox::exec() at platform.rs:220 — build ruleset in parent, apply in child via cmd.pre_exec(move || ruleset.restrict_self()?). Standard pattern, async-signal-safe (Landlock syscalls are raw libc, no allocations or locks).

Use best_effort() mode so MOLTIS runs unchanged on kernels < 5.13; log warn! when restrictions couldn't be applied.

Alternatives considered

  • Container sandbox (Docker/Podman) — requires runtime not available on Fly.io Firecracker, bare metal installs, restricted corporate environments
  • argv-level path deny ([Feature]: tools.exec.fs_deny_paths — argv-based path deny for container-less deployments #816) — catches ~85% of direct cases but cannot defend against $(...), base64 obfuscation, variable expansion, or indirect reads. Landlock enforces at the kernel VFS layer regardless of shell tricks
  • bubblewrap/firejail — require setuid binaries not present in constrained environments; adds deployment complexity
  • seccomp path filtering — possible but requires BPF programs order-of-magnitude more complex than Landlock's declarative API
  • chroot() — classic, but requires root, can be escaped, outdated

Implementation outline

Approximately 110 LOC, ~1-2 days for a Rust engineer familiar with the codebase:

  1. Add landlock = { version = \"0.4\", optional = true } to crates/tools/Cargo.toml with cfg(target_os = \"linux\")
  2. Add FS policy fields to SandboxConfig in crates/tools/src/sandbox/types.rs (and the corresponding schema crate) with #[serde(default)]
  3. In RestrictedHostSandbox::exec() at platform.rs:220: if policy is non-empty and cfg(target_os = \"linux\"), build RulesetCreated handling all AccessFs rights, add path_beneath allow rules, apply via cmd.pre_exec
  4. Add #[cfg(target_os = \"linux\")] integration tests verifying denied paths return EACCES
  5. Handle RulesetCreated ownership in pre_exec closure — may need to rebuild ruleset from path list inside the closure if RulesetCreated isn't Send

Acceptance criteria

  • Configured deny (or allow) policy causes cat /data/secrets/foo inside restricted-host exec to fail with EACCES / exit code 1
  • Empty/default config produces identical behavior to current restricted-host (back-compat)
  • MOLTIS starts normally on kernel < 5.13 via best_effort() (no hard fail)
  • warn! emitted when Landlock restrictions could not be applied
  • Linux-only: compiles and passes all tests on macOS without the Landlock code path
  • No regression in existing restricted_host.rs tests

Category

Sandbox

How important is this to your workflow?

Medium — would be very helpful

Additional context

Fly.io kernel version verified: 6.12.47-fly (well above the 5.13 ABI v1 minimum — we'd get modern Landlock including ABI v3 TRUNCATE and ABI v4 network-port restrictions if future features want them).

Tradeoff vs container sandbox — Landlock is weaker than full namespace isolation (no network/process isolation, same PID namespace, only kernel VFS enforcement). But it's infinitely stronger than current restricted-host which has zero FS isolation, and it works where containers can't.

Honest gap — allowlist vs denylist semantics: Landlock is fundamentally allowlist — its path_beneath rules permit access. Expressing operator intent "deny /data/secrets" requires either (a) fs_allow_paths where operator enumerates the positive set, or (b) fs_deny_paths with implementation-provided safe-default allow set minus denied paths. The config-API question above is which way makes sense for MOLTIS.

Reference files for implementation:

  • crates/tools/src/sandbox/platform.rs:220 — insertion point for pre_exec
  • crates/tools/src/sandbox/types.rs:153SandboxConfig where new fields go
  • crates/tools/src/sandbox/router.rs:235-238restricted-host backend selection (no changes needed)
  • crates/tools/src/sandbox/tests/restricted_host.rs — test suite to extend
  • landlock crate on crates.io
  • Landlock kernel documentation

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions