Skip to main content

API key authentication

When server_api_key is set, Voxray enforces bearer token authentication on all sensitive endpoints. Requests missing a valid key receive 401 Unauthorized with a standard error envelope.

Enabling authentication

{
  "server_api_key": "your-secret-key-here"
}

Protected and unprotected endpoints

EndpointProtectedNotes
POST /startYesSession creation
POST /webrtc/offerYesWebRTC signaling
POST /sessions/{id}/api/offerYesPer-session WebRTC offer (legacy path)
POST /api/v1/sessions/{id}/offerYesPer-session WebRTC offer (versioned path)
GET /wsYesWebSocket voice transport
GET /healthNoLiveness probe — always open
GET /readyNoReadiness probe — always open
GET /metricsNoPrometheus — firewall separately
GET /swagger/NoAPI docs
/health and /ready are intentionally unauthenticated so load balancers and orchestrators can probe them without credentials. Restrict /metrics access at the network level rather than relying on API key auth.

Sending the API key

Voxray accepts the key in either of two headers — use whichever fits your client library:
# Authorization: Bearer
curl -X POST https://voxray.example.com/start \
  -H "Authorization: Bearer your-secret-key-here" \
  -H "Content-Type: application/json" \
  -d '{}'

# X-API-Key
curl -X POST https://voxray.example.com/start \
  -H "X-API-Key: your-secret-key-here" \
  -H "Content-Type: application/json" \
  -d '{}'
Without a valid key, the response is:
# No key — rejected
curl -X POST https://voxray.example.com/start \
  -H "Content-Type: application/json" \
  -d '{}'
# → 401 {"error":{"code":"unauthorized","message":"Unauthorized"}}

CORS configuration

Voxray’s CORS behavior is explicitly opt-in. An empty cors_allowed_origins does not mean “allow all” — it means no Access-Control-Allow-Origin header is set, which will cause browsers to block cross-origin requests.
If cors_allowed_origins is omitted or empty and you are serving browser clients, your front-end JavaScript will fail with a CORS error. Set origins explicitly for every browser-facing deployment.

Setting allowed origins

{
  "cors_allowed_origins": [
    "https://yourapp.com",
    "https://www.yourapp.com"
  ]
}
Multiple origins are supported. Voxray reflects the matching origin in the response header — it does not echo a wildcard.

Headers always allowed

Regardless of origin configuration, Voxray always includes these in Access-Control-Allow-Headers:
Content-Type, Authorization, X-API-Key
This means browser clients can send the API key via either Authorization: Bearer or X-API-Key without needing custom CORS preflight configuration.

CORS and WebSocket

WebSocket connections (GET /ws) go through the same CORS check as HTTP endpoints. If your browser client uses the native WebSocket API, ensure the origin header sent by the browser matches one of your configured allowed origins.

Request body limits

Voxray limits JSON request body size on all endpoints that accept a body (/start, /webrtc/offer, /sessions/{id}/api/offer, /daily-dialin-webhook). This prevents large-payload denial-of-service without requiring any middleware.
SettingDefaultRecommended production value
max_request_body_bytes262144 (256 KB)1048576 (1 MiB)
{
  "max_request_body_bytes": 1048576
}
Requests that exceed the limit receive a 400 Bad Request with an error indicating the body was too large. The connection is not terminated — only the oversized request is rejected.
The default 256 KB limit is sufficient for all standard API payloads. The 1 MiB recommendation adds headroom for large SDP offers with many ICE candidates or custom body fields in /start without creating risk of multi-megabyte abuse.

Secrets management

Never commit API keys to git

Voxray supports two places to specify API keys: the api_keys map in config.json, and environment variables. The environment variable path is strongly preferred for production. What not to do:
{
  "api_keys": {
    "openai": "sk-proj-...",
    "daily_api_key": "..."
  }
}
If this config.json is committed to git, the keys are permanently in history even after deletion. What to do instead:
# Docker
docker run -p 8080:8080 \
  -e OPENAI_API_KEY=sk-proj-... \
  -e DAILY_API_KEY=... \
  -e VOXRAY_SERVER_API_KEY=... \
  -v $(pwd)/config.json:/app/config.json:ro \
  voxray:latest

