Skip to content

Kernel ring — OS sandbox rules

What it does: Applies network egress allowlist + Landlock filesystem rules + credential hiding to the agent's process; the deepest enforcement layer.

This is the layer attackers cannot bypass by patching userspace. Every other layer (gateway, MCP, codemode, harness) operates inside the agent's process; if any are compromised, the kernel is the last line of defense.

The kernel layer answers a single question: "can this process reach this address / read this file?" Even an LLM that perfectly hallucinates a forbidden tool call, or agent code that bypasses every userspace check, hits a network egress that physically cannot reach the destination.

This layer reuses TapPass's existing OpenShell sandbox (tappass/sandbox/) — Landlock for filesystem, L7 network policy via inference.local, credential hiding. What's new is the applier that takes the keyring's layer-1 slice and expresses it as OpenShell config at sandbox start (and re-applies on sync push).

See concepts/governed-agents-architecture.md §6 (defense in depth) and §9 (enforcement layers) for full context.

Inputs: the layer-1 slice of a SandboxConfig:

layer_1_kernel:
network_egress_allowlist:
- "*.tappass.ai"
- "api.tappass.ai"
fs_read_allowlist:
- "/var/run/tappass/<sandbox_id>/keyring.json"
- "/var/run/tappass/<sandbox_id>/agent-data/"
fs_write_allowlist:
- "/var/run/tappass/<sandbox_id>/agent-data/"
landlock_rules:
- {path: "/etc", access: ro}
- {path: "/proc", access: ro}
credential_hiding:
enabled: true
hidden_env_vars: [OPENAI_API_KEY, ANTHROPIC_API_KEY]

Outputs: an active sandbox namespace with the rules applied. Returns success/failure to host-runtime-cli.

Error cases:

  • Landlock unavailable (non-Linux host) → emit warning, mark this layer as unavailable in audit, continue with reduced defense
  • Network namespace creation fails → fail sandbox start; the agent process must not run with reduced kernel-layer enforcement under enforced policies
  • Apply during running sandbox (sync push) — narrow window: drain in-flight egress, swap rules atomically, resume

Non-goals:

  • Choosing what to allow/deny (that's the policy-to-sandbox-config-builder)
  • Per-call enforcement (that's the gateway/MCP layers)
  • Crash recovery if OpenShell daemon dies (that's the host runtime CLI's responsibility)

Where it lives: tappass-host/src/layers/kernel.py. Imports OpenShell primitives from tappass/sandbox/.

At sandbox start:

  1. Read layer_1_kernel from the mounted keyring file.
  2. Create a Linux network namespace; configure iptables / nftables egress rules from network_egress_allowlist.
  3. Apply Landlock filesystem rules via landlock syscall (or libsandbox wrapper).
  4. Start the credential-hiding proxy (inference.local) inside the namespace.
  5. Hand control to layer-2 applier (the agent process exec happens at the end of all layer applies).

On sync push:

  1. Re-read layer-1 slice.
  2. Diff against currently applied rules.
  3. Apply diff atomically (swap egress rule sets via netfilter; Landlock is more restrictive — may require sandbox restart for certain rule expansions, never for restrictions).

Integration points:

  • Reads keyring file via shared mount.
  • Coordinates with layer-2/3 appliers via the host runtime CLI's layer-application sequence.
  • Emits audit events: kernel_layer_applied, kernel_layer_unavailable, kernel_layer_apply_failed, egress_denied_by_kernel.
  • All acceptance_criteria pass.
  • Integration test: sandbox started with egress_allowlist: [tappass.ai]; curl https://collibra.com from inside the sandbox fails with network unreachable.
  • Integration test: sandbox started with Landlock rules; cat /etc/passwd allowed, cat /etc/shadow denied.
  • Integration test: sync push that narrows the allowlist; in-flight call to a now-denied destination fails on next attempt.
  • Audit events fire correctly for all paths (apply, fail, unavailable).
  • macOS / Windows graceful degradation tested.
  • Spec doc section in concepts/tappass-keyring-and-sync.md written.

With host-runtime-cli: the host CLI invokes our apply() function at sandbox start and on every keyring change. We expose a single entry point: apply(layer_config: dict, sandbox_id: str) -> ApplyResult.

With ring-harness, ring-interpreter: we run first in the sequence (kernel → codemode → harness → mcp → gateway). Subsequent layers depend on us having created the namespace and applied egress.

With policy-to-sandbox-config-builder: we consume layer_1_kernel from its output. The builder owns the schema; we own the applier.

Open questions:

  • (Q) Should the egress allowlist support per-tool destinations (e.g., when a tool catalog entry adds acme-internal.com to the universe)? Lean: yes; tools_destinations field added to the layer-1 config, merged with global allowlist. Owner: this component.
  • (Q) What happens if Landlock kernel version doesn't support a rule we want to apply? Lean: drop the rule with audit warning, continue with the rest. Owner: this component.
  • Choosing what to allow/deny → see policy-to-sandbox-config-builder
  • Cross-namespace isolation between sandboxes (already handled by the kernel — each sandbox is a separate namespace; we don't manage that at this layer)
  • macOS/Windows replacement for Landlock — out of scope for v1; the --layers kernel flag will be ignored on those platforms with audit warning
  • Container security (cgroup limits, seccomp profiles) → may belong here in v2; for v1, scoped to network + filesystem + credentials