Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Authorization header, auto-generated on first orchestrator start and stored in the platform data dir token file (PAM_TOKEN overrides it). The CLI reads and injects it automatically. Authenticates the public API surface: users and the pam CLI (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):

  1. Never run kill-server on the default socket — the user’s personal tmux must never be affected; catastrophic cleanup uses tmux -L pam kill-server only, all normal operations use session-specific commands.
  2. Validate session names before any tmux invocation — regex ^pam-[0-9a-f]{8}$, applied to every -t argument and to session-list parsing, prevents command injection via user input.
  3. Clean up only sessions pam created — two-part check (the session exists in the runs table registry AND its name matches the pam- prefix); orphan cleanup still respects the prefix constraint and never targets a user session, even one that somehow ended up on the pam socket.
  4. Restrict send-keys to 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 programmatic send-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:

  1. The server generates a new token and stores its fingerprint on hosts.token_fingerprint, replacing the previous fingerprint.
  2. The new token is printed once, the same one-time-display contract as pam host add. Copy it into the worker host’s worker-token file (or PAM_WORKER_TOKEN) immediately — it is not retrievable again.
  3. 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.