Yabanin Security Audit
Dependency CVE Scan
Tool: govulncheck (Go vulnerability scanner)
Date: 2026-07-23
Result: 0 vulnerabilities in reachable code.
| Vulnerability | Package | Status | Action |
|---|---|---|---|
| GO-2026-4316 | go-chi/chi/v5 v5.2.3 | Fixed | Upgraded to v5.2.4 |
| GO-2026-5932 | golang.org/x/crypto/openpgp | Not called | Package not imported in codebase; transitive only |
Scan Command
govulncheck ./...
Threat Model
Attack Surface
| Component | Exposure | Attack Vectors |
|---|---|---|
| Gateway (:8082) | Public internet | API key brute-force, prompt injection, SSRF via upstream URLs |
| Admin (:8086) | Internal only | Session hijacking, CSRF, privilege escalation |
| Dashboard (:3000) | Internal only | XSS, auth bypass |
| PostgreSQL (:5432) | Internal network | SQL injection, connection exhaustion |
| Redis (:6379) | Internal network | Cache poisoning, key enumeration |
| Classifier (:8080) | Internal only | Model abuse, information leakage |
| Admin API | Internal only | IDOR, mass assignment, audit log tampering |
Security Controls
Authentication & Authorization
- API Keys: SHA-256 hashed, stored in
api_keystable, resolved via Redis cache - Admin sessions: HMAC-signed cookies with rotation support
- Root-only routes: Admin operations (org management, cache flush, pricing) require
is_root=true - Team isolation: API keys bound to teams; whitelist enforcement per key/team
Input Validation
- Request validation:
pkg/validation/openai/validates all incoming requests - SQL injection: All queries use parameterized
pgx(no string concatenation) - SSRF protection:
pkg/ssrf/validates upstream URLs against allowlist - Prompt injection: inherent to LLM traffic; since the OUTPLAY program (2026-10) mitigable with opt-in controls — deterministic injection detectors, guard modes, and policy-pipeline steps (see security-owasp-llm.md, LLM01). Without those controls enabled the original posture ("not mitigated") still applies.
Data Protection
- PII redaction: Redis publisher redacts API keys, tokens, secrets from event payloads (
redactPII) - URL credential stripping:
redactSensitiveURLremoves credentials from logged upstream URLs - Encryption at rest: Provider API keys encrypted with AES-256-GCM (
pkg/encryption/) - Secrets management: Docker secrets (
/run/secrets/), not environment variables
Network Security
- Read-only containers: All services run with
read_only: truefilesystem - No new privileges:
security_opt: no-new-privileges:trueon all containers - Internal network: Services communicate via
app-internalbridge network - tmpfs only: Write paths limited to
/tmpin read-only containers
Rate Limiting
- Gateway: Per-IP rate limiting via token bucket
- Admin login: 5 attempts per 15 minutes (
loginLimiter) - Admin API: 1000 requests per minute (
rateLimiter)
Audit Trail
- Audit logs: All admin mutations logged with actor, IP, action, resource
- Request logs: Every API request persisted with org/team/cost/model metadata
- Stream persistence: Redis stream provides durable event trail before DB write
Budget Enforcement (agent budgets)
- Hard limit: Requests return HTTP 429 when the team (monthly) or agent (daily) budget is exceeded
- Soft limit: Logged but allowed
- Fail-closed: A budget-store error denies the request (503) — spend is never allowed while the budget store is unavailable
Key Revocation Propagation
Guarantee (updated): with Redis configured (the admin service shares the
gateway's Redis), a revoked api_keys / agent_keys credential is rejected
immediately — every revocation path (deactivate, delete, rotate, agent
revoke, budget removal) purges org:resolve:<sha256(key_hash)> from the
shared cache, so the next resolve hits the DB and denies. Without Redis the
bound degrades to YABANIN_ORG_RESOLVER_TTL seconds (default 300s).
- The gateway's org resolver (
pkg/auth) caches successful resolve results in Redis, keyed bysha256(plaintext), forOrgResolverTTLseconds. The admin service now shares that Redis and purges the cached entry on every revocation path (fail-soft: a purge failure logs a warning and the TTL bound applies). When Redis is absent the TTL is the propagation bound. - The bound is therefore exact:
stale window ≤ OrgResolverTTL. The fake-clock tests inpkg/auth/resolver_revocation_test.gopin both edges — a revoked key still resolves at TTL−1s and is denied at TTL, for both the api-key and agent-key paths, and the TTL passed to Redis equals the configured bound (tightening the env var tightens the bound). - Mitigation for a faster kill: revoke and flush the matching
org:resolve:<sha256>Redis key (requires shared Redis access); otherwise plan incident response around the configured TTL. SetYABANIN_ORG_RESOLVER_TTLlow (e.g. 30) for deployments with high key churn or strict revocation SLAs, at the cost of one extra DB pair-read per key per window. - Cache entries are HMAC-signed (
CacheSigner), so an attacker with Redis access cannot forge a valid resolve result for a key that was never issued; they can only keep an already-issued identity alive within the same bound. - Note: entries written before the key-revocation fix used a 3-field agent encoding whose cached reads misattributed the agent id to the team slot; they age out within one TTL (≤300s) and are rewritten in the correct 4-field format.
Pre-Launch Security Checklist
- Dependency scan: 0 reachable vulnerabilities
- All containers run read-only with no-new-privileges
- Secrets stored as Docker secrets (not env vars)
- All SQL queries use parameterized statements
- Upstream URLs validated against SSRF patterns
- PII redacted from event stream and logs
- Provider API keys encrypted at rest
- Admin routes protected with root-only middleware
- Rate limiting on gateway and admin login
- Audit logging on all admin mutations
- TLS termination at load balancer (configure before launch)
- CORS policy for dashboard (restrict to known origins)
- Secret rotation procedure documented (see runbook)
Recommendations
- TLS: Terminate TLS at the load balancer (nginx/ALB). Gateway and admin listen on plain HTTP behind the LB.
- CORS: Add
Access-Control-Allow-Originheader restricted to the dashboard domain. - Secret rotation: Rotate
session_key,cache_hmac, andpg_uriquarterly. - WAF: Consider Cloudflare/AWS WAF for DDoS protection before the gateway.
- Pen test: Schedule external penetration test before SOC2 compliance work.
2026-10 OUTPLAY Program — Threat-Model Addendum
Date: 2026-10-08 (lane OP-24). Scope: the Policy Pipeline (YPP) and
the MCP credential/aggregator surfaces delivered by the OUTPLAY program
(docs/plans/OUTPLAY-PLAN-2026-10-06.md, internal). This
addendum records the new threat models and compensating controls; the
sections above remain valid. The full per-risk mapping (OWASP Top-10 for LLM
Applications → controls, with honest coverage boundaries) lives in
security-owasp-llm.md.
Pipeline threat model (pkg/pipeline, pkg/policystore)
The pipeline is a programmable inspection/mutation conveyor over gateway traffic (pipeline.md). Its own attack surface, and the controls:
| Surface | Threat | Control |
|---|---|---|
Condition engines (pkg/pipeline/script: expr default, lua, starlark) | a policy condition becomes a DoS/escape vector | conditions are compiled once at policy load (invalid source = load error, never a silent skip); expr is loop-free by design; lua runs sandboxed with an instruction budget; per-step timeout bounds every engine; condition errors follow on_error (default fail-closed). Policies are admin-authored and git-reconciled — not client input |
Callout steps (pkg/pipeline/callout) | SSRF via scanner URLs; data exfiltration through projections | steps compile only with YABANIN_POLICY_CALLOUT_ALLOW_HOSTS set (empty = fail-closed); SSRF guard pins DNS answers and rejects reserved addresses at dial time (rebinding window closed); no redirects, no retries, per-URL circuit breaker, per-step timeout; expose projections bound what leaves (metadata never leaves); max_response_bytes caps replies |
LLM judge (pkg/pipeline/llmjudge) | pipeline recursion (judge calls the gateway, re-entering the pipeline, amplifying cost) | hard exclusion: gateway-made judge calls carry judge=true meta and X-Yabanin-Judge: true; internal calls are exempt, while any inbound request presenting that header (any value, empty or duplicated included) is rejected with 400 judge_reentry_unsupported after auth (pipeline.md §6.2); judge prompts are versioned resources; temperature pinned to 0; omitted confidence counts as 1.0 (fail-closed) |
| Verdict composition | a buggy step smuggles an allow past a deny | fixed strictness scale deny > mask > flag > allow; union across attached policies with most-restrictive verdict; unknown actions rank as deny; first deny short-circuits |
| Streaming mutation | torn writes leak pre-substitution values across chunk boundaries | prefix gate (64 tokens / 500 ms) denies before release; chunk-carry holds partial matches; transform runs before the cache writer (pipeline.md §7) |
| Decision log | sensitive content persisting via findings | evidence stored hashed; quoted user content persists only in shadow/debug retention; 90-day retention (migration 0009) |
MCP credentials threat model (pkg/mcpagg, migration 0007)
Summarized from mcp-gateway.md §6, §8 (R4) — the R4
posture as shipped: secrets exist only as enc:-encrypted envelopes
(fail-closed writes), under org-isolation RLS; reads return masked rows only
(secret_last4/has_secret); every CRUD/status step lands in the
hash-chained audit journal; plaintext bounded (8 KiB) and never logged. A
credential can never widen egress: the SSRF allow-list check on the server
URL precedes credential resolution on every invoke. Per-user credential flows
(submit/rotate, OAuth begin/callback) require an enabled grant — the grant is
the ceiling for every flow step.
Posture change vs. the 2026-07 audit
The July audit recorded "prompt injection: not mitigated". The program adds
opt-in mitigations (guard modes, injection detectors, policy steps,
tool-result post-checks) while keeping the default posture unchanged: every
new surface ships off (YABANIN_POLICY_MODE, YABANIN_MCP_GATEWAY,
guard mode all default off/unmounted), and egress knobs default to
fail-closed. An unconfigured deployment behaves as before.