This guide documents the production security controls for Voice-Agent-PuPuPlatter. It is intended for deployment owners who operate the Docker container, rotate provider keys, and verify browser-facing security behavior.
The Express server is the security boundary for provider credentials. Browser clients must never receive raw provider API keys in production. Browser clients may receive provider-scoped connection material only when the provider supports a browser-safe token or signed URL flow.
Protected token and session routes:
| Provider | Route |
|---|---|
| ElevenLabs | GET /api/elevenlabs/signed-url |
| OpenAI | POST /api/openai/session |
| xAI | POST /api/xai/session |
| Ultravox | POST /api/ultravox/call |
| Retell | POST /api/retell/create-web-call |
| Gemini | POST /api/gemini/session |
Set CORS_ORIGIN to one or more exact HTTPS origins:
CORS_ORIGIN=https://voice.example.comMultiple origins are comma-separated:
CORS_ORIGIN=https://voice.example.com,https://admin.example.comProduction rejects unsafe CORS posture:
- Missing
CORS_ORIGINwithNODE_ENV=production CORS_ORIGIN=*- Invalid origin strings
- localhost-only production origins
docker-compose.yml sets ALLOW_LOCALHOST_PRODUCTION_CORS=true for local
production smoke tests only. Remote production deployments must keep this flag
unset or false; docker-compose.deploy.yml forces it to false.
Requests without an Origin header remain allowed for same-origin navigation,
health probes, curl checks, and container health checks. Browser requests with
an unauthorized Origin do not receive an allow-origin header.
The server applies security headers to every response before API and static routes:
Content-Security-PolicyStrict-Transport-Securityin productionX-Frame-Options: DENYX-Content-Type-Options: nosniffReferrer-Policy: no-referrerPermissions-PolicyCross-Origin-Opener-Policy: same-origin
The CSP keeps current voice-provider behavior working by allowing same-origin assets, provider HTTPS and WSS endpoints, Blob/data media sources, and audio workers. Keep CSP changes small and verify each provider tab after changes.
JSON request bodies use an explicit size limit. The default is 128kb; override
only when a deployment has a documented need:
JSON_BODY_LIMIT=128kbProvider routes validate request shapes before external API calls. Validation covers allowed fields, string lengths, integer ranges, object depth, object key counts, and malformed JSON behavior. Validation failures return structured 400 responses and do not call provider APIs.
All /api routes use the broad API limiter:
- 100 requests per 15 minutes per client
Token and session routes use the stricter limiter:
- 10 requests per minute per client
Token and session routes also reject duplicate matching requests while an earlier request is still in flight. This prevents double-click and retry races from creating multiple provider sessions at the same time.
The Gemini Live browser flow uses a server-side GEMINI_API_KEY to mint
short-lived Live API auth tokens. The browser receives only the
auth_tokens/... value from /api/gemini/session, not the long-lived server
credential.
Development still has a local compatibility fallback that can return the raw key if the auth-token exchange is unavailable. Production does not use that fallback:
VITE_GEMINI_ENABLED=true
GEMINI_API_KEY=CHANGE_ME_GEMINI_API_KEYIf token creation fails in production, the frontend shows a Gemini session error. Other providers are unaffected.
Rotate provider keys on a regular cadence and immediately after suspected exposure. A 90-day cadence is a reasonable baseline for demo deployments.
- Create a new provider API key in the provider dashboard.
- Add the new key to the deployment environment as the runtime secret.
- Restart or redeploy the container so Node reads the new environment.
- Verify
/api/healthand the affected provider tab. - Revoke the old provider key.
- Watch
/api/metricsand server logs for 401, 403, or 429 spikes.
Rollback if the new key fails:
- Restore the previous known-good runtime secret.
- Restart or redeploy the container.
- Re-run health and provider checks.
- Keep the failed key disabled unless the provider dashboard confirms it was never used.
Never add provider keys to VITE_* variables, Docker build args, commits, logs,
or issue text.
Run local verification after deployment:
npm run deploy:verify -- --url https://voice.example.comCheck headers:
curl -sS -D - -o /dev/null https://voice.example.com/api/healthCheck unauthorized CORS behavior:
curl -sS -D - -o /dev/null \
-X OPTIONS https://voice.example.com/api/xai/session \
-H 'Origin: https://evil.example.com' \
-H 'Access-Control-Request-Method: POST'The unauthorized response should not include an Access-Control-Allow-Origin
header for the unauthorized origin.
Check allowed CORS behavior:
curl -sS -D - -o /dev/null \
-X OPTIONS https://voice.example.com/api/xai/session \
-H 'Origin: https://voice.example.com' \
-H 'Access-Control-Request-Method: POST'The allowed response should include the configured origin.
After a real HTTPS production URL exists, run a browser security header scanner against the public origin. Recommended checks:
- Mozilla Observatory
- securityheaders.com
- OWASP ZAP baseline scan, if approved for the environment
Scanner verification is blocked for local-only deployments because scanners need public HTTPS access. Record the blocker in the release notes or validation file until a production URL is available.
Open an incident if any of these occur:
/api/healthreports unsafe production CORS configuration- Security headers disappear from production responses
- Token/session routes return broad API limiter headers only
- Raw provider API keys appear in a browser response, browser console, network capture, or server log
- Provider routes accept malformed or oversized payloads and call upstream APIs
Follow docs/runbooks/incident-response.md for containment, rotation, rollback,
and escalation steps.
This phase does not add user authentication, tenant authorization, DDoS protection, secret-manager integration, penetration testing, or a CSP reporting endpoint. WAF rules are expected to be enforced by the hosting platform or edge provider, not by the repo itself, and should be configured before public launch. Those controls otherwise belong to later product phases.