v0.2.1 — security hardening
Security hardening release. No behavior changes to /route or /compare for legitimate clients; all changes are defense-in-depth.
Changes
Cloud Run — dedicated minimum-privilege SA
The runtime SA is now agent-router-runtime@research-agent-498415.iam.gserviceaccount.com with only roles/secretmanager.secretAccessor on the openai-api-key secret. The previous default Compute Engine SA had project-wide roles/editor — if the container had been compromised, an attacker could read/modify every Secret Manager secret, modify other Cloud Run services, etc.
Sanitized /compare per-row error messages
backend/app/compare.py no longer echoes f"{type(e).__name__}: {e}" to the client. Errors collapse to a fixed allowlist:
FileNotFoundError→"router artifact not available"ProviderUnavailableError→"upstream provider not configured"RateLimitError→"upstream provider rate limited"APIConnectionError→"upstream provider temporarily unreachable"- (etc., with
"router unavailable"as the fallback)
Full exceptions are still logged server-side via logger.warning for debugging.
CORS — no more wildcard with credentials
CORS_ALLOW_ORIGINS=* together with allow_credentials=True was a CORS spec violation. Now: explicit allowlist in production (https://agent-router-five.vercel.app, http://localhost:5173), and the combo * + credentials is auto-corrected (credentials disabled).
Body-size middleware
MAX_BODY_BYTES env var (default 10000) returns 413 payload too large based on Content-Length before parsing. Pydantic's max_length=2000 validation no longer pays the cost of buffering 32 MB of junk.
Security headers on every response
X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: no-referrerStrict-Transport-Security: max-age=31536000; includeSubDomainsCross-Origin-Resource-Policy: same-site
/docs, /redoc, /openapi.json hidden in production
Gated by ENABLE_API_DOCS (default true for local dev). The Cloud Run service sets it to false. Local dev keeps the interactive Swagger UI.
Non-root container
The image now creates a system user app (UID 1000) and switches via USER app after install. Limits the blast radius of any container escape.
Frontend: npm audit clean
Bumped vite 5 → 8 and @vitejs/plugin-react 4.3 → 6.0. The previous moderate-severity advisory (GHSA-67mh-4wv8-2f99, esbuild dev server) is resolved; npm audit reports 0 vulnerabilities. Production bundle size is unchanged.
New tests
test_security_headers_present_on_every_responsetest_body_size_limit_returns_413test_body_size_limit_with_malformed_content_length_returns_400test_compare_one_router_failure_does_not_break_response(updated to pin the sanitized error string)
22 backend tests total (was 19). 65 across the workspace.
Live
| URL | |
|---|---|
| Frontend | https://agent-router-five.vercel.app |
| Backend | https://agent-router-909428365094.us-central1.run.app |
Verifying the live hardening
URL=https://agent-router-909428365094.us-central1.run.app
# /docs hidden
curl -sI $URL/docs # → HTTP/2 404
curl -sI $URL/openapi.json # → HTTP/2 404
# Security headers present
curl -sI $URL/ | grep -iE 'nosniff|strict-transport|x-frame|referrer|cross-origin'
# CORS allows Vercel
curl -sI -X OPTIONS $URL/route \
-H 'Origin: https://agent-router-five.vercel.app' \
-H 'Access-Control-Request-Method: POST' | grep -i access-control-allow-origin
# CORS blocks unknown origin (no Allow-Origin in response)
curl -sI -X OPTIONS $URL/route \
-H 'Origin: https://evil.example.com' \
-H 'Access-Control-Request-Method: POST' | grep -i access-control-allow-origin || echo "blocked"
# 413 on oversized body
python3 -c 'import json; print(json.dumps({"input":"x"*15000}))' \
| curl -s -X POST $URL/route -H 'Content-Type: application/json' --data-binary @- -w '\nHTTP %{http_code}\n'