Fernet encryption
File:src/voicegateway/core/crypto.py
All API keys stored in the managed_providers table are encrypted with Fernet (AES-128-CBC with HMAC-SHA256 authentication) from the cryptography library.
How it works
Secret key resolution
The Fernet key is resolved in this order:VOICEGW_SECRETenvironment variable. Highest priority, recommended for containerized deployments.~/.config/voicegateway/.secretfile. Persisted on disk withchmod 600permissions.- Auto-generated on first run. A new Fernet key is generated and saved to the secret file.
VOICEGW_SECRET_FALLBACK variable is also supported for zero-downtime key rotation: the gateway attempts decryption with the primary key first, then falls back to the secondary key.
os.replace() for atomic file creation. The file is created with 0600 permissions from the start and never exists in a world-readable state.
Encryption API
encrypt() and decrypt() unchanged.
Key rotation
Rotating the Fernet key does not mean re-adding every provider by hand. Set the new key, keep the old one reachable as a fallback, and runvoicegw rotate-secret:
voicegw rotate-secret refuses to run unless both variables are set: VOICEGW_SECRET is the new primary key, VOICEGW_SECRET_FALLBACK is the previous one. It re-encrypts every row in managed_providers under the new primary and reports how many rows were rotated, skipped (empty), or failed (no configured key could decrypt them). A nonzero failure count exits with status 2 and lists the affected provider IDs.
Until rotation runs, VOICEGW_SECRET_FALLBACK also keeps decrypt() working on rows still encrypted under the old key: the gateway builds a MultiFernet from the primary key plus every comma-separated key in VOICEGW_SECRET_FALLBACK and tries them in order. If none of them decrypt a value, decrypt() raises:
VOICEGW_SECRET_FALLBACK once rotation succeeds. Leaving it set keeps the old key acceptable for decryption, which defeats the point of rotating.
API key masking
All API responses that include provider information mask the API key usingmask():
"sk-proj-abc123xyz789"becomes"sk-p...z789""short"becomes"*****"""becomes""
MCP token authentication
The MCP server authenticates callers with a bearer token. SetVOICEGW_MCP_TOKEN to a strong random string. Any request without a matching Authorization: Bearer <token> header is rejected with 401.
If VOICEGW_MCP_TOKEN is not set, the MCP server starts without authentication. This is acceptable for local development but should not be used in shared or networked environments.
Tenant isolation
In multi-tenant cloud deployments, tenant identity is derived server-side from the ingest API key (vk_ prefix). The key is presented as a bearer token on POST /v1/ingest and POST /v1/agents/heartbeat. The tenant ID from the key is stamped on every record at ingest time.
Key rules:
- A worker or record can only be written under the key’s tenant. The
tenant_idfield in request bodies is advisory only and cannot override the key-derived tenant. VOICEGW_API_KEYis the agent-side variable that holds thevk_key. Set it in the agent process environment.- The cloud verifies the key and resolves the tenant before any write.
Audit log
Table:config_audit_log
Every create, update, or delete on managed resources is recorded.
Querying the audit log
Security checklist
Related pages
Storage
The SQLite tables where encrypted keys are stored.
Fleet worker heartbeat
Tenant isolation rules for the heartbeat ingest contract.
Configuration layers
How managed provider credentials flow into the resolved config.
Environment variables
All VOICEGW_SECRET, VOICEGW_MCP_TOKEN, and related env vars.