Threat Model & Known Exposures
Status: unverified (point-in-time)
Threat classes and mitigations below are grounded in the code and in internal security reviews (July–September 2026, not published). "Open" or "fixed" describes the code at the time of writing, not any particular deployment; confirm against your own running system before relying on any row. This page deliberately records classes and statuses, not exploit steps. Report new vulnerabilities as described in SECURITY.md.
- Audience
- Engineers and operators deploying or contributing to the platform
- Related
- Security Architecture, Secrets, Compliance Posture, Observability
1. Assets
- Tenant data in per-tenant Postgres schemas (conversations, knowledge, memories, agent definitions).
- Tenant-supplied credentials (LLM provider keys, channel tokens, OAuth refresh tokens) encrypted under
APP_ENCRYPTION_KEY; the key itself and wherever it is stored (deploy/compose/.env, a Kubernetes Secret). - Identity: the Keycloak realm, its signing keys, the
tenant_idclaim that selects a schema. - Money paths: LLM spend on platform-pool keys; for operators who enable billing, payment webhooks and plan entitlements.
- Availability: the single tenant database and the core API (do not power-cycle a live stack to diagnose it; see Rollback & Recovery).
2. Trust boundaries
flowchart TB
subgraph Internet
A1[Authenticated tenant user]
A2[Anonymous: demo, share link, webhook sender, paired device]
A3[Hostile MCP server or web page content]
end
subgraph Edge
CF[Caddy / Ingress / optional CDN]
end
subgraph Runtime[Compose or Kubernetes]
CORE[core: JWT + tenant context]
KC[Keycloak]
SBX[mcp-stdio-runner / tool-sandbox]
end
subgraph Data
PG[(Postgres schema per tenant)]
KV[(.env / Kubernetes Secret)]
end
A1 --> CF --> CORE
A2 --> CF
CORE --> PG
KV -. env .-> CORE
CORE --> SBX
CORE -->|outbound fetch, MCP connect| A3
A1 --> KC
Boundaries that matter most: internet to core (JWT, or a per-surface credential for anonymous paths); tenant to tenant (schema selection from a claim); core to sandbox (untrusted code); core to outbound network (SSRF); operator or automation tooling to the host or cluster.
3. Threat classes and mitigations in place
| Threat class | Mitigation in code or config | Source |
|---|---|---|
| Forged or foreign-client tokens | Signature via JWKS; issuer set; azp allowlist. A live DAST confirmed alg:none, algorithm-confusion and bad-signature tokens rejected. | SecurityConfig.java; internal DAST review |
| Cross-tenant access | Schema-per-tenant from validated claim; claim-less tokens 403; sanitized schema names; no caller-supplied tenant ids | TenantContextFilter.java, SchemaNameSanitizer.java, internal SAST review |
| Privilege misuse within a tenant | PERM_* matchers; denied attempts audited | SecurityConfig.java |
| Anonymous-surface abuse | Demo tokens, per-IP limits and caps; share-link rate limits; signup limiter; device-claim limiter; Redis limiters with local fallback | SecurityConfig.java, LocalRateLimitFallback.java |
| Webhook spoofing and replay | Provider signatures, constant-time compare, Slack replay window, billing dedupe ledger | Webhook controllers; V250__billing_webhook_event.sql |
| Untrusted code execution | Interpreter allowlist plus inline-eval flag rejection; separate sandbox container with seccomp, cap-drop, no secrets | McpStdioCommandGuard.java; sandboxing design §8-§9 |
| SSRF from tenant-supplied URLs | Denylist, IPv6-mapped decode, trailing-dot normalization, resolve-time re-check (checkResolved); guards on demo/OpenAPI fetchers | McpRemoteUrlGuard.java |
| Prompt injection via tool output | Guardrail seam on every tool result; MCP output sanitizer strips invisible/bidi/tag characters. Described in source as partial, not a solution. | GuardrailService.java |
| Credential theft at rest | AES-256-GCM, fail-fast key validation, dual-key rotation | EncryptionService.java |
| Public metrics/actuator disclosure | ActuatorEdgeGuardFilter 404s forwarded requests to non-public actuator paths | ActuatorEdgeGuardFilter.java |
| Operator or automation power-cycling a live deployment | Procedural rule (no host reset, stop or delete without an explicit human decision); give CI and automated agents a least-privilege identity without destructive permissions | Deployment Guide |
4. Known exposures and open items
Status vocabulary: open = not shown fixed in the code; fixed in code = the code change is visible; deployment = depends on how you deploy, not on the code.
| # | Class | Status | Recorded in |
|---|---|---|---|
| 1 | Public operational endpoints. Core 404s proxied requests to non-public actuator paths (ActuatorEdgeGuardFilter), so metrics stay private as long as core's port is only reached through a proxy. The Compose Caddyfile 404s the Keycloak admin console and master realm on auth.; with another edge (Helm Ingress, CDN) you must do the same. An edge rule for /actuator/* (exempting /actuator/health) is defence in depth. | guard fixed in code; edge rules are deployment | Observability; deploy/caddy/Caddyfile; ActuatorEdgeGuardFilter.java |
| 2 | Keycloak run mode. Compose runs start --import-realm by default; KEYCLOAK_START_COMMAND=start-dev exists for local use and must not be used on a server. | deployment | deploy/compose/docker-compose.yml |
| 3 | Tenant-claim integrity. If the realm's unmanaged-attribute policy is ENABLED, a user can edit their own tenant_id. It must be ADMIN_EDIT; the realm reconcile (deploy/keycloak/reconcile-keycloak.sh, run by scripts/vegaduta up; the Helm reconcile Job) sets it, but a live realm must be checked. | deployment; high priority to verify | Keycloak Provisioning |
| 4 | Missing tenant context falls back to the shared public schema (app.tenancy.fail-on-missing-context defaults false; the code comment says it stays false until a soak proves the counter empty). | open by design; flip pending a soak with an empty counter | core/tenancy/.../TenantSchemaResolver.java |
| 5 | Anonymous-by-prefix routes. Path prefixes such as /api/demo/** are open by construction; any new endpoint under them is unauthenticated. Two permitAll() matchers (/scim/v2/**, /api/tenant/comp-grant) rely on controller-level checks I did not trace. | design risk; two items untraced | SecurityConfig.java |
| 6 | MCP remote connect: DNS rebinding and numeric-IP encodings were the top remaining remote-side risk after the first round of fixes; a resolve-time check is now present in the guard, its pinning completeness is not re-verified. Redirect re-guarding, pre-buffer result-size cap, static-tool sanitization, readiness-cache window, legacy env leak on SDK downgrade, and tool-name shadowing are follow-ups. | partly fixed in code; remainder open | McpRemoteUrlGuard.java |
| 7 | Untrusted-tenant STDIO MCP. Running published packages via allowed launchers is by design; the safe posture is trusted, staff-reviewed registration only. The isolation container exists and has been verified on a dev host; the feature ships flag-off. | open posture constraint | docs/architecture/MCP-STDIO-SANDBOXING-DESIGN.md §8-§9; ADR-0009 |
| 8 | Code-intel execution host isolation. A data-governance review found directory-per-tenant separation but commands run as host processes without a container mode implemented. | open as of that review | services/code-intel/ |
| 9 | Browser security headers on front-end origins. A live DAST once found none on the console origins (only the API had them); next.config now sets headers and the Compose Caddyfile adds HSTS, nosniff and a referrer policy. | re-check your deployment | web/next.config.*, web-admin/next.config.* |
| 10 | Weak database password fallback. Config comments now say the fallback was removed for production paths. | appears fixed in code | application.yml comments |
| 11 | Secrets posture. Secrets sit in a plaintext deploy/compose/.env on Compose hosts; envelope/KMS encryption is not wired; no rotation schedule. | open | Secrets |
| 12 | Encryption-key drift. A regenerated or mistyped APP_ENCRYPTION_KEY (new host, restore, second stack on the same database) makes stored credentials undecryptable. | procedural risk (deployment) | ADR-0010 |
| 13 | Security scanning. CodeQL runs on pull requests, pushes to main and weekly; static gates, gitleaks and dependency review run inside CI; OpenSSF Scorecard weekly. | fixed in CI | .github/workflows/ci.yml, codeql.yml, scorecard.yml |
| 14 | Operator identity not separated from automation identity. Give CI and automated agents an identity without destructive permissions on your hosts or cluster. | deployment | — |
| 15 | Edge configuration (HTTPS enforcement, minimum TLS, WAF and rate-limit rules, email DMARC policy) and plaintext service-to-service traffic inside the Compose networks or cluster must be considered per deployment. | deployment | deploy/caddy/Caddyfile, Helm Ingress values |
| 16 | No dedicated incident-response plan, vendor review or DR drill ships with the project. | open | Compliance Posture §3 |
How to keep this page honest
When an item is closed, change its status and cite the commit, not the intent. When a new review lands, add it to the evidence list and its date to the row. Prefer editing a row over deleting it, so the history of what was once open stays readable.
Evidence & references
core/security/src/main/java/com/agenticai/core/security/SecurityConfig.java,ActuatorEdgeGuardFilter.java,TenantContextFilter.java.core/tenancy/src/main/java/com/agenticai/core/tenancy/TenantSchemaResolver.java.docs/architecture/MCP-STDIO-SANDBOXING-DESIGN.md,core/mcp/.../McpRemoteUrlGuard.java,McpStdioCommandGuard.java.- Internal security reviews (MCP pentest, core SAST, live DAST, data-governance review; July 2026, not published).
.github/workflows/codeql.yml,static-gates.yml.deploy/caddy/Caddyfile,deploy/compose/docker-compose.yml.