Threat Model
This document identifies who would attack the platform, where trust boundaries exist, what data crosses those boundaries, and what threats apply to each flow. For the platform’s security posture and controls, see Security Model.
Scope
In scope: Control plane, enclave, LLM proxy, agent containers, database, Redis, SSE event stream, and all data flows between them.
Out of scope: External IAM provider internals, upstream LLM provider (Anthropic API) internals, and the operator’s broader cloud/network infrastructure.
Assets
What an attacker actually wants, roughly ordered by blast radius:
| Asset | Where it lives | Why it matters |
|---|
| IAM signing key (ES256) | IAM service; only the public half is published via JWKS | Root of trust for every platform and operator JWT - compromise forges arbitrary identities org-wide, not just one agent |
| Enclave API key (SHA-256 hashed) | Enclave process | Grants the enclave’s full capability set - impersonate any of its agents, forge JWTs, decrypt secrets in transit |
| Refresh token (24h, single-use, family-rotated) | Sidecar-only volume, never mounted into the main container | Long-lived; theft lets an attacker mint new platform JWTs repeatedly until rotation/replay detection catches it |
| Agent/org secrets (API keys, credentials) | Encrypted at rest (AES-256-GCM, DEK/KEK); decrypted only at delivery | Direct access to whatever third-party system the secret unlocks |
| Platform JWT (5-min, ES256) | Agent main container, read-only volume | Grants agent identity to the Enclave/LLM Proxy for the token’s remaining lifetime |
Database records (agents, secret ciphertext, audit logs, iam_* tables) | PostgreSQL | Central store - a database attacker can reach most of the above at rest |
| Redis state (JTI blacklist, SSO sessions, rate-limit counters) | Redis (optional component, deployed as separate broker-local and control-plane instances - see DF-14/DF-15) | Backs instant revocation and session validity; write access can un-revoke a token or forge a session |
| LLM audit log (full request/response bodies) | llm_audit_log table | May contain PII, credentials pasted into prompts, or proprietary content from every agent conversation |
| Bootstrap token (single-use, 1h) | Plaintext env var at container creation | One-time claim of a freshly created agent’s identity |
| Policy definitions & approval records | Database (policy_decisions) | Governs which tool calls agents may execute; tampering changes what actions are permitted |
Assumptions
- TLS is correctly configured and not intercepted at any boundary; several entries below (DF-2, DF-3) explicitly degrade to plaintext-secret exposure if this breaks (e.g.
--insecure-skip-tls). - The enclave/broker host OS and its Podman or Kubernetes control plane are not themselves compromised - the “Compromised Enclave” actor is analyzed as already holding enclave credentials, not as a host-level privilege escalation starting from nothing.
- The database and Redis (where deployed) are reachable only from platform services, never directly from agent containers or the public internet - the “Database Attacker” actor and the Redis threats in DF-14/DF-15 assume access came via a compromised service or leaked credential, not an open port.
- On the Kubernetes backend, kata-containers VM isolation (
kata-clh) actually contains a container escape at the hypervisor boundary. Kata is mandatory for every agent pod, so DF-10 treats that VM boundary as the containment guarantee - there is no in-pod hardening layer behind it - and does not separately model a kata/cloud-hypervisor escape. - Bootstrap and refresh tokens are stored only as specified (SHA-256 hashed at rest, single-use) and operators do not copy them into logs, tickets, or other systems outside the platform.
Threat Actors
| Actor | Starting Access | Motivation |
|---|
| Compromised Agent | Main container holding the delivered platform JWT (read-only) and secrets; the refresh token lives on the sidecar-only volume. Network access (forced through the proxy) to the enclave and proxy. | Escalate privileges, access other agents’ data, exfiltrate secrets, pivot to internal services. |
| Malicious Operator | Valid IAM JWT scoped to an organization. Access to the operator API. | Access another organization’s data, escalate within the org, abuse destructive operations. |
| External Attacker | No credentials. Network access to public-facing endpoints. | Steal tokens, exploit unauthenticated endpoints, man-in-the-middle, denial of service. |
| Compromised Enclave | Enclave API key, ECDSA signing key, secrets decryption key, database credentials. All agent secrets pass through in cleartext during delivery. | Impersonate any agent, forge JWTs, intercept secrets, manipulate provisioning. |
| Database Attacker | Read or write access to PostgreSQL. | Extract secrets, forge tokens, manipulate agent state, access cross-tenant data. |
Trust Boundaries
┌─────────────────────────────────────────────────────────┐
│ Operator Network │
│ │
│ ┌──────────┐ ┌────────────────┐ ┌───────────┐ │
│ │ dvx CLI │────▶│ Control Plane │◀────│ IAM │ │
│ │(operator)│ │ │ │ │ │
│ └──────────┘ └───────┬────────┘ └───────────┘ │
│ │ SSE + API │
│ ┌───────────────────┼──────────────┐ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ Enclave │ │ │
│ │ └──┬───────┬──┘ │ │
│ │ │ │ │ │
│ │ ┌───────┘ └────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────┐ ┌───────────┐ │ │
│ │ │ Agent │ │ LLM Proxy │──┼──▶ Anthropic │
│ │ │(untrust)│ │ │ │ │
│ │ └─────────┘ └───────────┘ │ │
│ │ │ │
│ │ Agent Network │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
| Boundary | Between | Authentication |
|---|
| B1 | Agent ↔ Enclave | Bootstrap token (single-use), then platform JWT (ES256, 5min) |
| B2 | Agent ↔ LLM Proxy | Platform JWT (ES256, 5min) or netproxy-forwarded service-signed identity headers |
| B3 | Enclave ↔ Control Plane | Enclave API key (SHA-256 hashed) |
| B4 | Enclave ↔ Compute Runtime | Unix socket (Podman) or service account (Kubernetes) |
| B5 | Operator ↔ Control Plane | IAM JWT (ES256, JWKS-validated) |
| B8 | LLM Proxy ↔ Upstream LLM | Resolved API key (injected from encrypted storage) or agent’s own credentials (passthrough mode) |
| B9 | All services ↔ Database | Connection string credentials |
| B10 | Agent cert ↔ Platform CA | X.509 client certificate (ECDSA P-256, 5min, issued by enclave CA) |
| B11 | Broker ↔ Redis; Control Plane/IAM ↔ Redis | Connection string credentials (optional component; broker-local and control-plane/IAM instances are typically separate - see DF-14/DF-15) |
Note: Boundaries B6 and B7 previously covered the web Operator Console (SPA ↔ Control Plane and Console ↔ IAM); the web console has been removed. Operators now use the dvx CLI, whose operator authentication is covered by B5 (and the OAuth2 device flow against IAM). The remaining boundary numbers are unchanged.
Note: The database and Redis are shared backing stores reachable from multiple services (Control Plane, IAM, Broker, LLM Proxy) and are not drawn as boxes in the diagram above to keep it legible - see boundaries B9 (database) and B11 (Redis), and data flows DF-14/DF-15.
The dvxir sidecar shares the agent’s trust domain
On Kubernetes, an agent pod has two containers: the dvxir sidecar (bootstrap, JWT refresh, tunnel, SSH) and the operator’s unmodified main container. They are one trust domain, not two - there is no trust boundary between them, and none is relied upon. Identity is delivered to the main container via shared volumes: the 5-minute JWT read-only (a tampered JWT fails ES256 validation at the enclave/proxy, so the worst case is self-DoS, not impersonation), and the dvxir binary at /opt/dvx/bin/dvxir. The longer-lived refresh token stays on a sidecar-only volume the main container never mounts and out of argv/env, so its exposure is no greater than the in-container model it replaces. shareProcessNamespace lets the sidecar enter the agent (for SSH), one-directionally; the agent gains only read visibility of the sidecar’s process list, never its capabilities. Moving the runtime into a sibling container therefore crosses no boundary and grants the agent nothing new.
Control plane ↔ IAM tables
IAM owns the iam_* tables (accounts, organizations, RBAC roles, OAuth2 clients), and other services normally only validate IAM-issued JWTs via JWKS - they do not mutate IAM data. The operator IAM-management surface (/api/v1/iam/*, boundary B5) is the documented exception: it lets the control plane write to iam_* tables - creating accounts, mutating organizations, assigning/revoking roles, and managing OAuth2 clients - making the control plane a second writer alongside dvx iam. These writes are gated by RBAC (members:*, org:manage, clients:manage) and are strictly org-scoped from the operator JWT claims - no route accepts a client-supplied organization id, including for platform-admin - so the blast radius of a leaked operator token stays confined to its own tenant. The control plane and IAM share one database and trust domain, so this is a write path within an existing boundary, not a new one.
Data Flows
DF-1: Agent Discovery
- Boundary: B1 (Agent → Enclave)
- Trigger: Agent startup
- Auth: None (unauthenticated)
- Data: Agent sends
GET /v1/agent/discover. Enclave returns capabilities: service name, version, available endpoints.
DF-2: Agent Bootstrap
- Boundary: B1 (Agent → Enclave)
- Trigger: Agent first boot with bootstrap token
- Auth: Single-use bootstrap token in request body
- Data: Agent sends
{ token }. Enclave returns identity bundle: { agent_id, organization_id, agent_name, jwt, refresh_token, secrets (decrypted), config, config_hash }.
DF-3: Agent JWT Renewal
- Boundary: B1 (Agent → Enclave)
- Trigger: Every 4-5 minutes via agent refresh loop
- Auth: Refresh token in request body (single-use, family-rotated)
- Data: Agent sends
{ refresh_token }. Enclave returns { jwt (new), refresh_token (new, rotated), secrets, config, config_hash }.
DF-4: Agent Secret Access
- Boundary: B1 (Agent → Enclave)
- Trigger: Agent requests a credential on demand
- Auth: Platform JWT (Bearer token)
- Data: Agent sends
GET /v1/agent/secrets/{name}. Enclave returns { name, value (decrypted) }. Also: GET /v1/agent/secrets returns name list (no values).
DF-5: Agent Config Access
- Boundary: B1 (Agent → Enclave)
- Trigger: Agent startup, drift detection
- Auth: Platform JWT (Bearer token)
- Data: Agent sends
GET /v1/agent/config. Enclave returns merged config (platform → org → agent). Agent can also write agent-scoped config via PUT /v1/agent/config/{key}.
DF-6: Agent LLM Request
- Boundary: B2 (Agent → LLM Proxy), then B8 (Proxy → Upstream)
- Trigger: Agent makes an inference call
- Auth: Platform JWT (Bearer token) or
x-api-key header - Data: Agent sends Anthropic Messages API request. Proxy validates JWT, resolves the agent’s LLM API key from the database, injects it, forwards to upstream. Buffers the SSE response, runs audit interceptors, replays to agent.
DF-7: LLM Audit Logging
- Boundary: B9 (Proxy → Database)
- Trigger: Every LLM request/response pair
- Auth: Database connection credentials
- Data: Proxy writes to
llm_audit_log: organization_id, agent_id, model, input/output tokens, full request body (JSONB), full response body (JSONB).
DF-8: Enclave SSE Event Stream
- Boundary: B3 (Enclave → Control Plane)
- Trigger: Enclave startup (long-lived connection)
- Auth: Enclave API key (Bearer token)
- Data: Enclave connects to
GET /v1/broker/events with optional Last-Event-ID cursor. Control plane streams SSE events (e.g., agent.created with agent_id, name, org_id). Heartbeat every 30 seconds.
DF-9: Agent Provisioning (Podman)
- Boundary: B4 (Enclave → Podman)
- Trigger: Enclave receives
agent.created SSE event - Auth: Unix socket (implicit local trust)
- Data: Enclave generates bootstrap token, calls Podman REST API to create container with env vars:
DVX_BROKER_URL, DVX_BOOTSTRAP_TOKEN (plaintext), DVX_AGENT_ID, DVX_AGENT_NAME, DVX_TLS_CA. CA cert bind-mounted read-only.
DF-10: Agent Provisioning (Kubernetes)
- Boundary: B4 (Enclave → Kubernetes API)
- Trigger: Enclave receives
agent.created SSE event - Auth: In-cluster service account token
- Data: Enclave creates namespace (labeled, PSA Privileged), applies NetworkPolicy, waits for the merged CA ConfigMap (trust-manager: public roots + interception CA), and creates a two-container pod - the dvxir sidecar plus the operator’s main image - with
runtimeClassName: kata-clh, so the pod runs inside its own kata VM. Kata is mandatory, not a per-broker opt-in: with no kata-clh RuntimeClass configured, no agent pod is created at all. The in-pod security context is deliberately loose for every agent (the image’s own default UID including root, image-default capabilities, Unconfined seccomp, allowPrivilegeEscalation: true, and a root dvxir sidecar that drops all capabilities and adds nine back - five for s6-overlay init plus four for nsenter SSH), while automountServiceAccountToken: false still denies Kubernetes API access. Platform identity inputs (bootstrap token, client cert) go to the sidecar only; operator-set per-agent env vars are injected into the main container. The kata VM boundary, not the pod security context, is what contains a container escape (see DF-10 Elevation of Privilege).
DF-11: Operator API (Agent CRUD)
- Boundary: B5 (Operator → Control Plane)
- Trigger: Operator action via the
dvx CLI - Auth: IAM JWT (Bearer token, validated against JWKS)
- Data: Create, list, get, update, delete agents. Change agent status. Revoke tokens. Trigger re-provisioning. All scoped by organization_id from JWT.
DF-12: Operator API (Enclave Management)
- Boundary: B5 (Operator → Control Plane)
- Trigger: Operator action
- Auth: IAM JWT
- Data: Register enclave (returns API key once), list, get, delete, rotate API key. All scoped by organization_id.
DF-13: Operator API (Secrets Management)
- Boundary: B5 (Operator → Control Plane)
- Trigger: Operator action
- Auth: IAM JWT
- Data: Create/update secret (plaintext in, encrypted at rest with AES-256-GCM). List secrets (metadata only - name, agent, timestamps - no encrypted values returned).
DF-14: Broker-Local JTI Blacklist (Kill Switch)
- Boundary: B11 (Broker ↔ Broker-local Redis)
- Trigger: Operator issues an isolate/restore action via the broker kill-switch RPC; also consulted on every platform JWT validation
- Auth: None on the connection itself (local/in-network Redis, authenticated via
requirepass/TLS if configured) - Data: The broker adds/removes the agent’s JTI under a
dvx:jti: key with a TTL (internal/redis/blacklist.go). Isolation and restoration are performed entirely enclave-side - agent_runtime.isolated_at, refresh-token revocation, and the JTI blacklist all live in the broker’s own Redis, distinct from the control plane’s. Every subsequent JWT validation (agentauth.Validator configured WithBlacklist) checks this store before accepting a token.
DF-15: Control Plane / IAM Redis (Sessions, Device Flow, Rate Limiting)
- Boundary: B11 (Control Plane/IAM ↔ Redis)
- Trigger: SSO login/logout, OAuth2 device authorization polling, login rate limiting
- Auth: None on the connection itself; Redis auth/TLS if configured
- Data: IAM stores SSO session blobs (
iam:session:) and a per-account session index (iam:account-sessions:) - the same key format is written by both the IAM OAuth service and the control-plane operator service’s account-suspension revocation path (design doc 036), so they must stay in lock-step. It also stores OAuth2 device codes (5-minute TTL) for the device authorization flow, and sliding-window login rate-limit counters (iam:ratelimit:) keyed by IP/account.
DF-16: Event Publishing and Purging
- Boundary: Internal (Control Plane)
- Trigger: Agent creation with broker_id (publish), hourly timer (purge)
- Auth: None (in-process)
- Data: Events written to
broker_events table with auto-incrementing cursor. In-process event bus notifies SSE handler. Purge deletes events already consumed by their enclave (cursor-based) or owned by deleted enclaves.
DF-17: Telegram Integration
- Boundary: B5 (Operator → Control Plane), then external (Control Plane → Telegram API)
- Trigger: Operator links a Telegram bot to an agent
- Auth: IAM JWT (operator), then Telegram bot token (validation call)
- Data: Operator sends bot_token. Control plane validates via Telegram
getMe API. Token encrypted and stored as agent secret (telegram:bot_token). Integration record created. Agent receives decrypted token during bootstrap/renewal.
DF-19: Broker URL Announce & Discovery
- Boundary: B3 (Enclave → Control Plane) for the announce; B5 (Operator → Control Plane) for discovery
- Trigger: Broker (enclave) startup advertises its operator-reachable URL; operator later resolves an agent via the
dvx CLI - Auth: Broker JWT (
POST /v1/broker/announce) for the announce; operator IAM JWT (agent GET) for discovery - Data: The broker reports its configured operator-reachable URL (
--operator-broker-url, falling back to --agent-broker-url) to the control plane, which records it trust-on-first-use (TOFU) in brokers.url, keyed strictly by the authenticated broker_id from its JWT (never client-supplied). The operator URL is intentionally distinct from the agent-facing URL in split topologies (in-cluster agents reach the broker by Service DNS; external operators reach it via an ingress/tunnel). The control plane cannot auto-detect it (the broker dials out, often from behind NAT). The control plane returns this URL as broker_url on the agent GET/list responses so the dvx CLI can reach the correct broker for SSH/port-forward/tunnel without an explicit --broker-url. The CLI sends the operator’s IAM JWT to this broker URL to obtain a short-lived SSH certificate (and for terminal/tunnel), so the URL governs where an operator credential is sent. Mitigations: the URL is pinned on first announce and a later announce with a different URL is rejected and logged as a tamper signal (a stolen broker credential cannot silently repoint operators at an attacker host after the legitimate broker has connected once); an operator can re-pin authoritatively via PATCH /api/v1/brokers/{id}; and --broker-url always overrides the discovered value. TLS verification of the broker endpoint remains mandatory on the CLI.
STRIDE Analysis
DF-1: Agent Discovery
| Category | Threat |
|---|
| Spoofing | An attacker could stand up a fake enclave and respond to discovery requests, directing agents to malicious endpoints. |
| Tampering | A MITM could alter the endpoint list in the response, redirecting agents to attacker-controlled services. |
| Repudiation | Discovery requests are unauthenticated and not logged - there is no record of which agents queried the enclave. |
| Information Disclosure | The response reveals enclave version and supported endpoints, which aids reconnaissance. |
| Denial of Service | The endpoint is unauthenticated and could be flooded. |
| Elevation of Privilege | Not applicable - no authentication or authorization occurs. |
DF-2: Agent Bootstrap
| Category | Threat |
|---|
| Spoofing | An attacker who intercepts or guesses a bootstrap token can impersonate the agent and claim its identity. |
| Tampering | A MITM could alter the response, injecting a malicious JWT or modified secrets into the agent’s identity bundle. |
| Repudiation | The bootstrap event is logged, but the agent has no way to prove which identity bundle it received. |
| Information Disclosure | The response contains decrypted secrets in plaintext over the wire. If TLS is compromised or --insecure-skip-tls is used, all secrets are exposed. |
| Denial of Service | An attacker could consume the bootstrap token before the legitimate agent, permanently blocking it from bootstrapping. A flood of invalid tokens causes repeated database lookups. |
| Elevation of Privilege | Prevented by design. A bootstrap token for one agent cannot be used to claim a different agent’s identity - tokens are bound to a specific agent_id. |
DF-3: Agent JWT Renewal
| Category | Threat |
|---|
| Spoofing | Mitigated. An attacker with a stolen refresh token can obtain a valid JWT and impersonate the agent, but this is bounded: refresh tokens are single-use and family-rotated, so the theft window closes as soon as either party (attacker or legitimate agent) next renews - see the Denial of Service row below for what happens on that collision. |
| Tampering | A MITM could modify the renewed JWT or secrets in transit if TLS is compromised. |
| Repudiation | Renewal events are logged with agent_id. Replay detection creates a forensic trail when tokens are reused. |
| Information Disclosure | Same as bootstrap - decrypted secrets are delivered in the response body. |
| Denial of Service | An attacker who steals a refresh token can use it first, which triggers replay detection and revokes the entire token family - denying service to the legitimate agent. |
| Elevation of Privilege | Prevented by design. Refresh tokens are scoped to a single agent - they cannot be used to renew tokens for a different agent. |
DF-4: Agent Secret Access
| Category | Threat |
|---|
| Spoofing | An attacker with a stolen JWT can retrieve secrets for that agent. |
| Tampering | Not applicable - endpoint is read-only. |
| Repudiation | Secret access is not individually logged (only renewal/bootstrap delivery is audited). |
| Information Disclosure | Secrets are returned in plaintext. A stolen JWT grants access to all of the agent’s secrets for the JWT’s lifetime. The secret name list endpoint reveals what integrations an agent has. |
| Denial of Service | Repeated requests could cause load on decryption operations. |
| Elevation of Privilege | Prevented by design. An agent can only access its own secrets - the enclave resolves agent_id from the JWT claims, not from the request. |
DF-5: Agent Config Access
| Category | Threat |
|---|
| Spoofing | An attacker with a stolen JWT can read and write agent-scoped config. |
| Tampering | A compromised agent can write arbitrary values to its own config, potentially influencing platform behavior (e.g., downstream URL overrides). |
| Repudiation | Config writes are not individually audited. |
| Information Disclosure | Config values (including org-level overrides) are returned in plaintext. May reveal infrastructure details (URLs, feature toggles). |
| Denial of Service | An agent could write excessive config entries. |
| Elevation of Privilege | Agents can only write agent-scoped config - they cannot modify org-level or platform-level config. However, if config merging gives agent-level config precedence, a compromised agent could override org-level settings for itself. |
DF-6: Agent LLM Request
| Category | Threat |
|---|
| Spoofing | An attacker with a stolen JWT can make LLM calls as the agent, consuming API credits and potentially exfiltrating data via crafted prompts. |
| Tampering | A compromised agent can send any content to the LLM - there is no request content validation at the proxy layer. |
| Repudiation | Full request and response bodies are written to the audit log, providing non-repudiation for LLM interactions. |
| Information Disclosure | The proxy resolves and injects the LLM API key - a compromised proxy or database attacker could intercept it. The agent never sees the raw API key (it’s injected after JWT validation). |
| Denial of Service | No rate limiting on the proxy - a compromised agent can make unlimited LLM calls, exhausting API credits. Request body limit is 32 MB. |
| Elevation of Privilege | The proxy falls back from agent-level to org-level secrets for API key resolution - all agents in an org share the org-level LLM credential. |
DF-7: LLM Audit Logging
| Category | Threat |
|---|
| Spoofing | Not applicable - audit writes are internal (proxy to database). |
| Tampering | If the database is compromised, audit records could be modified or deleted. The table is append-only by convention but not enforced at the database level. |
| Repudiation | The audit log exists to prevent repudiation. If it can be tampered with, repudiation becomes possible. |
| Information Disclosure | The audit log stores complete request and response bodies as JSONB. This may contain sensitive data from LLM interactions (PII, credentials in prompts, proprietary content). Anyone with database read access can see all logged conversations. |
| Denial of Service | The audit log grows continuously with no built-in retention. Over time it could consume all available storage. Logging is fire-and-forget - audit failure does not block the request. |
| Elevation of Privilege | Not applicable - audit writes use the proxy’s database credentials. |
DF-8: Enclave SSE Event Stream
| Category | Threat |
|---|
| Spoofing | An attacker with a stolen enclave API key can connect to the SSE stream and receive events intended for that enclave. |
| Tampering | Events are written by the control plane and delivered read-only via SSE - a network attacker cannot inject events into the stream. However, a database attacker could insert fake events directly into the broker_events table. |
| Repudiation | Event consumption is tracked via cursor (last_event_id) but individual event reads are not logged. |
| Information Disclosure | Events contain agent_id, agent_name, and organization_id. A stolen API key reveals all agents assigned to that enclave. |
| Denial of Service | A rogue client holding the API key could maintain a persistent connection, potentially blocking the legitimate enclave from connecting (depending on server-side connection limits). Events accumulate if the enclave never processes them. |
| Elevation of Privilege | Events are scoped to the authenticated broker_id - an enclave cannot receive events for other enclaves. |
DF-9: Agent Provisioning (Podman)
| Category | Threat |
|---|
| Spoofing | An attacker with access to the Podman Unix socket can create containers that appear to be legitimate agents. |
| Tampering | The bootstrap token is passed as a plaintext environment variable - any process that can inspect container metadata or /proc on the host can read it. The container image could be tampered with if the registry is compromised. |
| Repudiation | Container creation is not audited at the platform level (Podman’s own logs may capture it). |
| Information Disclosure | Bootstrap tokens and TLS CA paths are visible in container environment variables. No network isolation - containers on the same host can communicate with each other and reach host services. |
| Denial of Service | No resource limits are applied to containers by default. A single agent could exhaust host CPU or memory. |
| Elevation of Privilege | Containers share the host network namespace (pasta networking). A compromised agent can reach other agents, the database, Redis, and any host-local service. |
DF-10: Agent Provisioning (Kubernetes)
| Category | Threat |
|---|
| Spoofing | An attacker with access to the Kubernetes API and the enclave’s service account could create pods that impersonate agents. |
| Tampering | The bootstrap token is passed as a plaintext environment variable in the pod spec. Anyone with get pods permission in the namespace can read it. |
| Repudiation | Kubernetes audit logging (if enabled) records pod creation. |
| Information Disclosure | Bootstrap tokens in pod env vars. NetworkPolicy blocks private ranges but not link-local addresses (169.254.0.0/16) - cloud metadata services may be reachable. |
| Denial of Service | No resource limits are set on agent pods by default - a single agent could exhaust node resources. An attacker who can trigger agent creation could exhaust namespace or pod quotas. |
| Elevation of Privilege | Every agent pod runs in its own kata-containers VM (kata-clh) - mandatory for all agents, not an opt-in - so a container escape is bounded to that one agent’s VM rather than the node. That VM is the only containment boundary: the in-pod context is deliberately loose (image-default/root UID, image-default capabilities, Unconfined seccomp, allowPrivilegeEscalation: true, plus a root dvxir sidecar holding CAP_SYS_ADMIN, CAP_SYS_PTRACE, CAP_SYS_CHROOT, and CAP_AUDIT_WRITE for nsenter SSH). There is no in-pod hardening layer behind it, and the loosening is not free even for the boundary we do rely on: capabilities and seccomp also bound the guest-kernel syscall surface that a guest-to-VMM escape would chain through, so Unconfined everywhere maximizes exposure to that single boundary. automountServiceAccountToken: false still blocks the Kubernetes API from every container in the pod. Agent namespaces run PSA Privileged fleet-wide, which knowingly gives up Baseline’s admission-level blocks on hostPath, hostNetwork, and hostPID. Kata does not subsume those: a hostPath is bridged into the guest over virtiofs, so a host mount crosses the VM boundary by design - a provisioning-spec regression that added one would expose host files to the agent despite kata. (Host namespace sharing is at best degraded under a VM runtime, but nothing now rejects a spec that asks for hostNetwork/hostPID.) Today the only thing preventing that is that the pod spec is entirely backend-controlled (no operator input); the compensating control named by ADR 046, and not yet implemented, is a narrow validating admission policy forbidding host mounts and host namespaces on agent namespaces. |
DF-11: Operator API (Agent CRUD)
| Category | Threat |
|---|
| Spoofing | An attacker with a stolen or forged IAM JWT can perform operations as that operator. |
| Tampering | An authenticated operator can modify any agent in their organization - there is no role-based restriction on who can delete, suspend, or revoke. |
| Repudiation | Operations are not individually audit-logged at the application level (only database-level timestamps on mutations). |
| Information Disclosure | Agent list reveals all agents, their status, enclave assignments, and last-heard-from timestamps for the organization. |
| Denial of Service | No rate limiting - an authenticated operator could mass-create or mass-delete agents. List endpoints return all records without pagination. |
| Elevation of Privilege | Open - no owner tracked here. JWT claims include Roles and Permissions fields, but no handler checks them - any authenticated user in the organization has full access to all operations (create, delete, suspend, revoke). Queries are scoped by organization_id from the JWT, preventing cross-tenant access, but there is no intra-org restriction at all. Same gap repeated in DF-12 and DF-13. |
DF-12: Operator API (Enclave Management)
| Category | Threat |
|---|
| Spoofing | Same as DF-11 - stolen IAM JWT. |
| Tampering | An operator can delete an enclave that has active agents assigned to it, orphaning those agents (no pre-deletion check). Token rotation invalidates the previous key immediately. |
| Repudiation | Enclave creation and token rotation return plaintext keys - no audit record of who received the key. |
| Information Disclosure | Enclave API key is returned in plaintext once. If the response is logged or intercepted, the key is compromised. Enclave list reveals infrastructure topology (names, status, last_seen_at). |
| Denial of Service | Deleting an enclave orphans all its agents. Rotating a key immediately disconnects the enclave from SSE. |
| Elevation of Privilege | Open - same gap as DF-11. Any org member can manage enclaves, including deleting one out from under active agents. |
DF-13: Operator API (Secrets Management)
| Category | Threat |
|---|
| Spoofing | Same as DF-11 - stolen IAM JWT. |
| Tampering | An authenticated operator can overwrite any agent’s secrets. There is no confirmation step or version control on secret values. |
| Repudiation | Secret writes update timestamps but do not log who wrote the value or what the previous value was. |
| Information Disclosure | Secret values are accepted in plaintext over the wire (HTTPS), encrypted before storage. The list endpoint returns metadata only (no values). However, the copyable field reveals whether a secret’s plaintext was retained for display. |
| Denial of Service | No limit on the number or size of secrets per agent. |
| Elevation of Privilege | Open - same gap as DF-11. Any org member can create or overwrite secrets for any agent in the org. |
DF-14: Broker-Local JTI Blacklist (Kill Switch)
| Category | Threat |
|---|
| Spoofing | Not applicable - the broker connects to its own Redis with a fixed connection string; there is no per-request identity to spoof. |
| Tampering | An attacker with write access to the broker’s Redis could delete a blacklisted JTI, silently un-revoking an isolated agent’s still-valid JWT for the remainder of its ≤5-minute lifetime. |
| Repudiation | The blacklist add/remove itself is not independently logged; only the control-plane audit_log row for the operator’s isolate/restore action is recorded (internal/operator/service_killswitch.go) - there is no enclave-side record of the Redis mutation succeeding. |
| Information Disclosure | JTIs are opaque random identifiers; blacklist membership discloses only that a specific token was revoked, not any secret or claim content. |
| Denial of Service | Open. If blacklist checking is required (WithBlacklist) and Redis becomes unreachable, JWT validation fails closed for every agent on that broker (ErrBlacklistRequired/check error) - a Redis outage denies service platform-wide, not just to isolated agents. Conversely, if a deployment opts out (WithoutBlacklist), an isolated agent’s JWT keeps working until it naturally expires (≤5 min), so isolation is not truly instant without Redis. |
| Elevation of Privilege | Not applicable - the blacklist can only revoke a token, never mint or upgrade one. |
DF-15: Control Plane / IAM Redis (Sessions, Device Flow, Rate Limiting)
| Category | Threat |
|---|
| Spoofing | An attacker with Redis write access could forge a session blob or device-code entry and potentially authenticate as an arbitrary account without going through login. |
| Tampering | Open. Session storage and account-wide revocation are written by two separate codebases (IAM’s OAuth service and the control plane’s operator service) that must agree on the exact same key format by convention (design doc 036) with no shared runtime check - a key-format drift introduced in either writer would silently break account-session revocation (a suspended account’s sessions would not actually revoke). |
| Repudiation | Session and device-code reads/writes are not individually logged. |
| Information Disclosure | Session and device-code values are effectively bearer credentials; an attacker with Redis read access could hijack an active SSO session or complete a pending device authorization before the legitimate user does. |
| Denial of Service | Mitigated, favoring availability. The login rate limiter fails open on a Redis error (return true, err // Fail open - allow request if Redis is down in internal/iam/ratelimit.go) - a Redis outage silently disables login rate limiting rather than blocking logins, which is a deliberate availability-over-strictness tradeoff worth confirming is still the intended default. |
| Elevation of Privilege | Not applicable - this store holds session/rate-limit state, not authorization decisions. |
DF-16: Event Publishing and Purging
| Category | Threat |
|---|
| Spoofing | Events are created by the control plane’s service layer - no external input. A database attacker could insert fake events. |
| Tampering | A database attacker could modify event payloads (agent_id, org_id) or manipulate the enclave’s cursor to cause events to be replayed or skipped. |
| Repudiation | Events are append-only by convention. No integrity protection (signing, HMAC) on event payloads. |
| Information Disclosure | Events contain agent_id, agent_name, and organization_id - visible to anyone with database read access. |
| Denial of Service | Events accumulate if enclaves don’t process them. The purge job only removes consumed events. A disconnected enclave’s events grow without bound. |
| Elevation of Privilege | Fake events could trigger provisioning of containers - but the provisioner validates agent state (must be “created” or “provisioning”) before generating a bootstrap token. |
DF-17: Telegram Integration
| Category | Threat |
|---|
| Spoofing | An attacker with a valid Telegram bot token could register it before the legitimate operator, hijacking the bot’s identity. The getMe validation only checks that the token is valid, not that the operator owns the bot. |
| Tampering | The bot token is encrypted at rest but delivered in plaintext to the agent during bootstrap/renewal. A compromised agent has full access to the Telegram bot. |
| Repudiation | Integration creation is recorded in the database with timestamps but no audit trail of who created it. |
| Information Disclosure | The Telegram bot token grants full control of the bot (read messages, send messages, access chat members). This is a high-value credential delivered alongside all other secrets. |
| Denial of Service | Deleting the integration deletes the encrypted secret - the agent loses access on next renewal. |
| Elevation of Privilege | The Telegram bot token is scoped to a single bot - it cannot be used to access other bots or the operator’s Telegram account. |
DF-18: Execution-Time Policy Evaluation
- Boundary: B2 (Agent → LLM Proxy)
- Trigger: Agent’s LLM response contains
tool_use blocks - Auth: Platform JWT or netproxy-forwarded service-signed identity headers
- Data: LLM proxy inspects tool calls against execution-time policies (3-layer architecture: inspectors → policy engine → enforcers). Policy decisions are logged to
policy_decisions. Actions requiring approval enter the approval workflow; denied actions are returned as tool_result(is_error).
| Category | Threat |
|---|
| Spoofing | Tool name spoofing: A compromised agent could rename tools to evade LLM-level policies (e.g., rename stripe.create_refund to process_payment). LLM-level policies match on agent-defined names, so this is a real risk. Mitigations: network-level policies as backstop (destination matching doesn’t depend on tool names), tool catalog with “expected tools” alerting, and tool definition change detection between requests. Approval spoofing: Approval resolution requires an authenticated IAM session; the resolved_by field is set from the caller’s verified identity. LLM Judge resolvers authenticate via platform service credentials with resolved_by set to the judge’s config ID. |
| Tampering | Policy version drift: If a policy is updated while an approval is pending, the wrong version could apply. Mitigation: policy decisions are logged with a snapshot of the policy at evaluation time (policy_version field). TOCTOU between evaluation and execution: The proxy evaluates tool_use blocks from the LLM response, but the agent may modify parameters before actual execution. LLM-layer policies constrain the LLM’s output; network-layer policies are the binding enforcement point. For high-risk operations, operators should define policies at both layers. |
| Repudiation | All policy decisions (allow, deny, require_approval) are logged to policy_decisions with the full evaluation context. Approval resolutions record resolved_by, resolver_type, and timestamp. |
| Information Disclosure | Denial messages default to a generic “action denied by policy” with no threshold values, policy names, or rule details exposed. Operators can opt into custom denial_message fields when they want to help well-behaved agents self-correct. LLM Judge resolver manipulation: A compromised agent could craft tool inputs or conversation content to manipulate an LLM Judge resolver into auto-approving a dangerous action (prompt injection via request_context). Mitigations: request_context sanitization with structured envelopes and clear delimiters, LLM Judge resolvers use dedicated service accounts, operators can disable LLM Judge tiers for high-risk policies via "human_only": true, and all auto-approvals are logged with resolver_type: "llm_judge" for separate audit review. |
| Denial of Service | Approval timeout flooding: An attacker could flood the approval queue to overwhelm human reviewers, causing legitimate requests to time out or exhaust escalation tiers. Mitigations: max pending approval requests per agent (default: 10), org-wide pending approval limit (default: 100), per-agent rate limit on approval request creation (max 50/hour). When any limit is exceeded, require_approval verdicts become deny with reason “approval rate limit exceeded.” |
| Elevation of Privilege | Policies are stored in the database and only modifiable by authenticated operators with org:manage permission. Agents cannot read or modify their own policies through the enclave API - the enclave only delivers the action result, never the policy definition. |
DF-19: Broker URL Announce & Discovery
| Category | Threat |
|---|
| Spoofing | A broker can only announce its own URL: the record is keyed by the broker_id in the authenticated broker JWT, never a client-supplied id, so one broker cannot set another’s URL. |
| Tampering | The announced URL is recorded trust-on-first-use and is immutable via announce thereafter - a second announce with a different URL is ignored and logged. Repointing requires either the authoritative operator PATCH /api/v1/brokers/{id} (RBAC org:manage, org-scoped) or direct database access. The residual window is between broker registration and the first legitimate announce (classic TOFU race); a mismatch alert fires when the real broker later announces a different URL. |
| Repudiation | First-announce pins and ignored-mismatch announces are logged with broker_id and the URLs involved. |
| Information Disclosure | The broker_url is returned only on org-scoped operator agent reads (B5); it reveals the broker’s network address to operators already entitled to use it. |
| Denial of Service | A broker that never announces simply leaves broker_url empty; the CLI then falls back to requiring --broker-url, so a missing/failed announce degrades to the prior behaviour rather than blocking operations. |
| Elevation of Privilege | The discovered URL is where the CLI sends the operator’s IAM JWT for SSH-cert/terminal/tunnel, so a compromised broker that poisons its own URL before first legitimate announce could harvest an operator credential broader than its enclave. Mitigations: TOFU pin + mismatch logging, authoritative operator PATCH override, the always-available --broker-url flag, and mandatory TLS verification of the broker endpoint. |