Skip to content

Security: delta9world/Delta-9

SECURITY.md

Delta-9 Security Posture

This document is the source of truth for Delta-9's security model. It combines (a) the threat model + audit findings, (b) the per-agent RBAC matrix that defines who can do what, and (c) the current enforcement state for each control.

Last revised: 2026-05-19. Re-audit cadence: every two months OR after any change that adds an agent, an external dependency, or a new authentication surface.

1. Trust boundaries

                          ┌──────────────────────────────┐
                          │   External (untrusted)       │
                          │   Loyverse / Stripe /        │
                          │   Square / LINE webhooks     │
                          └────────────┬─────────────────┘
                                       │ HMAC-verified payload
                                       ▼
            ┌───────────────────────────────────────────┐
            │  Cluster edge (Traefik + Tailscale ACL)   │
            │  - panel.tailnet                          │
            │  - n8n webhook receiver                   │
            └────────────┬──────────────────────────────┘
                         │ mTLS / IP-allowlist
                         ▼
   ┌────────────────────────────────────────────────────┐
   │  Service plane                                     │
   │  - orchestrator / engines / gates / summarizer     │
   │  - panel / web dashboard                           │
   │  - LiteLLM (chokes ALL outbound LLM)               │
   └────┬─────────────────┬─────────────────┬───────────┘
        │                 │                 │
        ▼                 ▼                 ▼
   ┌─────────┐      ┌──────────┐      ┌───────────┐
   │  Redis  │      │ Postgres │      │  Ollama   │
   │ broker  │      │ (panel + │      │ (local)   │
   │ queues  │      │ pgvector)│      │           │
   └─────────┘      └──────────┘      └───────────┘
        │
        ▼
   ┌──────────────────────────────────────────────┐
   │  Outbound third-party (cost / data exit)     │
   │  Anthropic API · Gemini · Langfuse · LINE    │
   │  (only LiteLLM + n8n + line_notifier reach)  │
   └──────────────────────────────────────────────┘

Trust shrinks as you move down. Every boundary must enforce something:

Boundary Control today Gap
External → Cluster edge HMAC on webhook receiver (Stripe/Square/Loyverse); Tailscale ACL on panel Some sources allow empty secret (signature skipped); see Finding §3.1
Edge → Service plane TOTP+JWT (panel); HTTP basic over LAN (web) None major
Service ↔ Service In-cluster network only; LiteLLM master key No NetworkPolicy (Finding §4.1); orchestrator→engine has no auth header
Service → Redis Connection only No Redis ACL / requirepass (Finding §5.1)
Service → Postgres DB user creds; RLS policies present in DDL SET LOCAL app.current_tenant is never called (Finding §7.1)
Service → External LLM LiteLLM master key + budget caps None major

2. RBAC matrix — what each agent is allowed to do

This is the authoritative permission table. New agents added to the codebase MUST be added here. The detailed per-section breakdown lives in docs/security/RBAC.md; the headline matrix is:

Agent Redis (R/W key prefixes) LLM scope DB External egress
ingest_v2 W t:{tenant}:events, delta9:seen:* Loyverse API
orchestrator R/W delta9:work_queue, delta9:*_queue, t:{tenant}:project:state LiteLLM only Google Chat, web API
engine.js LiteLLM delta9-p0 LiteLLM only
summarizer R delta9:p2p3_summarize LiteLLM delta9-p2p3-summarizer LiteLLM only
audit_gate R delta9:audit_queue, W delta9:audit_dissent LiteLLM cross-validator LiteLLM only
consensus_gate R/W delta9:consensus_*, R/W t:{tenant}:emb:* 3-engine quorum LiteLLM + Ollama
judge_gate R/W delta9:judge_* LiteLLM delta9-judge LiteLLM only
reranker_server none
panel ❌ (Day 1) none RW panel.db none (cluster-local)
webhook-receiver W t:{tenant}:events inbound only
n8n R delta9:*_dissent own SQLite LINE_WEBHOOK_URL
line_notifier LINE_NOTIFY_TOKEN endpoint

The NetworkPolicy in k8s/security/networkpolicies.yaml encodes the egress column; the matching K8s ServiceAccounts in k8s/security/serviceaccounts.yaml provide one identity per service so future RBAC Roles can attach.

3. Audit findings (2026-05-19)

Severity: 🔴 critical · 🟠 high · 🟡 medium · ⚪ info.

3.1 🔴 FERNET_KEY auto-generated in dev fallback

  • Where: delta9_panel/delta9_panel/settings.py::_default_fernet_key
  • Impact: If FERNET_KEY is unset in production, the panel boots with a fresh random key per pod restart. Every encrypted TOTP secret on the panel-data PVC becomes undecryptable. Operators get locked out with no recovery path except backup codes.
  • Fix in this PR: production guard — refuses to boot when is_prod and FERNET_KEY is missing or matches a dev-generated value. See delta9_panel/tests/test_settings_production_guard.py.

3.2 🔴 Redis has no requirepass / ACL

  • Where: k8s/redis-deployment.yaml
  • Impact: Any pod in the cluster reads all queues, the embedding cache, ProjectState, and the dedup set. No tenant isolation at the Redis layer.
  • Fix this PR: staged rollout plan in docs/security/REDIS_ACL_MIGRATION.md. We don't flip the flag in this PR because turning on requirepass mid-flight breaks every running client until they pick up the new env var. Coordinated rollout is the safer path.

