Threat model¶
Status: draft M0, as of milestone M0 (planning), terminal addendum of 2026-10-10; as of milestone M2 (terminal across several api nodes, M2-47, 2026-10-20). Living document; maintenance rule in section 9.
Controls with IDs SEC-...: ../spec/19-security.md. Permissions and
guard rules SR1 to SR12: ../spec/07-permissions.md. Identity:
../spec/08-identity.md. Privacy:
../spec/20-privacy.md. Dev isolation:
../spec/21-dev-isolation.md. Terminal:
../spec/24-terminal.md.
Method: system boundaries and data flows, assets, attackers, STRIDE per component, then a register of the most important threats with rating, mitigation, test and residual risk. AI-specific threats follow the classification of the OWASP Top 10 for LLM Applications 2025 (https://genai.owasp.org/llm-top-10/, retrieved 2026-10-10 [verified]).
1. Scope¶
In the model: server (all roles), data storage, native apps, admin console, agent protocol and reference adapters, apps and webhooks, push, telemetry, backups, supply chain, multi-user dev environment, terminal (pairing, relay, host service, terminal view of the clients).
Not in the model, with reason:
- Compromised server host or hypervisor: assumption in 19, section 1. The remedy is host hardening in operations.
- Physical attacks on server hardware, side channels in CPUs.
- Internal misuse at LLM, push or telemetry providers beyond the contracts: governed legally through data processing agreements (../compliance/processors-template.md).
- The inner behavior of agent runtimes (for example what Hermes Agent keeps in its memory): the product only limits what the runtime reaches and triggers.
2. System boundaries¶
Source: diagrams/trust-boundaries.mmd. Re-render with mermaid-cli
11 (npx -y @mermaid-js/mermaid-cli@11 -i trust-boundaries.mmd -o trust-boundaries.svg).
| Boundary | Between | What it protects | Controls |
|---|---|---|---|
| TB1 | Device and network edge | Tokens, content in transit, identity | TLS, DPoP, console cookie, SEC-CORS-* |
| TB2 | Network edge and server process | Authenticity of proxy headers, plaintext only internally | SEC-TLS-1, server.trusted_proxies, 08 section 2.4 |
| TB3 | Server and data storage | Content, hashes, audit integrity | Roles in PostgreSQL, Unix socket, verify-full, ADR 0021 |
| TB4 | Server and agent adapter | who triggers which agent, what the agent sees | API token with DPoP, invocation token (I3), authorize at delivery, SR9 |
| TB5 | Runtime and LLM provider or tools | Content to third parties, effect of tools | Tool class (SR2), backend policy (SR1), data processing agreement |
| TB6 | Server and apps or external systems | Data to apps, writes from outside | Scopes plus grants, signed webhooks to allowed targets, incoming webhooks without triggering |
| TB7 | Server and external services | Identity, push metadata, telemetry, object bytes to an S3 service | Egress client platform/egress for every outgoing HTTP connection except the OTLP export to the collector (target allow list, IP check at connection setup, TLS always verified, lint against other clients; SEC-NET-1 to 5, SEC-TLS-4), OIDC check, push only with IDs, telemetry without content. S3 driver (M3-02): object bytes go to the S3 service only through the egress client (purpose objectstore, host policy of exactly the endpoint, http or private addresses only with an allow_private entry, no redirects), with access keys only from the secret provider (file.s3.access_key_ref, file.s3.secret_key_ref), never from the environment or the instance metadata service |
| TB8 | Operations and supply chain and instance | Root of trust, code integrity | Server command only on the host, signed releases, pinned actions |
| TB9 | Dev environment of a person and host of the runner | Host, secrets of the owner, other people | VM boundary, egress rules, readiness check (21) |
| TB10 | Device, server relay and host service of a connected host | Terminal content, shell on the host, LLM login of the host | Noise NK end-to-end, one-time token, fresh step-up, SR10, I6, host service without admin (24) |
3. Assets¶
| ID | Asset | Protection goal |
|---|---|---|
| A1 | Messages, threads, runs, agent outputs | Confidentiality, integrity |
| A2 | Files, transcripts | Confidentiality, integrity |
| A3 | Identities, sessions, device keys, tokens of people | Confidentiality, authenticity |
| A4 | Private signing keys, pepper | Confidentiality |
| A5 | Agent and app tokens, URLs of the incoming webhooks | Confidentiality |
| A6 | Permission configuration (grants, groups, owners, operators, backend policy, tool class) | Integrity |
| A7 | Audit log | Integrity, completeness |
| A8 | LLM access (API keys, personal subscriptions), outside the server | Confidentiality, contract compliance |
| A9 | Hosts of the agent runtimes with their secrets | Integrity, confidentiality |
| A10 | Backups | Confidentiality |
| A11 | Push credentials of the owner | Confidentiality |
| A12 | Metadata (who, when, with whom, devices, IPs) | Confidentiality, data minimization |
| A13 | Availability of chat and agents | Availability |
| A14 | Code, dependencies, releases | Integrity |
| A15 | Shell on connected hosts and their terminal content | Confidentiality, integrity |
| A16 | Keys of the host services (Noise, login) and pinned keys in the devices | Confidentiality, authenticity |
4. Attackers¶
| ID | Attacker | Abilities | Goal |
|---|---|---|---|
| E1 | Outsider without an account | reaches the network edge | takeover, data, disruption |
| E2 | Member with few permissions | valid account, own devices | read foreign spaces, misuse agents |
| E3 | Content attacker | brings text, files or links into a channel (directly, through an import, a webhook or a person who forwards something) | prompt injection against agents |
| E4 | Malicious or compromised agent runtime | valid agent token, sees what the agent sees | exfiltrate data, write into foreign channels |
| E5 | Malicious app | valid installation with scopes | exfiltrate data, deceive people |
| E6 | Thief or finder of a device | device, perhaps unlocked | content, session |
| E7 | Owner with operating error or abuse | all owner permissions | overly broad grants; insight into other people's content |
| E8 | Supply chain attacker | manipulated dependency, action, image | code in the server, in CI or in apps |
| E9 | Network attacker | reads or modifies traffic | tokens, content |
| E10 | Service provider with insight | sees push, telemetry or LLM data | profiling |
| E11 | Thief of a backup | has a backup file | content, hashes |
| E12 | Malicious instance operator of the server | controls the server, key distribution and relay | read along terminal sessions or open them with nobody at the controls |
| E13 | Attacker on a connected host | account on the host, without the permissions of the host service | gain the permissions of the host service, steal keys |
5. STRIDE per component¶
S = Spoofing, T = Tampering, R = Repudiation, I = Information Disclosure, D = Denial of Service, E = Elevation of Privilege. "Register" refers to section 6.
5.1 Native app¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| S | Stolen token used on another device | DPoP with a non-exportable key (08, 7.3) | T-TK-01 |
| T | Manipulated app bypasses client checks | no permission logic in the client; the server checks everything (07) | T-AZ-01 |
| R | Person denies a grant | step-up with biometrics, audit with device and session | T-AZ-04 |
| I | Local cache on a lost device | encrypted DB, app lock, wipe on 4012 (08, 9 and 10) |
T-DV-01 |
| D | Mass reconnects after a restart | full jitter backoff (04, 6.5), rl.gateway_connect |
T-DS-01 |
| E | Pending device gains access | tokens only for self-information (403 device_pending for everything else, no gateway), typed-in confirmation code (08, 5.2) |
T-ID-05 |
5.2 Admin console in the browser¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| S | Phishing of the owner login | passkeys are bound to the origin; step-up via passkey | T-AC-02 |
| T | CSRF on admin actions | SameSite=Strict, mandatory header, Origin check (SEC-CORS-4) |
T-AC-01 |
| R | Owner denies a change | audit with step-up time and session | T-AZ-04 |
| I | XSS reads data from the console | CSP without foreign origins, Trusted Types, Flutter renders no HTML from data (19, 5) | T-AC-01 |
| D | Rate-limit lockout of the owner | lockout only of the password path (08, 13) | T-ID-03 |
| E | Clickjacking on confirmation buttons | frame-ancestors 'none', X-Frame-Options: DENY |
T-AC-01 |
5.3 Network edge and proxy¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| S | Forged identity header | loopback only, trusted proxies only, binding per request (08, 2.4) | T-ID-02 |
| T | Forged X-Forwarded-For |
IP only from trusted proxies (SEC-TLS-1, 08, 13) | T-ID-03 |
| I | Eavesdropping on the network | TLS, HSTS | T-NW-02 |
| D | Connection flood | rate limits, timeouts (SEC-IN-3) | T-DS-01 |
| E | Direct access bypassing the proxy | plaintext only on loopback and from trusted proxies | T-ID-02 |
5.4 API (REST)¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| S | Requests with someone else's token | signature, aud, DPoP, session check |
T-TK-01 |
| T | Duplicate JSON keys, unknown fields | strict decoding (SEC-IN-1, SEC-IN-2) | T-IN-01 |
| R | Person denies an admin action through the API | audit according to the column "Audit" in 05 | T-AZ-04 |
| I | IDOR, existence revealed through 403 or timing | one single decision point, 404 instead of 403, equality test (07, AZ-P1, AZ-P4) | T-AZ-01, T-AZ-02 |
| D | Large bodies, expensive searches | size limits, rl.search, page limits |
T-DS-01 |
| E | Route without an action, administration without step-up | mandatory registration, step-up (07, SR5) | T-AZ-01 |
5.5 Gateway (WebSocket)¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| S | Cross-site WebSocket hijacking with the console cookie | Origin check on upgrade (SEC-CORS-3) |
T-AC-01 |
| T | Replay of identify |
DPoP with a server nonce from hello (04, R-SY-01) |
T-TK-01 |
| I | Delivery after access removal | permissions at delivery time, access.changed in log order (07, 8.2) |
T-AZ-03 |
| D | Slow consumer | outbound buffer limit, 4014 (04) |
T-DS-01 |
| D | Connections held open without identify until sync.identify_timeout, oversized or malformed frames |
Close 4003 after sync.identify_timeout; the upgrade reads no database; frames above sync.ws_max_frame_bytes and frames failing the JSON guard or the schema close with 4002 before any use case runs (M1-64) |
T-DS-01 |
| D | Draining the rl.gateway_connect bucket of a foreign device |
The bucket counts only after the credentials of identify or resume are verified, keyed by the verified device or token (05, M1-64) |
T-DS-01 |
| E | Focus on foreign threads | gateway.focus checks per thread (05) |
T-AZ-01 |
5.6 authz and identity¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| S | Account takeover through email matching | matching only through (issuer, subject) or enrollment code (08, 3.1) |
T-ID-01 |
| T | Stale permission cache | version counter per request, valid_until (07, 7.3) |
T-AZ-03 |
| R | Probing of foreign resources goes unnoticed | denials in the audit, aggregated per window (R-AZ-04) | T-AZ-04 |
| I | Counters and search show foreign channels | SQL filter plus authorize per element (07, 7.4) |
T-AZ-02 |
| D | Permission recomputation on many changes | global counter, recomputation only when needed; load test | T-DS-01 |
| E | Delegation beyond one's own level | R-AZ-05 made precise (07, 2.6) | T-AZ-05 |
5.7 PostgreSQL with event log and audit¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| T | Modification of the audit log | application role only INSERT/SELECT, hash chain, exported checkpoints (ADR 0021) |
T-DB-01 |
| R | Proof of an action is removed afterwards | hash chain, checkpoints outside the database | T-DB-01 |
| I | Database dump | tokens only as HMAC, secrets only as references (03) | T-BK-01 |
| D | Lock bottleneck at the sequence counter | counter step last (04, 3.2); load test | T-DS-01 |
| E | SQL injection | only sqlc with parameters (SEC-IN-5) | T-IN-01 |
5.8 Files, object storage, virus scanner¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| S | File with a wrong type | detection by the server (SEC-UP-2) | T-UP-01 |
| T | File attached across channel boundaries | scope_channel_id (SEC-UP-9) |
T-AI-01 |
| I | Location data in photos, permanent links | strip metadata, no object URLs to clients (SEC-UP-10, 11) | T-UP-01 |
| D | Decompression bomb, scanner outage | pixel limit, separate process, fail closed (SEC-UP-4 to 6) | T-UP-01 |
| E | Malicious code in parsers | parsers in a separate process, only pure Go in M1 | T-UP-01 |
| I | Ticket URL reaches third parties (Open with, log of a player) | short validity file.ticket_ttl, binding to principal and session, file.read on every fetch, ticket never in logs |
T-AZ-03, T-LG-01 |
5.9 Worker and jobs¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| T | Job with system permission reads without a filter | reviewed exception list for queries without a permission filter (07, AZ-P12) | T-AZ-02 |
| R | Expiry of grants without a trace | system actions grant.expire in the audit (07, 8.3) |
T-AZ-04 |
| I | Webhook to a wrong target | allowlist, check at connection setup (SEC-NET-*) | T-NW-01 |
| D | Queue congestion | separate queues, metrics (17-observability.md) | T-DS-01 |
| I | A worker consumer reads the whole event log without a permission filter | jobs check push.deliver or webhook_out.deliver before sending (R-AZ-11); the reads are on the exception list of system reads (07, 7.4); logs and job arguments carry only the consumer name, sequence numbers and IDs (M2-01) |
T-AZ-02 |
| D | A worker consumer stands still and events fall out of sync.event_retention |
gauge bothy.event.consumer.pending_age, alarm BothyEventConsumerLag at consumer.pending_alert_after, held strictly below the retention by the start check (M2-01) |
T-DS-01 |
5.10 Agent adapters and runtime¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| S | Forged invocation to the adapter | EdDSA-signed invocation token, aud, expiry (I3, R-SY-08) |
T-TK-05 |
| T | Prompt injection through content | origin labeling, tool classes, approval cards | T-AI-01, T-AI-03 |
| R | Who triggered the agent? | runs.triggered_by, audit, token with sub |
T-AZ-04 |
| I | Agent reads foreign channels for the triggering person | acting-for binding SR9, read horizon hint | T-AI-02 |
| D | Loops, cost explosion | agents never trigger, rate limits, cost limits | T-AI-07 |
| E | Person with few permissions uses an agent with a shell | SR2 tool class, SR3 dev isolation | T-AI-02, T-DEV-01 |
| S | Takeover of a consumer with a stolen agent token: an attacker identifies with the same consumer, takes the stream over, and the real adapter is closed with 4015 |
DPoP binding of agent tokens (the token alone does not connect), revocation of the token and agent.disable close with 4012, the takeover is logged with both gateway session ids, and 4015 tells the adapter not to reconnect, so the loss is visible (M2-07) |
T-TK-02 |
| T | Adapter reports a false reported mode: a heartbeat claims organization while the runtime uses a personal subscription, or reports a lower tool class than it loaded |
The heartbeat evidence (reported_billing_mode, billing_evidence) serves only display, warning, audit and alarm and is never a permission (BR-05, M2-20); the binding check stays in the adapter per run (I3) and at run.start (BR-06); a reported tool class only raises the effective class, because authorize counts the higher of configured and reported class (SR2) |
T-AI-08 |
| I | An agent's suggestions reveal content that the triggering person may not read: while it computes command.autocomplete_requested, an adapter reads the API with its own rights and returns names from channels the person cannot read (M2-17) |
The request is checked like agent.invoke (trigger, I2, SR2, SR4) and goes only to the owner; the server cannot filter free-form choices, which are only checked as display texts (SEC-IN-4). SR9 applies only in a run; residual risk until M2-14 decides whether an autocomplete token authenticates requests as acting_for |
T-AI-02 |
5.11 Apps and webhooks¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| S | Forged outgoing webhook at the receiver | asymmetric signature (ADR 0016) | T-PL-01 |
| T | Incoming webhook as a trigger | origin = app, never triggers (SR4) |
T-TK-03 |
| R | Unclear who gave an app which scopes | audit of installation and scope changes with step-up | T-PL-01 |
| I | App reads more than needed | scopes plus grants (07, 2.10) | T-PL-01 |
| D | Webhook flood | rl.webhook_in |
T-DS-01 |
| E | App confirms approval cards | apps never trigger, never approve_actions (SR4) |
T-PL-01 |
| S | Forged or replayed webhook at the receiver | v1a always, v1 per subscription, webhook-timestamp and webhook-id for freshness and duplicates, test vectors (G10; M2-26) |
T-PL-01 |
| I | App receives events outside its grants or scopes | webhook_out.deliver before every attempt with current permissions, dead without a request (M2-26) |
T-PL-01 |
| I | Webhook to a private target (DNS rebinding) | host list webhook.allowed_hosts, egress client with an address check at connection setup, no redirects (M2-26) |
T-NW-01 |
| I | HMAC secret from the database or the idempotency store | derived, never stored, one-time response (M2-26) | T-TK-02 |
| D | Slow receiver congests deliveries | order per subscription, queue delivery, delivery quota, disabling (M2-26) |
T-DS-01 |
| S | Forged request to the interaction address of an app | v1a with purpose webhook_out, checked by the app against getWebhookKeys; no token or secret is sent to the app (M2-25) |
T-PL-01 |
| I | Form values and options leave the instance | only to the owning app, only https to a host in webhook.allowed_hosts checked before the egress client, no redirects, canary test (SEC-T10; M2-25) |
T-PL-01 |
| D | Slow interaction address | bound by app.interaction_timeout, job without retry after the request, queue default; the component stays used (M2-25) |
T-DS-01 |
| E | App command or reply triggers an agent | origin = app, no agent.invoke, no run and no trigger_state (SR4; M2-25) |
T-PL-01 |
| E | App card imitates a system header | header from server fields, author and origin set by the server, an app card has no run (SEC-OUT-4; M2-25) |
T-PL-01 |
| S | Forged or replayed GitHub delivery to the reference app | X-Hub-Signature-256 (HMAC-SHA256 over the raw body) checked in constant time before anything is parsed, duplicates by X-GitHub-Delivery within github_app.dedupe_window (11 10.1; M2-49) |
T-PL-02 |
| T | Foreign text from GitHub mentions people or sets links | logins in code spans and bodies in fenced code blocks whose delimiter the text cannot close (03 content format rule 2), links only to the https address of the event, titles without control and bidi characters (SEC-IN-4; M2-49) |
T-PL-02 |
| I | Chat content goes to GitHub | only with messages:read, events:subscribe and a subscription to events_url, only text messages of people in threads linked to an issue, named in data_egress; nothing about the person from /issue (M2-49) |
T-PL-01 |
| D | Flood of GitHub deliveries at the reference app | github_app.max_body_bytes, github_app.max_queued_jobs (503 above), one worker (M2-49) |
T-PL-02 |
| E | Loop between chat and GitHub | the app's own messages (author = app) never mirrored, only origin = human, the app's own GitHub issues and comments do not come back (M2-49) |
T-PL-02 |
| S | Forged, replayed or method-swapped Jira delivery to the reference app | X-Hub-Signature read as method=signature, the method compared with an allow list holding only sha256 before any HMAC is computed (no fallback to SHA-256), HMAC-SHA256 over the raw body compared in constant time before anything is parsed, duplicates by the SHA-256 of the body within jira_app.dedupe_window (11 10.2, RK-83; M2-50) |
T-PL-02 |
| T | Foreign text from Jira mentions people or sets links | display names and field values in code spans, descriptions and comments in fenced code blocks whose delimiter the text cannot close (03 content format rule 2), links only to <jira_app.jira_base_url>/browse/<KEY> built from the configuration, titles without control and bidi characters (SEC-IN-4; M2-50) |
T-PL-02 |
| I | Chat content goes to Jira | only with messages:read, events:subscribe and a subscription to events_url, only text messages of people in threads linked to an issue, as ADF text that Jira never reads as markup, named in data_egress; nothing about the person from /issue (M2-50) |
T-PL-01 |
| D | Flood of Jira deliveries at the reference app | jira_app.max_body_bytes, jira_app.max_queued_jobs (503 above), one worker (M2-50) |
T-PL-02 |
| E | Loop between chat and Jira | the app's own messages (author = app) never mirrored, only origin = human, the app's own Jira issues and comments do not come back, checked at receipt and again in the worker (M2-50) |
T-PL-02 |
5.12 Push¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| I | Push provider reads content | push only with IDs and a neutral placeholder (ADR 0031) | T-PU-01 |
| D | Provider outage | the app catches up on opening (04) | T-DS-01 |
| E | Push after access removal | push.deliver per push (07, 8.1) |
T-AZ-03 |
| S | Preview token stolen from the shared keychain | DPoP binding to the device key, ends with registration, session and device, message.read at each fetch |
T-AZ-03 |
| I | Push or preview token in logs | never logged, canary test SEC-T10 | T-LG-01 |
| T | Someone registers a foreign device token | the token is a device secret that the server never returns; takeover only moves IDs without content (ADR 0031) | T-PU-01 |
5.13 Telemetry and logs¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| I | Secrets or content in logs and traces | allow lists, canary test (SEC-LOG-*, SEC-T10) | T-LG-01 |
| D | Telemetry blocks the message path | fail-open, queue (Baseline R-Telemetrie) | T-DS-01 |
5.14 Operations and backups¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| S | First visitor becomes owner | first owner only via server command (08, 4) | T-ID-04 |
| I | Backup stolen | encryption, keys kept separately (SEC-BAK-*) | T-BK-01 |
| T | Restore brings back deleted data | deletion journal (SEC-BAK-3) | T-BK-02 |
| T | The smoke test overlay or its settings end up in an installation (test images, staging, disabled backup, file export of telemetry) |
overlay only under infra/compose/test, excluded from make release-build; own project name bothy-smoke-<run ID>; no published port; environment staging |
T-OP-01 |
5.15 Supply chain¶
| STRIDE | Threat | Mitigation | Register |
|---|---|---|---|
| T | Manipulated dependency, action or image | lockfiles, SHA pinning, digests, Renovate with a waiting period (SEC-SC-*) | T-SC-01, T-SC-02 |
| E | Code from a foreign PR on a runner of the maintainers; poisoned cache reaches release secrets | only GitHub-hosted runners (SEC-SC-3, ADR 0039); release jobs without cache and without values of other jobs outside of conditions (D-W-52) | T-SC-03, T-SC-05 |
| S | Forged release | signature, provenance, SBOM (ADR 0036) | T-SC-04 |
5.16 Terminal¶
Pairing, relay, host service and terminal view (24-terminal.md).
| Part | STRIDE | Threat | Mitigation | Register |
|---|---|---|---|---|
| Pairing | S | A foreign host redeems an intercepted pairing code | code single-use and short-lived, the owner types in the fingerprint, failed attempts limited | T-TM-06 |
| Pairing | T | Server swaps the key of a host | pinning per device, hard warning, audit of every re-pairing, notification to all owners | T-TM-01 |
| Relay | I | Relay, proxy, logs or database dump read terminal content | Noise NK end-to-end; only length and time visible |
T-TM-01 |
| Relay | T | Relay replays the first packet or puts the token into its own handshake | one-time token, cek, prologue, PTY only after start |
T-TM-02 |
| Relay | D | Flood of openings, slow end | rl.terminal, rl.host, session limits, buffer limit with 4014 |
T-DS-01 |
| Host service | E | Shell escalates to root or administrator on the host | user without admin and without groups with root-level permissions (for example docker), doctor refuses the start (HD1 to HD3); foreign home folders not readable (HD9; for the home of another session user, HD11) |
T-TM-04 |
| Host service | I | Person with a terminal copies the keys of the host service and impersonates the host from elsewhere | sessions as a separate session user without read permission on the keys (HD8); a second live control connection disables the host; re-pairing | T-TM-05 |
| Host service | T | Shell replaces the PTY helper, forges check results and reads along later sessions | helper as a system service with binary, definition and channel endpoint owned by a trusted owner (root; on Windows SYSTEM, Administrators or TrustedInstaller); verification of the peer via the process ID of the service manager; memory protection of the helper (HD10); warning for owners on the host of another person | T-TM-12 |
| Host service | E | Background process or long-running process survives the access removal | process group, cgroup or Job Object per session; a separate session user per person, never reassigned (T12); hint to take it out after the access removal and to delete it together with its long-running processes | T-TM-11 |
| Relay | D | Edge without hash routing splits the stream paths of a session across two nodes (M2-47) | lone side closes after terminal.open_timeout with 4102, metric, no broker, example edge (16 4.4) |
T-TM-15 |
| Relay | E | End, revocation or second control connection on another node goes unnoticed (M2-47) | end and revocation through the log tail of every node, state transition as a signal in the transaction, reconciliation after reconnect, probe across nodes (24 3.4, 9) | T-TM-05, T-TM-11 |
| Host service | E | Person places settings or hooks of the coding agent in a folder that the session of another person uses, and they run there as the other person's session user | No shared locations (Ruling T13): working directory and CLAUDE_CONFIG_DIR per session user, index of the working directory only in the own list; HD11 for every configured path after resolution, including folder chain and chain files such as CLAUDE.md in it, on Windows also the drive root |
T-TM-13 |
| Host service | D | One person with a terminal brings the host to a standstill for the others, with a subscription login in their account (HD6) or with a chain file in a shared folder on the path to the locations of others (HD11) | the report names every affected session user; folders on the path writable for no session user, on Windows also the drive root after bothy-host install; instance operator step in 16 section 4.4: remove the login or entry, or take the session user out with bothy-host session-user remove |
T-TM-14 |
| Host service | R | Who worked on which host and when? | audit per opening, attach and end, with device and reason | T-AZ-04 |
| Permissions | E | Person with a grant opens a foreign host or the server host | SR10, server host owners only, checked twice | T-TM-03 |
| Permissions | E | A second person uses the personal subscription on a host | a separate session user per person, never shared (SR12, T12), without shared locations (T13, HD11); I6 transactional and on every opening; doctor HD6 as an additional check |
T-TM-08 |
| Permissions | E | Agent opens a terminal after prompt injection | terminal only for people in a session with a fresh step-up | T-TM-09 |
| Device | S | Stolen session or stolen device opens a terminal | step-up fresh with biometrics or passkey per opening |
T-TM-03 |
| Device | T | Escape sequences write to the clipboard or open links | SEC-TERM-6 | T-TM-10 |
| Console | I | Browser extension reads an open session | passkey required, CSP, recommendation of a separate browser profile | T-TM-07 |
| Console | S | A foreign page opens the stream path with the console cookie (cross-site WebSocket hijacking) | origin check on upgrade (08, section 7.5), SameSite=Strict; without the server's terminal session token and the tab's Noise key no PTY is created |
T-TM-07 |
6. Threat register¶
6.1 Rating¶
- L (likelihood without the named mitigations):
lowneeds rare prerequisites or insiders;mediumis plausible in a targeted attack or an operating error;highis to be expected without a mitigation. - I (impact):
lowaffects one person or one thread and is fixed quickly;mediumaffects a space or a limited data leak;highaffects the instance, a host, all content or has legal consequences. - Residual risk after mitigations: level and what remains.
6.2 AI-specific threats¶
| ID | Threat | L | I | Mitigations | Tests | Residual risk |
|---|---|---|---|---|---|---|
| T-AI-01 | Prompt injection across space boundaries. Content from space A reaches an agent in space B: one agent serves both spaces, a file is passed on, a thread is moved, an import brings foreign texts, a runtime retains knowledge. (OWASP LLM01) | high | high | agent per audience recommended, read horizon hint H1 (07, 6); files only in their own channel (SEC-UP-9); moving only with the same operator (03, threads); origin of every content in the invocation (SEC-AI-1); imports with origin = import never trigger (SR4); trust mark per content in the context, files always as data (ADR 0054) |
AZ-M44, AZ-M45, SEC-T08, conformance test of the adapters (AZ-P6) | medium: injection itself cannot be prevented; the runtime memory is the responsibility of the instance operator |
| T-AI-02 | Privilege escalation through agents (confused deputy). A person with few permissions triggers an agent with powerful tools or a wide read horizon. (OWASP LLM06) | high | high | tool class host only for owners or one's own ready dev environment (SR2); acting-for binding (SR9); I1 for personal subscriptions (SR1); grants to agents only by the owner (07, 2.6) |
AZ-M20, AZ-M44, AZ-P11, AZ-P7; M2-14: TestAZM44_ActingForInsufficient, TestAZM44_EndToEnd, TestAZP11_ActingForProperty, TestSR9_EveryScopedList |
medium with scoped: tools with their own credentials act within the scope of these credentials; low with chat |
| T-AI-03 | File and link content as an injection source. A PDF, a transcript or a web page that a tool loads contains instructions. | high | medium | the server loads no URLs (SEC-NET-*); files labeled with the origin "file" (SEC-AI-1); external effects only through approval cards (scoped); network tools only in the runtime with a tool class; trust mark per content in the context, files always as data (ADR 0054) |
SEC-T09, SEC-T14, test_context_format_trust_blocks, test_context_format_attachments_and_transcripts, test_ag20_context_marks_unverified_content (plugins/hermes-adapter) |
medium: depends on the discipline of the runtime and on people who read cards |
| T-AI-04 | Agent writes into foreign channels. A compromised or deceived runtime posts in spaces that it does not serve. | medium | medium | agents write only with a grant or as operator (07, 2.9); acting-for binding when writing outside their own thread (SR9); rl.message (05 4.7, 09 14 point 4) |
AZ-M28, AZ-M29, AZ-P11, AG-26; M2-14: TestAZP11_ActingForProperty, TestAZP11_WritesEndToEnd, TestSR9_RevocationImmediate, TestSR9_InvalidHeader |
low |
| T-AI-05 | Approval obtained by deception through cards. An agent builds a card that looks harmless, or imitates a system header. | medium | high | card header from server fields (SEC-OUT-4); approve_actions required; by default only the triggering person of the run (SEC-AI-4); audit of every confirmation |
SEC-T14, AZ-M42, AZ-M43 | medium: a person can still confirm a poorly described effect |
| T-AI-06 | Exfiltration through rendering. Agent output contains image URLs with data in the path, or links that send data when loaded. (OWASP LLM05) | high | medium | clients load no foreign resources (SEC-OUT-2); link target visible (SEC-OUT-1) | SEC-T13 | low: only a deliberate click on a link remains |
| T-AI-07 | Loops and consumption. Agents trigger each other, or a person starts runs en masse. (OWASP LLM10) | medium | medium | only origin = human triggers (SR4); rl.trigger; at most one active run per thread (R-DM-14); cost limits per agent (10-backend-policy.md) |
AZ-M30, AG-01 | low |
| T-AI-08 | Misuse of a personal subscription. A second person uses an agent on the personal subscription of the owner; violation of the provider terms, suspension of the organization possible. | medium | high | I1 transactional, I2 before every invocation, I3 configured and reported mode in the adapter (SR1), a reported mode that cannot be determined aborts (billing_mode_mismatch, fail closed); badge "Personal" in every header; the reported mode of the heartbeat is evidence for display, warning, audit and alarm only, never a permission (BR-05, M2-20) |
AZ-M21 to AZ-M25, AZ-P6, AZ-P7, AZ-M51, BR-07, BR-08, TestFollowupNeverReachesPersonalAgentForOthers, test_ag11_actual_mode_unequal_target, test_unknown_actual_mode_gives_mismatch, test_actual_mode_matches_used_credential, test_change_of_actual_mode_during_run_aborts (plugins/hermes-adapter) |
low in the server; medium in the adapter: the proof of the reported mode with Hermes Agent depends on a non-public interface, and no test in the repository fully covers a mode falsely reported as safe (RK-13); whether a runtime is permitted in personal mode is the responsibility of the instance operator (Baseline R-Anthropic) |
| T-AI-09 | Leak of the channel prompt. An agent reproduces its prompt. (OWASP LLM07) | high | low | prompts readable only with manage, never in events (R-AZ-09); hint "no secrets in the prompt" (SEC-AI-8) |
SEC-T10 | low, as long as prompts contain no secrets |
6.3 Tokens, devices, identity¶
| ID | Threat | L | I | Mitigations | Tests | Residual risk |
|---|---|---|---|---|---|---|
| T-TK-01 | Theft of a device token (malware, log, proxy). | medium | high | DPoP with a non-exportable key, nonce, jti check, short lifetime (08, 7) |
ID-T14, ID-T17 | low; malware that uses the key on the device remains possible while it runs |
| T-TK-02 | Theft of an agent or app token (configuration file, repository). | medium | medium | only the hash stored, prefix for secret scanners, DPoP required for agents, scopes, at most write, no triggering, expiry required (08, 12) |
ID-T23, SEC-SEC-4; M1-44: TestAPITokens_Issue/TestIDT23_OnlyHashStored, TestAPITokens_Issue/TestIDT23_ShownOnce, TestIDT23_WrongChecksumRejectedWithoutLookup, TestAPITokens_Authenticate/TestIDT23_ExpiredRejected, TestAPITokens_Authenticate/TestIDT23_RevokedRejected, TestAPITokens_Authenticate/TestIDT23_DPoPBoundAgentTokenWithoutProof, TestAPITokens_Lifecycle/TestRevokeToken_Immediate, TestAPITokens_Lifecycle/TestAPIToken_NoReplayAfterRevocation, TestAPITokens_Authenticate/TestAPITokenNeverLogged, test_bothy_token_with_valid_checksum_is_detected, test_checksum_vectors_match_go; M2-24: TestCreateToken_AppShownOnce, TestAppToken_EffectiveScopes, TestCreateToken_AppScopes, TestAuthenticateAPIToken_AppInstallation, TestAppScope_TokenAndInstallation, TestUninstall, TestUninstall_RacesCreateToken |
medium for app tokens without DPoP: reading and writing within the scope of the app until revocation |
| T-TK-03 | Theft of the URL of an incoming webhook. | medium | low | one channel, writes as the app, never triggers, rl.webhook_in, revocable |
AZ-M53, SEC-T10 (URL not in the log); M2-27: TestAZM53_IncomingWebhookNeverTriggers, TestPostIncomingWebhook_NotFoundEquality, TestPostIncomingWebhook_TokenOnlyAsPattern, TestWebhookActor_ScopesLimitedToMessagesWrite |
low: spam in one channel until revocation |
| T-TK-04 | Theft of a private signing key. | low | high | only in the key store secrets.signing_key_dir (mode 0700, files 0600, ADR 0066), private file deleted at retired, scheduled rotation, emergency rotation with mode = emergency (19, 10) |
SEC-T11, M1-32 (TestEmergencyRotation_*, canary) |
medium: whoever has it forges tokens until the rotation; detection through audit anomalies |
| T-TK-05 | Reuse of an invocation token. | medium | medium | expiry agent.invocation_token_ttl, aud, jti single-use via compare-and-swap at run.start (R-SY-10) |
AZ-P6; M2-06: TestReferenceVerifier_RejectsAlgAndTyp, TestReferenceVerifier_Points (replay, expiry), TestVerify_Reasons, TestInvocationToken_ForeignPurposeKey, TestAG22_EmergencyRotationTokenLevel; M2-29: azp6_adapter_never_starts and ag08_invalid_tokens against the FakeServer and the real server, test_verifier_agrees_with_reference_cases |
low |
| T-DV-01 | Lost or stolen device. | high | medium | app lock, encrypted cache, revocation with 4012, wipe at the next contact, push registrations of the device disabled in the revocation transaction (08, 9 and 10; no revoked push, D-W-108) |
ID-T26, client tests in 15-client-architecture.md | medium: a device without contact keeps the cache; protection then only through OS encryption |
| T-ID-01 | Account takeover through identity providers (matching by email, foreign tenant, mix-up). | medium | high | matching only through (issuer, subject), tid list, iss check, PKCE (08, 2 and 3) |
ID-T01 to ID-T05; M1-41: TestOIDC_Login/TestIDT01_PKCEDowngradeRejected, TestOIDC_IssParameter/TestIDT02_StateIssAndVerifier, TestOIDC_Login/TestIDT03_IDTokenRejected, TestOIDC_Login/TestIDT04_NoMappingByEmail, TestOIDC_Entra/TestIDT05_EntraTenantAllowList, TestOIDC_Login/TestOIDCLogin_LeakedAuthorizationURLCannotLink, TestIDTokenClaimValidation, TestOIDCEgress_PrivateTargetsRejected |
low |
| T-ID-02 | Forged proxy header. | medium | high | driver only with loopback listeners (otherwise off, /readyz reports it, W4), only trusted peers, binding per request (08, 2.4) |
ID-T08 to ID-T10, SEC-T01; Go tests TestIDT08_DriverOffWithoutLoopback, TestTrustedHeaderLogin/TestIDT09_MissingLoginHeader401, TestTrustedHeaderLogin/TestIDT10_BindingMismatchRevokesSession, TestTrustedHeaderLogin/TestBinding_MissingHeaderRevokes, TestSECT01_HeaderFromUntrustedPeerIgnored, TestTrustedHeaderProvisioning/TestTrustedHeaderNeverLogged (internal/identity/adapters/httpapi) |
low; residual risk with a misconfigured proxy |
| T-ID-03 | Guessing of passwords and codes. | medium | medium | Argon2id, limits per account, code and IP, real client IP (08, 13) | ID-T11, ID-T20, ID-T21, ID-T25 | low |
| T-ID-04 | Takeover during the initial installation. | medium | high | 503 setup_required until the server command (08, 4) |
ID-T06, ID-T07 | low |
| T-ID-05 | A foreign device gets approved (a person blindly presses "Allow"). | medium | high | typed-in confirmation code, step-up when approving (08, 5.2) | ID-T12, ID-T13 | low |
| T-ID-06 | Delayed disabling after leaving (account disabled in the identity provider, the session continues). | medium | medium | user.disable immediately; new login at the latest after auth.idp_recheck_interval (08, 3.5) |
ID-T22 | medium until the planned automatic adoption of disablings (ADR proposal SCIM) |
6.4 Permissions¶
| ID | Threat | L | I | Mitigations | Tests | Residual risk |
|---|---|---|---|---|---|---|
| T-AZ-01 | Missing check at a route or op (IDOR). | high | high | one single decision point, x-authz-action required, startup check, route test (ADR 0017) |
AZ-P1, AZ-P2, AZ-P3 | low |
| T-AZ-02 | Leak through counters, search, suggestions or response time. | high | medium | SQL predicate plus authorize per element, lint, 404 equality (07, 7.4) |
AZ-P4, AZ-P5, AZ-P12, AZ-M06 | low |
| T-AZ-03 | Delayed access removal (cache, open connection, ongoing run, push). | medium | high | version per request, re-evaluation on access.changed, cancellation of the runs, I2 and push.deliver (07, 8.2) |
AZ-P9, AZ-P10, AZ-M51; TestGateway_AZP9TwoInstances, TestGateway_RevocationDuringOpenConnection, TestGateway_RevocationCloses4012, TestAuthzObserve_AccessChangedRaisesFloor, TestAuthzObserve_NewRightsVersionInvalidatesWithoutAccessChanged, TestAuthzObserve_StructureEventInvalidates, TestHub_UnauthenticatedDecisionCloses4012, TestQueue_BoundedAndPurgeByScope |
low; on expiry by time, the client cache is cleared only after authz.expiry_sweep_interval |
| T-AZ-04 | Repudiation of permission changes and denials. | medium | medium | audit according to 05, hash chain, step-up with device, system actions for expiries and key changes | AZ-P13 | low: a hard crash loses the open aggregations of denials of one window (audit.deny_aggregate_window, 19 SEC-AUD-6); allow decisions and permission changes are recorded in their transaction |
| T-AZ-05 | Privilege escalation through delegation (manage grants more than it has, revokes something stronger). |
medium | medium | delegation limits (07, 2.6) | AZ-M37 to AZ-M39 | low |
| T-OW-01 | Owner reads other people's DMs or misuses "View as person". (insider, E7) | low | high | DMs never for owners (SR6), "View as person" read-only, with step-up, time limit and audit (07, 9.2) | AZ-M02, AZ-M46, AZ-M47 | medium: owners with database access technically have everything; to be settled organizationally through a works agreement (20, 9) |
6.5 Platform, network, data¶
| ID | Threat | L | I | Mitigations | Tests | Residual risk |
|---|---|---|---|---|---|---|
| T-PL-01 | Malicious plugin or app. Reads everything within its scopes, deceives people with cards or messages. | medium | medium | apps never in the server process (B6), scopes plus grants, at most write, no triggering, installation only by owners with step-up, badge "App" (SEC-OUT-3), webhooks only to allowed targets |
AZ-M32 to AZ-M34, AZ-M49; M2-24: TestAZM32_AppWritesWithGrantAndScope, TestAZM33_AppReadWithoutScope, TestAZM34_AppWithoutGrant, TestInstallApp_RejectsInvalidManifests, TestValidateManifest_Rules, TestInstallApp_OwnerStepUpAudit, TestUpgrade_NewScopesStayInactive, TestUpgrade_RemovedScopeActsAtOnce, TestSuspendResume, TestAppAccessChanged_SuspendResumeUninstall, TestAppAuditDetails_Shape, TestAppEventScope_AllCatalogueEvents; M2-25: TestAppCardRules, TestAppNeverTriggersAgent, TestInteractionsURLSecurity, TestAppCommandsSkipI2; M2-49: plugins/github-app/test/live/github-app.live.test.ts; M2-50: plugins/jira-app/tests/live/test_live.py |
medium: what an app may read, it can exfiltrate; that is trust in the provider of the app |
| T-PL-02 | Forged, replayed or looping events of an integration. | medium | low | X-Hub-Signature-256 in constant time before parsing, duplicates by X-GitHub-Delivery and webhook-id, github_app.max_body_bytes and github_app.max_queued_jobs, one worker, GitHub text only in code spans and code blocks, links only to https addresses of the event, the app's own messages and GitHub objects never mirrored back, only origin = human (11 10.1); for Jira X-Hub-Signature with the method allow list sha256 checked before any HMAC, duplicates by the SHA-256 of the body, jira_app.max_body_bytes and jira_app.max_queued_jobs, Jira text only in code spans and code blocks, links only to <jira_app.jira_base_url>/browse/<KEY>, text to Jira only as ADF (11 10.2) |
M2-49: plugins/github-app/test/signature.test.ts, github-endpoint.test.ts, render.test.ts, loop.test.ts, issue-command.test.ts, logs.test.ts, test/live/github-app.live.test.ts; M2-50: plugins/jira-app/tests/test_signature.py, test_jira_endpoint.py, test_render.py, test_loop.py, test_issue_command.py, test_logs.py, test_scopes.py, tests/live/test_live.py |
low: an intercepted GitHub delivery can be replayed after github_app.dedupe_window, and an intercepted Jira delivery after jira_app.dedupe_window, because neither signature carries a timestamp |
| T-UP-01 | Malicious uploads (malware, bombs, active content, location data). | high | medium | SEC-UP-1 to SEC-UP-11 | SEC-T08 | low; the virus scan does not detect everything, hence no inline rendering of active formats |
| T-NW-01 | SSRF. | medium | high | no loading of foreign URLs, egress client with IP check at connection setup (SEC-NET-*) | SEC-T09 | low |
| T-NW-02 | Eavesdropping or redirection on the network. | medium | high | TLS, HSTS, verify-full, no disabled certificate verification |
SEC-T01, SEC-T02, SEC-T17 | low |
| T-PU-01 | Push metadata. Push providers see time, device, app, frequency. | high | low | only IDs and a neutral placeholder (ADR 0031); IDs random | tests in 13-push-notifications.md | low: metadata stays visible; data processing according to 20 |
| T-BK-01 | Theft of a backup. | medium | high | encryption, key kept separately, no secrets in plaintext (SEC-BAK-1, 2) | restore drill | low, as long as the key stays separate |
| T-BK-02 | Restore brings back deleted data. | medium | medium | deletion journal (20, 4.6; SEC-BAK-3) | SEC-T15 | low |
| T-OP-01 | Test settings in an installation. The overlay of the Compose smoke test (test images, a disabled backup, telemetry written to a file, environment staging) is copied into or combined with an installation. |
low | medium | overlay only under infra/compose/test with a "test only" header, excluded from the release bundle (release-build), own project name per run, no published port, explicit --env-file so a real .env is never read |
test_release_bundle_excludes_smoke_overlay, test_overlay_publishes_no_ports (M1-73) |
low: an operator can still copy the file by hand |
| T-DB-01 | Modification of the audit log with database access. | low | high | permissions of the application role, hash chain, signed and exported checkpoints (ADR 0021) | SEC-T16; the application-role part is proven since M1-24 (TestSECT16_AppRoleCannotUpdateOrDelete, TestAudit_TriggerBlocksOwnerUpdate in services/server/migrations), the detection of a changed row and of a rewritten chain through the checkpoints of the sink is proven since M1-50 (TestSECT16_ChangedRowIsDetected, TestSECT16_RewrittenChainIsDetectedByCheckpoint, TestVerifyIgnoresDatabaseCheckpoints in services/server/internal/audit/adapters/httpapi) |
medium: whoever controls the database and the export target can change both |
| T-LG-01 | Secrets or content in logs and telemetry. | high | medium | allow lists, type for secrets, route patterns (SEC-LOG-*) | SEC-T10 | low |
| T-IN-01 | Input attacks (injection, parser differences, bidi deception). | medium | medium | SEC-IN-1 to SEC-IN-8 | SEC-T06, SEC-T07 | low |
| T-DS-01 | Overload (connections, search, uploads, sequence counter). | medium | medium | rate limits (05), size and time limits, load tests (23-quality.md) | load tests | medium: one host carries everything (Baseline B1, B13) |
| T-AC-01 | Attacks on the console (XSS, CSRF, clickjacking, session theft). | medium | high | BFF cookie, CSP, Trusted Types, origin check, short session, step-up via passkey (08, 7.5; 19, 5) | SEC-T03 to SEC-T05, ID-T19 | low; malicious browser extensions remain (recommendation: a separate browser profile for administration) |
| T-AC-02 | Phishing of the owner. | medium | high | passkeys are bound to the origin; OIDC through the provider with its MFA | low with passkeys, medium with a plain password with TOTP |
6.6 Supply chain and dev environment¶
| ID | Threat | L | I | Mitigations | Tests | Residual risk |
|---|---|---|---|---|---|---|
| T-SC-01 | Manipulated dependency. (OWASP LLM03 for model and agent packages) | medium | high | lockfiles with checksums, Renovate with a waiting period, OSV-Scanner, license gate (SEC-SC-1) | gates in 23 | medium: a new, still unknown compromise is detected by no scanner |
| T-SC-02 | Manipulated GitHub Action. | medium | high | full SHA, zizmor, actionlint, minimal permissions (SEC-SC-2); example: the Trivy incident in Baseline R-Qualität |
gates in 23 | low |
| T-SC-03 | A self-hosted runner executes foreign code. | high | high | only GitHub-hosted runners with a fixed label (SEC-SC-3, ADR 0039) | runners-check (rule runner-not-github-hosted) |
low |
| T-SC-04 | Forged release or update. | low | high | signature, notarization, Sparkle EdDSA, provenance, SBOM (Baseline R-Desktop-Verteilung, ADR 0036) | release check in 18 | low |
| T-SC-05 | Cache poisoning through a dependency PR. A pull request (for example an update from Renovate) executes foreign code and modifies a cache that a later job with signing secrets uses. Second edge: a compromised job without signing secrets, in the release the job resolve, dictates its own path, a version or an artifact dist-* that a release job builds, signs or publishes. |
medium | high | Every job runs in a new VM of a GitHub-hosted runner (ADR 0039); the release jobs in release.yml restore no cache; Renovate with a waiting period (ADR 0041). Release jobs use outputs of resolve only in the if: of the job or of a step (D-W-72), resolve the tag again from the fixed component list, abort on a mismatch and download artifacts only by ID (D-W-52). Environments count in any spelling, environment names only literally (D-W-110) |
runners-check (rules runner-not-github-hosted, environment-not-literal, release-untrusted-output, release-artifact-by-id); that release jobs use no cache is checked by the review |
low for release secrets and release content; medium for the deny list and SONAR_TOKEN, which jobs of pull requests from the repository get, and for gate results that a compromised pull request job can forge |
| T-DEV-01 | Dev runner as a back door to the host. A coding agent without tool permission prompts runs with the permissions of the host user; every person with trigger and every injection in the repository thereby has a shell on the host. |
high | high | SR2 and SR3: owners only, until an environment per person with a VM boundary is ready (21) | AZ-M15 to AZ-M20, Red Team P3 | low today, because only owners trigger; medium after enabling, depending on the environment |
6.7 Terminal¶
| ID | Threat | L | I | Mitigations | Tests | Residual risk |
|---|---|---|---|---|---|---|
| T-TM-01 | Reading terminal content in the server (relay, proxy, log, database dump, backup, memory dump of an api process). | medium | high | Noise NK end-to-end (SEC-TERM-1); no content in log, audit, telemetry, event log |
TM-T01, TM-T02, TM-T06, TestRelay_ForwardsBytesUnchanged (with the canary and counter checks of M2-44) |
low against these attackers. Not protected against a malicious instance operator of the server (E12): they can distribute their own key and allow sessions. The only countermeasures are pinning per device (warns only on a change after the first opening; ineffective in the web console, because the server delivers the code of the console), audit of the re-pairing and host_service.require_local_confirm; residual risk medium, honestly documented (Ruling T3) |
| T-TM-02 | Replay and swap at the relay. The relay repeats message 1 or inserts a foreign token to start a PTY that nobody operates. | medium | high | one-time token with jti, cek bound to the ephemeral key, prologue with session and host, PTY only after start (SEC-TERM-2) |
TM-T03 to TM-T05, TestOpen_HappyPathShell (claims), TestAttach_TMT12 (new jti per grant), TestRelay_HostRefusalCodes (M2-44) |
low |
| T-TM-03 | Unauthorized opening through a stolen session, delegation, a foreign host or the server host. | medium | high | step-up fresh per opening, target host outside the tree, granted only by owners, SR10, server host owners only (SEC-TERM-3, 5, 10) |
TM-T07, TM-T08, AZ-M54, AZ-M55, AZ-M58, AZ-M60, TestOpen_TMT07_StepUp, TestAttach_TMT12, TestStream_IdentifyDecisions, TestOpen_DeniedOpensNothing (M2-44) |
low |
| T-TM-04 | Privilege escalation on the host. The shell runs as a user with admin permissions, passwordless sudo or in a group with root-level permissions (docker, lxd, libvirt, disk), or it reads the home folders of other users (on macOS through the group staff). |
medium | high | doctor HD1 to HD3 refuses the start (fixed block list of groups plus host_service.extra_forbidden_groups, Docker socket), HD9 for readable home folders of users who are not session users; an open home of a session user blocks, according to HD11, only that session user, by name (24 section 2.2 item 4); installation with its own user, session-user remove closes the home, afterwards the instance operator deletes the user together with its long-running processes (16, 4.4) |
TM-T13, TM-T24 | medium: gaps in the operating system of the host and local services remain; the host belongs to the trust zone of its assignee |
| T-TM-05 | Theft of the keys of the host service, for example by a person with a terminal on the host (also after their access removal) or from a backup of the state folder. With both keys the attacker logs in from elsewhere as the host, receives every session.open, passes the handshake and reads along every later session, including those of owners together with the passwords they type. They do not need a relay for this. |
medium | high | sessions run as session users without read permission on the state folder (HD8, 24 section 2.2); file 0600 or DPAPI; a second live control connection disables the host with audit and notification to all owners (24, section 3.4); re-pairing discards both keys |
TM-T18, TM-T22, TM-T23, TestCrossNode_DuplicateControlConnection (probe across nodes and probation, M2-47) |
medium: whoever escalates to the service user on the host has the keys; if the real host is then off, the attacker receives sessions until the real host comes back and triggers the disabling. host_service.require_local_confirm closes this for hosts with a person in front of them |
| T-TM-06 | Pairing of a foreign host with an intercepted code. | low | high | code single-use, short-lived, format with checksum and secret scanner rule; the owner types in the fingerprint; failed attempts limited | TM-T19, TM-T20 (TestTMT19_FingerprintMaxAttempts, TestTMT20_PairingCodeRejections, TestTMT20_WrongChecksumNoDatabase, TestHostPairingCode_MatchesGitleaksRule, test_pairing_code_with_valid_checksum_is_detected, M2-41) |
low |
| T-TM-07 | Browser extension or XSS in the console reads or controls an open session. | medium | high | passkey required per opening, CSP with Trusted Types, no HTML from output, origin check on upgrade of the stream path (SEC-TERM-7, ADR 0070) | SEC-T05, ID-T28, origin test (PLAN M6-18) | medium: a malicious extension with access to the origin remains; recommendation of a separate browser profile |
| T-TM-08 | Misuse of someone else's LLM login through the terminal, also through a subscription login that someone creates during a session, or by reading the API key from the environment of the profile claude. |
medium | high | Load-bearing: one session user per person and host, never shared, not even after the end of the assignment; no terminal without an assignment (SR12); the host service binds every session user to the first person (Ruling T12, 24 section 2.2); no location connects the accounts again: working directories and CLAUDE_CONFIG_DIR per session user, HD11 for every configured path (Ruling T13, T-TM-13). In addition I6 with an individual grant, checked on every opening; as an additional check doctor HD6, which refuses the start on a contradiction to the declaration and runs before every PTY and periodically (a hit ends all sessions), only through host_service.claude_bin with a fixed search list, which HD10 checks like the helper, including folder chain and interpreter; key per host with a spending limit and a new key after access removal; profile claude with a controlled environment (24, 7) |
TM-T09, TM-T13, TM-T27, TM-T28, TM-T29, AZ-M56, AZ-M57, AZ-M61 | low for other people's subscription logins: a login in a session user reaches only its person, even if HD6 does not see it (macOS without host_service.claude_bin, warning auth_status_unchecked) or its result is forged from the same account, as long as no location connects the accounts again (T13). Remains: a shared folder outside the configured paths in which a person starts the coding agent themselves (T-TM-13); a local privilege escalation on the host lifts the separation of the session users (T-TM-04); after retiring and re-pairing the host, or after loss of the state folder, only the instance operator keeps a previously assigned session user away from another person (24 section 2.2, 16 section 4.4); the declaration of the owner cannot be verified, doctor shows a false declaration organization with a hidden subscription only at known locations; with organization the API key is readable by every session |
| T-TM-09 | Prompt injection opens a terminal. | high | high | terminal actions only for people in a session with step-up; agent and app tokens rejected | TM-T17, AZ-M59, TestOpen_DeniedOpensNothing, TestClose_RightsAndRepeats and TestStream_IdentifyDecisions with the agent token (M2-44) |
low |
| T-TM-10 | Escape sequences write the clipboard, open links or query data. | medium | medium | SEC-TERM-6 | TM-T14 | low |
| T-TM-11 | Session or processes remain after access removal. The session stays open, or processes of the person keep running (nohup, setsid, child processes outside the ConPTY, long-running processes such as crontab, systemd services of the user, LaunchAgent, scheduled task). |
medium | high | access removal ends sessions in the same transaction; the server additionally measures idle time; every session in its own process group, cgroup or Job Object, whose end ends the whole tree (24, 6.3, 6.5 and 8.3); a session user belongs to only one person and is never reassigned (SR12, T12) | TM-T10, TM-T11, TM-T28, TestCrossNode_CloseReachesRelayAndControl, TestCrossNode_ListenerLossReconciles (ends and reconciliation across nodes, M2-47) |
low for other people: no session end ends long-running processes that were set up as the session user, but they run in the session user of the person whose access was removed, which nobody else gets; they do not reach the keys (HD8), no foreign home folders (HD9; for the home of another session user, HD11) and cannot replace the helper (HD10). Remains: they keep using computing time, network and local services of the host until the instance operator deletes the session user together with its long-running processes (locking alone does not end them; 16 section 4.4 item 1); the console points this out after the access removal of a person who is not an owner; after bothy-host session-user remove they can reopen the home of their user until then and bring the host to a standstill through HD9 (T-TM-14); non-owners work only on their own host (SR10). After retiring and re-pairing the host, or after loss of the state folder, only the instance operator prevents the session user, together with such long-running processes, from being assigned to another person (24 section 2.2, 16 section 4.4) |
| T-TM-12 | Replacing or tapping the PTY helper. A shell or a long-running process of the session user ends the helper and puts its own in its place, attaches to the running helper or to shells of later sessions, forges HD6, HD8, HD9 and HD11 and reads the plaintext of later sessions of the same session user. | medium | high | helper as a system service under the session user; binary, definition and channel endpoint not writable for it; channel endpoint from the service manager; bothy-host checks user and process ID of the peer, and on a wrong peer the message of HD10 names the session user of this channel (24 section 2.2, rule for names); memory protection of the helper; on Linux kernel.yama.ptrace_scope at least 1; HD8 additionally through the service user (HD10, 24 section 2.2); warning before the step-up for owners on the host of another person, and audit foreign_assignee (24, 8.4) |
TM-T22, TM-T26, TM-T28 | medium: processes of the same user ID reach the PTY device of every session of this session user; since T12 these are only sessions of the same person, sessions of different people are separated by the user ID. Against an assignee who manipulates their own host or escalates privileges there, the only protection is that owners do not work there or take the warning seriously |
| T-TM-13 | Shared location between session users. Person A writes into a location that the session of B reads or executes: .claude/settings.json with hooks or apiKeyHelper, CLAUDE.md or env in a shared working directory or a folder above it, a shared CLAUDE_CONFIG_DIR or a program of a profile. The code of A then runs as the session user of B and reaches B's files and login; a /login in a shared CLAUDE_CONFIG_DIR reaches all people. |
medium | high | Ruling T13 (24 section 2.2 item 6): working directories per session user (host_service.claude_workdirs, read only at start; HD7 removes a folder listed for two session users from both lists), index only in the own reported list (wdi, resolved by the host service in exactly this list of osu), CLAUDE_CONFIG_DIR with organization fixed in the home of the session user and with personal only in the home, no foreign chain files (CLAUDE.md, CLAUDE.local.md, AGENTS.md, .mcp.json, .claude, .git) in the folders above, programs including folder chain owned by a trusted owner (on Windows SYSTEM, Administrators or TrustedInstaller), the profile claude with organization only with host_service.claude_bin; HD11 checks every configured path after full resolution including links, and the home including folder chain and chain files, with one rule; "writable" includes creating entries; on Windows bothy-host install removes the permission to create folders in the drive root; before every PTY and periodically, and it blocks only the affected session user; shared code only through git (SEC-TERM-13) |
TM-T29, TM-T24 | low for configured locations. Remains: a folder outside the configuration that two session users can write and in which a person starts the coding agent themselves in their shell (HD11 does not see it; the instance operator does not set it up, according to 16 section 4.4); a file that a later version of the coding agent loads from the folder chain and that is not yet among the chain files; HD11 never sees permissions that a session user opens and closes again between two checks, sees an entry that A creates in this time only at the next start of the service or call of bothy-host doctor, and does not see at all what A writes into an existing file that is writable for A |
| T-TM-14 | Standstill of a host caused by one person. With none or organization a person with a terminal logs in with a subscription in their own session (/login); HD6 detects this, the host service ends all sessions of all people and accepts no new ones until HD6 passes in every session user from host_service.session_users (Ruling T2). Second path: a chain file of one person in a folder on the path to the locations of several session users (for example a C:\.git from the time when the root of the drive was writable for everyone) makes HD11 fail for every other one of these session users. Third path: a long-running process that a person set up as the session user reopens the home of their former session user after bothy-host session-user remove; HD9 then refuses the start of the whole host on the next start or bothy-host doctor. |
medium | medium | The report of HD6 names every affected session user with its own entry, and the console also the person of its assignment, also on a hit at start: the host service then does not start, but reports the result beforehand, and bothy-host doctor names the same names on the host (24 sections 2.2 and 7.4); instance operator step in 16 section 4.4: remove the login in exactly this account, or end the assignment and take the session user out of the host service with bothy-host session-user remove (locking the user alone does not end the standstill, because HD6 runs in every name from host_service.session_users); audit and host.updated to all owners. Against the second path: folders on the path to a location are writable for no session user (HD11); on Windows bothy-host install closes the drive root; the report of HD11 names every affected session user, and the instance operator removes the entry (16 section 4.4). A home that a person with terminal access opens for others is no path to the standstill: HD11 checks it as a location of this person and blocks only that person, not HD9 with "start refused" (24 section 2.2 item 4). An attempt to replace the PTY helper (T-TM-12) brings the host to a standstill through HD10 as well; the message then names the session user of this channel (24 section 2.2, rule for names). Against the third path: the message of HD9 to the server names every former session user whose home can be listed, the console also the person of its assignment (24 section 2.2, "Reason for HD9"); instance operator step in 16 section 4.4 item 1: right after remove remove the long-running processes of the user, end its processes and delete it together with its home, after which HD9 no longer checks its home; locking alone is not enough |
TM-T21, TM-T24, TM-T26, TM-T29 (i) | medium: until the instance operator acts, nobody has a terminal on the host, or none of the blocked session users has one. This stays so because Ruling T2 forbids the start on a contradiction to the declaration and HD11 must not skip a foreign chain file; the second path exists only with an entry from a time when a folder on the path was writable; the consequence is reversible and stays on the host |
| T-TM-15 | Misrouting and lost signals across several nodes (M2-47). An edge without hash routing sends the two stream paths of a session to two api nodes, or a signal between nodes (NOTIFY ephemeral) is lost, forged or late, so a lone side waits, bytes would have to cross nodes, or an end, a host transition or a second control connection on another node goes unnoticed. |
low | medium | Lone side closes after terminal.open_timeout with 4102, counted in bothy.terminal.relay.unpaired, /readyz stays ready, no broker, frames never leave the node; example edge with hash routing (16 4.4); end and revocation through the log tail of every node; state transition as a signal in the transaction; signals carry only kind and IDs and every receiver reads the state from the database; reconciliation after a listener reconnect; probe across nodes and probation of a new control connection (24 3.4, 9) |
TM-T15 (TestTMT15_TwoNodesWithHashRouting, TestTMT15_WithoutHashRouting, TestTMT15_WithoutHashRouting_Attach), TestCrossNode_DuplicateControlConnection, TestCrossNode_CloseReachesRelayAndControl, TestCrossNode_ListenerLossReconciles, TestCrossNode_HostTransitionsCloseEverywhere, TestCrossNode_SignalsCarryOnlyIDs, TestCrossNode_ForgedSignalOpensNothing (M2-47) |
low: while a node's LISTEN ephemeral connection is down, its control connections of a host disabled or re-paired elsewhere stay open until the reconciliation after the reconnect (the sessions still end through the log tail), and opens through its control connections fail visibly with open_failed |
6.8 Visualization¶
Normative together with 25-visualization.md; controls SEC-VZ-* in 19-security.md, section 11b.
| ID | Threat | L | I | Mitigations | Tests | Residual risk |
|---|---|---|---|---|---|---|
| T-VZ-01 | Clickjacking through a Grafana embedding. With allow_embedding Grafana no longer sends X-Frame-Options; a foreign page frames Grafana and lures a logged-in owner into clicks. |
low | medium | embedding off by default; only behind an auth proxy that sets frame-ancestors to the origin of the console; frame-src of the console exactly this origin (SEC-VZ-2) |
VZ-T12, SEC-T19 | low, as long as the proxy is configured correctly; the configuration of the proxy is the responsibility of the instance operator |
| T-VZ-02 | Leak through the live stream. A space manager sees runs, costs or instance values outside their channels, or content gets into frames. | medium | medium | delivery mode per frame, filter on visible channels, costs only with cost.read.aggregate, rows without a channel only for owners, no titles and details (SEC-VZ-3, SEC-VZ-4) |
VZ-T05, VZ-T06, VZ-T07 | low |
| T-VZ-03 | Performance monitoring of individual people through runs in channels with only one person. | medium | medium | no values per person, no triggering person, no counters across people; live stream can be switched off; note for co-determination (20, section 9) | DP-T12 | medium: from runs in a channel with one person, conclusions about that person are possible; the owner can read the thread anyway |
| T-VZ-04 | Unattended TV screen shows costs or runs to visitors, or the session behind it is misused. | medium | low | TV mode without controls, costs hidden by default (viz.tv_show_costs), admin actions need step-up, no kiosk token (R-VZ-09) |
medium: the session on the screen has the read permissions of the logged-in person; recommendation: a separate person with only manage on the shown spaces |
|
| T-VZ-05 | Malicious or modified Grafana plugin. | low | medium | plugin from the own repository with supply chain checks; unsigned only allowed with its own ID (SEC-VZ-5); signature after V1 | VZ-T13 | low |
| T-VZ-06 | Overload from many viewers of the live stream. | medium | low | one base frame per node and interval, maximum number of subscriptions per node, rate limit rl.read for the REST query, cached frame |
VZ-T08 | low |
7. What remains open¶
- Prompt injection is unsolved. The product does not rely on detection, but on boundaries: who triggers, what the agent reaches, what takes effect only with a card.
- The server does not see the memory of a runtime across runs (T-AI-01, H1).
- The terminal stream does not protect against a malicious instance operator of the server (T-TM-01); the product assumes that the server is trustworthy.
- Long-running processes that a person sets up as a session user on a host survive their access removal until the instance operator deletes this user together with them (T-TM-11); they stay in their own session user, which nobody else gets (T12). All sessions of one person on a host share one trust zone through their user ID; between people, the user ID separates only until a local privilege escalation (T-TM-04, T-TM-12) and only as long as no location connects the accounts again; HD11 checks the configured locations, not every folder of the host (T-TM-13).
- A person with a terminal can bring a host with
noneororganizationto a standstill for everyone through a subscription login in their own account, until the instance operator removes the login or takes their session user out of the host service; likewise with a chain file from a time when a shared folder on the path was writable for them, until the instance operator removes it; likewise with a long-running process that reopens the home of their former session user afterbothy-host session-user remove(HD9), until the instance operator deletes this user (T-TM-14). - One host carries everything (T-DS-01). More availability means several roles on several hosts (Baseline B1), not part of M1.
8. Link to tests¶
Every threat in the register names tests from 07, 08 or 19. A threat without a test has an empty entry in the column "Tests" and, in the next red team gate, must either get a test or justify its residual risk explicitly.
Terminal (M2-48): the catalogue services/server/test/terminal/catalogue.yaml lists, for every
TM-T ID of 24-terminal.md section 14 and for every AZ-M, ID-T and SEC-T
ID that section 6.7 names, where it runs (end-to-end scenario, platform test per operating system,
chaos test of make perf or the unit test of the implementing ticket), which ticket implemented
it and which T-TM threats it covers. The docs gate tools/docs/terminal_catalogue.py, run by
make docs, keeps it so: every T-TM row of 6.7 names at least one test; every test ID named
there has a catalogue entry that lists the threat; every Go test named there exists as a function
under services/; every PLAN reference names a row of the plan; and every place the catalogue
names exists. A new threat row, a renamed test or a deleted scenario file therefore fails
make docs in the same change.
9. Maintenance rule¶
The threat model is updated:
- At the start of every milestone: enter the new components, data flows and boundaries of the milestone.
- Before every red team gate (end of every milestone, before the merge into
main, before every release): The reviewer compares the diff with the system boundary diagram. A new data flow across a boundary, a new external dependency, a new login method, a new tool class or a new app feature without an entry here is a finding that blocks the merge. - Immediately after a security incident, a finding from a penetration test or an ADR that changes a boundary from section 2.
- Every change carries the milestone in the status header ("as of milestone ...") and goes in as a pull request with review by a second person or a fresh red team.
- If a mitigation is removed or weakened, the residual risk of the entry rises in the same change; an entry never keeps a rating whose basis is gone.