ADR-0002: Linux users over Docker for per-chat isolation

Status: Accepted (provisional — revisit after D2 isolation report) Date: 2026-04-23 Deciders: Jacob

Context

Each chat (1:1 or group) needs an isolated workspace so compromise of chat A cannot read chat B's files, exfiltrate secrets, or influence chat B's Claude Code sessions. Cortex uses Docker containers per workspace. picortex explicitly rejects that model. Alternatives:

Option Spin-up cost Memory per idle Security Complexity
Docker container per chat seconds + image pull 100-500 MB strong high (daemon, network, volumes)
Linux user + home dir + POSIX perms milliseconds ~ a few MB (tmux+bash) moderate low
firejail / bubblewrap namespace milliseconds ~ a few MB strong medium (seccomp policy tuning)
nsjail milliseconds ~ a few MB strong medium
Landlock (6.1+ kernel) ~ nothing ~ a few MB strong medium (new-ish API)
VM per chat (microVM, Firecracker) 100s ms 128+ MB very strong high

Jacob's context:

  • Single user (Jacob). Threat model is "sloppy group-chat prompt injection" not "adversarial tenant."
  • Small machine. Target is a 4 GB VPS or Mac Mini.
  • Iteration speed matters more than defense-in-depth at v0.1.
  • Prior research in ~/memory/research/openclaw-group-chat-security.md and openclaw-security-audit-2026-02-20.md documents the prompt-injection threat model in depth.

Decision

v1: per-chat Unix user + home dir with POSIX permissions (0700) + runuser/sudo -u.

This will be hardened in D2 by potentially wrapping the tmux session entry point in bubblewrap or Landlock — those are cheap composable additions to the user-based base, not replacements for it.

Consequences

Positive

  • Sub-second chat provisioning (useradd, mkdir, chown, tmux new-session).
  • Idle cost: one tmux + one bash ≈ a few MB RAM.
  • No Docker daemon, no image registry, no volume orchestration.
  • POSIX is boring and well-understood; less novel surface area.
  • Trivially composes with bubblewrap / Landlock later without re-architecting.

Negative

  • Shared kernel. A kernel-level exploit affects everyone.
  • Shared /tmp, shared networking by default. Needs mitigation.
  • No easy "rm -rf the container" if a chat goes bad — teardown means userdel -r + cleanup.
  • Cap'ed by useradd uid range on some systems; unlikely to matter at Jacob's scale.

Mitigations

  • /tmp per user: set pam_namespace so /tmp is per-user-isolated.
  • Network: Claude Code processes run without NET_BIND_SERVICE and behind a firewall that blocks outbound except api.anthropic.com and a known allowlist. Egress filter via iptables owner-match.
  • FS allowlist: chat user can't write outside its home. Enforced by owner-only permissions on the home dir and read-only bind mounts for shared tools (/usr/local/bin/picortex-shims).
  • Resource limits: cgroups v2 per chat user — CPU share, memory cap (256 MB default), pids.max.
  • Seccomp / Landlock: added in D2 if threat model changes.
  • Teardown path: userdel -rf chat-<hex> + remove sudoers drop-in. Scripted in scripts/destroy-chat.sh.

Trigger for revisit

  • D2 report finds Linux-user perms insufficient for the threat model.
  • First isolation incident.
  • Jacob invites a second user to picortex.
  • picortex opens to multi-tenant.

Alternatives considered in detail

  • Docker containers per chat: rejected (see top of file). Would add ~50 MB image-cache baseline per chat and >1 s spin-up; operationally heavy for a personal tool.
  • OpenClaw sandbox: rejected per explicit user directive ("no OpenClaw"). Also: sandbox model is whole-agent, not per-chat.
  • Firejail: considered; keep as a possible D2 addition. Its default profiles are friendlier than bubblewrap for interactive shells.
  • bubblewrap (bwrap): the strongest "cheap" hardening option; composes well with Linux users. Primary D2 candidate.
  • nsjail: more flexible than bwrap but more config; hold for D2.
  • Microsandbox / gVisor / Firecracker: overkill for v1.

References

  • ~/memory/research/openclaw-group-chat-security.md
  • ~/memory/research/openclaw-security-audit-2026-02-20.md
  • Cortex R2.1 — "each chat = own filesystem" invariant
[[curator]]
I'm the Curator. I can help you navigate, organize, and curate this wiki. What would you like to do?