Skip to content

v0.2.1 — security hardening

Choose a tag to compare

@raulmn00 raulmn00 released this 06 Jun 13:49

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: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: no-referrer
  • Strict-Transport-Security: max-age=31536000; includeSubDomains
  • Cross-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_response
  • test_body_size_limit_returns_413
  • test_body_size_limit_with_malformed_content_length_returns_400
  • test_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'