TacidOS
Developers
Documentation

Decision 0011

bubblewrap/Landlock/seccomp, microVMs, and a kernel-enforced egress proxy

Architecture decision record 0011: bubblewrap/Landlock/seccomp, microVMs, and a kernel-enforced egress proxy

Status
Accepted
Date
2026-09-03

Context

Tools spawn processes and parse hostile content. Network policy enforced inside an application is not enforcement.

The three plans

  • Grok: bubblewrap or Landlock + seccomp for tool processes; net kill via rfkill + nftables with a badge.
  • Fable: systemd hardening plus bubblewrap; a per-domain egress allowlist in policy.
  • Codex: a sandbox executor zone; microVMs for risky code; prompt-injection boundaries.

Decision

Spawned tools run under bubblewrap + Landlock + seccomp with scoped mounts, limits and no network unless policy grants it. Every daemon carries systemd hardening. High-risk code runs in a microVM (microvm.nix). Parsers of untrusted content (PDF, archives, images, web) run in session sandboxes, never in tacidd. Network policy is enforced in the kernel rather than in the process being confined, and tacid-egress enforces the allowlist, injects credentials, writes the ledger and implements one-command airgap with a badge (Fable’s allowlist made real, after review).

The kernel enforcement as built is not the nftables rules this record describes, and is stronger than them. RestrictAddressFamilies = [ "AF_UNIX" ] on every Tacid unit but the proxy is a seccomp filter on socket(2): an agent process does not get an IP socket it may not use, it does not get the socket. IPAddressDeny=any — a cgroup eBPF program — stays beside it as a second mechanism.

The skuid rules were never built, and building them turned out to be the wrong thing: tacid-egress is the only crate in this tree that opens a TCP connection, so what was described as narrowing the daemon’s route to the network was really the discovery that it needs no route at all. IPAddressAllow=localhost was the hole — every local port, and so any other listener as a relay — and it is closed by removal rather than by a filter. The stem’s nftables.profile operation went with it. See ADR-0084, which also records why a rule matching a session by cgroup could not have been checked before it shipped.

Consequences

  • One more process; one fewer lie.
  • Cloud API keys are never in tacidd’s memory.
  • Airgap is a reflex, not a setting.

Alternatives rejected

  • Allowlist enforced by the HTTP client: bypassed by any tool with a socket.
  • Containers as the sandbox: heavier, and not a security boundary without the same primitives.