3.3 🟠 No NetworkPolicy in cluster

  • Where: k8s/*.yaml
  • Impact: Every pod can reach every other pod and any IP on the internet. No defense if a single workload is compromised.
  • Fix this PR: ships k8s/security/networkpolicies.yaml — default-deny + per-service allow rules matching the egress matrix. Operators apply after labelling their pods (app: <name> selectors must already match — kubectl describe deploy/<name> to verify).

3.4 🟡 Container hardening missing

  • allowPrivilegeEscalation: false, capabilities.drop: ["ALL"], readOnlyRootFilesystem: true are not set on any Deployment.
  • Fix this PR: documented in docs/security/CONTAINER_HARDENING.md with a copy-paste template. Existing Deployments aren't touched here — applied per-service in follow-ups so any rootfs-write regression is testable in isolation.

3.5 🟡 SET LOCAL app.current_tenant is never called — CLOSED

  • Where: every Postgres call in delta9_panel, core/, src/agents/
  • Impact: If RLS policies are active in production, a tenant-scoped query that forgets its tenant_id = filter could leak. If they're not active, the RLS in infra/sql/* is dead code.
  • Fix this PR: closed: The panel now applies set_config('app.current_tenant', ...) via SQLAlchemy's after_begin event (delta9_panel/db/tenant_context.py). Shared data readers like core/pos_cache.py have been updated to check for the postgresql dialect and explicitly run set_config.

3.6 🟡 WEB_API_KEY is header-only

  • Where: web/server.js POST /api/events
  • Impact: A leaked key replays as orchestrator.
  • Fix this PR: documented + rotation guidance in docs/security/RBAC.md. Switching to HMAC-signed bodies is tracked separately.

3.7 🟡 Audit log gaps — PARTIALLY CLOSED

  • /auth/logout (panel + web) no audit rowclosed: both routes (panel /auth/logout, web HTML form, web JSON form) now write a logout.ok audit row when the cookie decodes cleanly. Logout still succeeds for expired/malformed cookies (auditing the failed-decode would just clutter the log with expired-session noise).
  • /console/clear (web) no audit rowclosed: writes a console.clear: <conversationId|all> row alongside the existing console.chat row so the timeline is self-consistent.
  • 🟡 Still open: cross-service state writes (orchestrator, gates, summarizer) emit structured JSON logs but NOT to the panel's audit_log. Plumbing them needs an audit-bus (Redis stream consumed by the panel, or a Postgres audit_log shared across services). Tracked as a follow-up under this same finding.

3.8 🟡 :latest / floating image tags

  • Where: every Delta-9 image; ollama/ollama:latest; LiteLLM main-stable
  • Impact: Supply-chain risk if a registry namespace is compromised or a base image moves unexpectedly.
  • Fix this PR: docs/security/IMAGE_PINNING.md walkthrough.

3.9 🟡 Read-only role can see /api/cron/health (PR #79)

  • Where: PR #79 delta9_panel/api/cron_health.py
  • Impact: Non-admins enumerate CronJob status. Low-impact, but inconsistent with other operator surfaces that are admin-gated.
  • Fix: 1-line change goes into PR #79 (Depends(require_admin)), not this PR — to avoid merge-order coupling.

3.10 ⚪ ProjectState integration unverified

  • Where: core/orchestrator.py + engines when PROJECT_STATE_ENABLED=1
  • Impact: Law #7 mandates both engines read+append. The hook exists; whether it actually runs on every call needs a follow-up audit by code-reading the per-engine paths.

3.11 🔴 TOTP /verify-otp brute-force missing rate limit — CLOSED

  • Where: delta9_panel/delta9_panel/api/auth.py
  • Impact: The /verify-otp endpoint lacked the @limiter.limit decorator present on /login. It also failed to trigger line_notifier.record_login_failure(). An attacker could bypass brute-force protection and rapidly guess 6-digit TOTP codes (~1,000,000 combinations) without alerting operators.
  • Fix this PR: closed: Added @limiter.limit("5/minute") and wired pyotp.verify() failures into the brute-force alert and audit logging systems.

4. What this PR ships

File Type Purpose
SECURITY.md (this file) doc Threat model + findings + RBAC summary
docs/security/RBAC.md doc Detailed per-agent permission matrix
docs/security/CONTAINER_HARDENING.md doc securityContext template
docs/security/IMAGE_PINNING.md doc Digest-pin walkthrough
docs/security/REDIS_ACL_MIGRATION.md doc Staged ACL rollout plan
k8s/security/serviceaccounts.yaml manifest One SA per Delta-9 workload
k8s/security/networkpolicies.yaml manifest Default-deny + per-service allow
delta9_panel/delta9_panel/settings.py code Production FERNET_KEY guard
delta9_panel/tests/test_settings_production_guard.py test Covers the guard
CLAUDE.md doc Link to this file from the panel section

Nothing in this PR changes runtime behaviour on a happy-path production system: it adds policy artifacts (SA, NetworkPolicies) that operators apply explicitly, plus a guard that only fires when production is misconfigured (which is exactly when you want to fail).

5. Reporting a vulnerability

If you believe you've found a security issue in Delta-9, do not open a public GitHub issue. Instead, email the maintainers (or open a private security advisory on the GitHub repository) with:

  1. A minimal reproduction (commands or HTTP requests)
  2. The affected version (commit SHA)
  3. Whether the issue is exploitable from outside the cluster

We aim to acknowledge within 48 hours and ship a fix or mitigation within 7 days for critical-class issues.

There aren't any published security advisories