# Kubernetes — reference a Secret
env:
  - name: OPENAI_API_KEY
    valueFrom:
      secretKeyRef:
        name: voxray-secrets
        key: openai-api-key
  - name: VOXRAY_SERVER_API_KEY
    valueFrom:
      secretKeyRef:
        name: voxray-secrets
        key: server-api-key

config.json and git

Voxray’s default .gitignore excludes config.json. Verify two things before your first commit:
  1. config.json is listed in .gitignore (or is absent from the repo entirely)
  2. config.example.json, if it exists, does not contain real provider keys — only placeholder strings like "YOUR_OPENAI_KEY_HERE"
# Audit for real-looking keys in example config
grep -E 'sk-|key-|Bearer ' config.example.json

Key rotation

Voxray caches resolved API keys in memory after the first lookup. To rotate any key — provider API keys or server_api_key — a process restart is required. There is no hot-reload mechanism for secrets in the current release. For zero-downtime rotation in Kubernetes:
  1. Update the Secret with the new key value
  2. Perform a rolling deployment (kubectl rollout restart deployment/voxray)
  3. Kubernetes will bring up new pods with the new key before terminating old pods

Container hardening

Non-root user

The Voxray Dockerfile runs the server as a non-root user named voxray. This is the default — no configuration change is required. Verify with:
docker run --rm voxray:latest id
# uid=1001(voxray) gid=1001(voxray)
Do not override the user with --user root in production. If a bind port below 1024 is required, use a reverse proxy instead of running as root.

Read-only config mount

Mount config.json as read-only. The :ro flag prevents the process from modifying the config file, which closes a class of config-corruption bugs and limits blast radius if the process is compromised:
docker run -v $(pwd)/config.json:/app/config.json:ro voxray:latest
In Docker Compose:
services:
  voxray:
    image: voxray:latest
    volumes:
      - ./config.json:/app/config.json:ro

Minimal base image

The Voxray image is built on Alpine Linux 3.20, which has a base size of approximately 7 MB. The attack surface is minimal — no shell utilities, no package manager, no cron, no sshd in the final stage.
# Final stage — Alpine 3.20 only
FROM alpine:3.20

Network policies

In Kubernetes, restrict outbound traffic from Voxray pods to only the external APIs they need to reach. Voxray does not require inbound connections from other pods except your load balancer or ingress. Example NetworkPolicy skeleton:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: voxray-egress
spec:
  podSelector:
    matchLabels:
      app: voxray
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 10.0.0.0/8
              - 172.16.0.0/12
              - 192.168.0.0/16
      ports:
        - protocol: TCP
          port: 443
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: default
        - podSelector:
            matchLabels:
              app: redis
      ports:
        - protocol: TCP
          port: 6379
Adjust the CIDR blocks and pod selectors to match your cluster topology.

Production security checklist

Run through every item before promoting to production.
  • TLS enabled — on-server (tls_enable: true) or reverse proxy termination
  • server_api_key set to a long random string — use a secrets manager or environment variable; rotate on suspected compromise
  • CORS origins explicitly set — cors_allowed_origins lists every domain your front-end runs on; no wildcards
  • Body size limit configured — max_request_body_bytes: 1048576 or appropriate for your payload sizes
  • /metrics not publicly accessible — firewalled at the network level or restricted to your Prometheus scrape CIDR
  • API keys in environment variables — OPENAI_API_KEY, DAILY_API_KEY, VOXRAY_SERVER_API_KEY, and all other provider keys sourced from env or a secrets manager
  • Config file not committed to git — config.json in .gitignore; config.example.json contains only placeholder values
  • Running as non-root — default Docker image user is voxray (uid 1001); do not override with --user root
  • Network policies restrict outbound to only required provider APIs — no unrestricted egress from Voxray pods