Security
Status: proposed — consolidated 2026-09-01 from material already designed in 030-http-api.md, 010-tmux-executor.md, 020-provider-adapters.md, 002-data-model.md, and 050-ui-dashboard.md. No new design decisions were made here, except where flagged as a new Open Question below.
Scope
Threat model and controls for a multi-host, single-operator platform (ADR-012): transport security (TLS), token classes (user bearer vs worker token), token lifecycle and fingerprint storage, WS query-token exposure, tmux safety boundaries, secret handling in provider env injection, and PostgreSQL credential confinement to the orchestrator.
Transport Security
Served via TLS in deployment; termination strategy (reverse-proxy vs
native) is a deployment choice, not fixed here — every token model below
assumes TLS protects the transport (030-http-api.md
§ Bind Configuration). The bind address defaults to 0.0.0.0 (network-
exposed by design, ADR-012), so TLS is load-bearing, not optional
hardening: without it, both the bearer token and the worker token travel
in the clear over a listener other hosts can reach.
Token Classes
Two separate credential classes authenticate against the central orchestrator, never interchangeable:
- User bearer token — a 32-character hex string in the
Authorizationheader, auto-generated on first orchestrator start and stored in the platform data dirtokenfile (PAM_TOKENoverrides it). The CLI reads and injects it automatically. Authenticates the public API surface: users and thepamCLI (030-http-api.md § Authentication Scheme). - Worker token — minted by the operator (
pam host add), never self-registered by a worker. Authenticates the internal/api/internal/worker/*surface (registration, heartbeat, signal ingest) and, on the Attach WebSocket, is passed as a query parameter because browser WebSocket clients cannot set headers — see § WS Query-Token Exposure below. Lifecycle detail (rotation, clone detection) is § Token Lifecycle below.
There is no user account model — a single bearer token stands in for “the operator”, consistent with the single-operator scope in ADR-012.
WS Query-Token Exposure
The Attach WebSocket authenticates with ?token= in the URL, because a
browser WebSocket client cannot set an Authorization header on the
handshake (030-http-api.md § Live Pane Streaming). This
means the token appears in URLs — proxy access logs, browser history, and
any tool that logs full request URLs. Avoid logging full WebSocket URLs
in application logs. The worker-facing uplink connection is not subject
to this constraint: a worker is not a browser and authenticates with a
normal Authorization bearer header on the handshake instead
(030-http-api.md).
tmux Safety Boundary
The tmux executor runs on a dedicated socket (-L pam), never the user’s
default tmux socket, and is bound by four safety rules
(010-tmux-executor.md § Safety Rules):
- Never run
kill-serveron the default socket — the user’s personal tmux must never be affected; catastrophic cleanup usestmux -L pam kill-serveronly, all normal operations use session-specific commands. - Validate session names before any tmux invocation — regex
^pam-[0-9a-f]{8}$, applied to every-targument and to session-list parsing, prevents command injection via user input. - Clean up only sessions pam created — two-part check (the session
exists in the
runstable registry AND its name matches thepam-prefix); orphan cleanup still respects the prefix constraint and never targets a user session, even one that somehow ended up on thepamsocket. - Restrict
send-keysto an allowlisted key set — programmatic input is limited to control sequences (Ctrl+C,Ctrl+D), Enter/Backspace/ Delete, and printable ASCII; shell metacharacters (|,&,;,$, backticks), terminal-reconfiguring escape sequences, and tmux prefix sequences (Ctrl+B) are rejected. This restriction applies only to programmaticsend-keys— a human holding the attach lease via the PTY relay has unrestricted keyboard input, by design.
Explicit NEVER list (from the same source): never run kill-server
without -L pam; never use glob patterns in session targeting
(kill-session -t "pam-*"); never target a session from an unvalidated
user-provided path or workdir; never pass unvalidated input to any -t
flag; never send arbitrary shell commands via send-keys; never parse
pane output as structured completion data; never access sessions on the
default socket.
Secret Handling in Provider Env Injection
Provider adapters substitute {api_key} into a provider CLI’s argv
template or environment at spawn time — described as “API key from
environment or secure storage”
(020-provider-adapters.md § Capability
Declaration). Neither the concrete “secure storage” mechanism nor a log-
redaction policy for the substituted value is defined anywhere in the
current docs set. This is a genuine gap, not a documented default —
tracked as OQ-17 below rather than assumed.
PostgreSQL Credential Confinement
Database credentials live on exactly one host: workers never connect to
PostgreSQL directly, and all writes flow through the central API/uplink
(002-data-model.md § Concurrency Model). The
connection DSN is configured once, in the orchestrator’s config.toml
[database] section (060-configuration.md); no
worker-side config key for it exists.
Shell Least-Privilege (Tauri Desktop Shell)
The desktop shell is a monitoring-only thin client — never an execution
node — so it needs no local filesystem or shell capabilities. Tauri v2
capabilities ship with everything disabled: no invoke, no
Electron-style ipcRenderer/remote equivalents used from SPA code
(050-ui-dashboard.md § Desktop Shell). All user
actions are forwarded as API requests only; the orchestrator and its
workers perform all actual work.
Token Lifecycle (OQ-10, OQ-14 — confirmed 2026-09-01)
Worker auth model (OQ-10)
Confirmed default: a pre-provisioned worker token over TLS, not mTLS.
pam host add mints the token and prints it once; the worker stores it in
PAM_WORKER_TOKEN or its local worker-token file and presents it on every
registration and heartbeat (030-http-api.md,
040-cli.md). mTLS was rejected as the default because it does
not solve the problem OQ-14 actually addresses: a cloned mTLS client
certificate aliases a host just as effectively as a cloned bearer token — the
two concerns (transport authentication vs. detecting two live processes
presenting the same credential) are orthogonal, and mTLS adds certificate
provisioning and rotation overhead without closing the OQ-14 gap.
Token rotation procedure (OQ-10)
Run pam host rotate-token <name> (040-cli.md) to mint a fresh
token for an already-registered host:
- The server generates a new token and stores its fingerprint on
hosts.token_fingerprint, replacing the previous fingerprint. - The new token is printed once, the same one-time-display contract as
pam host add. Copy it into the worker host’sworker-tokenfile (orPAM_WORKER_TOKEN) immediately — it is not retrievable again. - The old token keeps authenticating until the worker process actually restarts with the new one — there is no server-side grace-period timer and no forced cutover — so a worker left un-restarted after rotation simply keeps using its old (still-valid) token. Restart the worker promptly after rotating to complete the swap.
Rotate a host’s token when: a token may have leaked (compromised host,
committed secret, exposed log), as routine credential hygiene, or when
retiring a host (rotate, then pam host disable, so a copy of the old token
elsewhere stops working).
Instance-suspect clearing (OQ-14)
pam host clear-suspect <name> (040-cli.md) is the operator
side of the OQ-14 clone-detection algorithm documented in
002-data-model.md: it clears hosts.instance_suspect
and hosts.instance_flagged_at, and accepts whichever instance_id that
host most recently presented as the new pinned value. Because clearing the
flag also re-pins trust, only run it after confirming out of band (checking
which machine is actually supposed to be running that worker token) which
of two live presenters is legitimate — clearing the flag on the wrong
presenter re-admits the clone to dispatch.
Open Questions
- OQ-17: what is the concrete “secure storage” mechanism for provider API
keys (
{api_key}substitution), and is there a log-redaction policy for the substituted value? Registered in 071-risks-and-open-questions.md.