AKOS security review (self-review, 2026-11-01)
Scope: 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).
| Area | Status | Evidence / notes |
|---|---|---|
| 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) |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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) |
| Audit integrity | hash-chained events, DB trigger forbids UPDATE/DELETE, GET /audit/verify |
test_audit_chain_detects_tampering |
| 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) |
| Path traversal | AKP file names validated, artifacts addressed by sha256/uuid, docs names alphanumeric | test_path_traversal_and_validation |
| Idempotency | idempotency_key unique on executions and jobs; replay returns the stored execution |
test_policy_modes_and_provenance |
| Dependencies | pinned (uv.lock, pnpm-lock.yaml), uv sync --frozen in deploy hook |
manifest hooks |
Residual risks and exceptions
- 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 labelledunobservedand excluded from verified exports. - 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. - Single application DB role can in principle modify data tables (not audit rows). A separate read-only role for exports is a possible hardening.
- In-process rate limiting resets on restart and is per process (one process today).
- TypeScript client typecheck depends on
tscfromweb/node_modules; blocked (not failed) when absent. - pg_trgm/FTS search is lexical; no embedding model is used (deliberate: no external calls, deterministic).