1# AKOS security review (self-review, 2026-11-01)23Scope: code in `akos/`, deployment on M4M64c behind the MacLustr Tunnel, Claude Code integration. Method: threat model (`docs/threat-model.md`) walked control by control with the automated tests as evidence (`make test`, `make test-node`).45| Area | Status | Evidence / notes |6|---|---|---|7| Transport | TLS terminated by Caddy on BHS64 (auto certs), HSTS set by the app; backend bound to loopback + WireGuard address only (`lsof` on node: `127.0.0.1:8520`, `10.67.0.33:8520`, `127.0.0.1:8521`) | `akos/app.py:serve`, `tests/security/test_ssrf_injection_limits.py::test_path_traversal_and_validation` (headers) |8| Proxy trust | `X-Forwarded-*` honoured only from 127.0.0.1, ::1, 10.67.0.1 | `akos/api/deps.py:client_ip`, uvicorn `forwarded_allow_ips` |9| Authentication | argon2id passwords; HttpOnly/Secure/SameSite=Lax session cookie (12 h); bearer tokens stored as sha256, constant-time compare, revocable, scoped, optionally bound to a project | `test_auth_tenant.py` |10| Authorization | roles viewer/operator/admin; scopes read/execute/discover/export/mcp/admin; project-bound tokens cannot read other projects' executions, bodies, datasets or projects | `test_auth_tenant.py::test_tenant_separation` |11| SSRF | scheme allowlist, blocked hosts (localhost/.local/.internal/metadata), private/link-local/CGNAT/loopback rejection after DNS resolution, IP pinning for the connection, redirects re-validated (max 3), credentials-in-URL rejected | `test_ssrf_blocks_private_and_metadata_targets`, `test_redirect_to_private_is_refused` |12| Secrets | values only in keychain; DB/API/logs/exports/generated code grep-tested for a fake secret; redaction filter on logs and API serialisation; rotation/revocation tested; no secret in git (`.gitignore`, `.env.example`) | `test_credentials_scoped_rotation_revocation_no_leak` |13| Prompt injection via docs | OpenAPI parsed structurally; descriptions stored as strings; codegen from templates; static AST scan refuses eval/exec/subprocess/os.system/file writes/unapproved imports | `test_prompt_injection_in_docs_is_inert`, `test_scan_rejects_dangerous_code` |14| Enforcement boundary | sandbox-exec profile denies outbound network except loopback and denies writes outside the workdir; egress proxy allows only live grants; blocked attempts logged | `tests/security/test_enforcement.py` (node) |15| Audit integrity | hash-chained events, DB trigger forbids UPDATE/DELETE, `GET /audit/verify` | `test_audit_chain_detects_tampering` |16| Rate limiting / exhaustion | per-token token bucket (300/min default), login 20/min per IP, 10 MiB request body cap, 25 MiB response cap, 30 s execution timeout, bounded worker concurrency, per-project/agent daily budgets | `test_rate_limit_token_bucket`, `test_policy_modes_and_provenance` (big response) |17| Path traversal | AKP file names validated, artifacts addressed by sha256/uuid, docs names alphanumeric | `test_path_traversal_and_validation` |18| Idempotency | `idempotency_key` unique on executions and jobs; replay returns the stored execution | `test_policy_modes_and_provenance` |19| Dependencies | pinned (`uv.lock`, `pnpm-lock.yaml`), `uv sync --frozen` in deploy hook | manifest hooks |2021## Residual risks and exceptions221. **Unmanaged routes** (host shell outside sandbox, Claude Code `Bash`, remote MCP servers) are outside the boundary. They are documented, hooks only advise, and anything not mediated is labelled `unobserved` and excluded from verified exports.232. **Keychain trust domain**: anyone with an SSH session as the app user on M4M64c can read the keychain. Same posture as the other apps on the cluster; mitigated by SSH key-only access and audit of credential use. A central vault (e.g. 1Password Connect) would be a drop-in replacement of `akos/vault/keychain.py`.243. **Single application DB role** can in principle modify data tables (not audit rows). A separate read-only role for exports is a possible hardening.254. **In-process rate limiting** resets on restart and is per process (one process today).265. **TypeScript client typecheck** depends on `tsc` from `web/node_modules`; blocked (not failed) when absent.276. **pg_trgm/FTS** search is lexical; no embedding model is used (deliberate: no external calls, deterministic).28