Security
This chapter lists what the gateway exposes, what each part trusts, and the settings that lock it down. Work through Hardening checklist on every new install, then read the sections for the parts you use.
Hosted customers: Vexyl runs and secures the gateway. Your part is in Hosted customers.
What is exposed
| Listener | Default address | Who must reach it | How it is protected |
|---|---|---|---|
AudioSocket (AUDIOSOCKET_PORT, TCP) |
127.0.0.1 (AUDIOSOCKET_HOST); 0.0.0.0 in the Docker image |
Your Asterisk only | Loopback, or the firewall |
HTTP API (HTTP_PORT, TCP) |
127.0.0.1 (HTTP_HOST) |
Your dialplan, dialler or monitoring | IP whitelist HTTP_ALLOWED_IPS. There is no API key |
SIP bridge (SIP_PORT, UDP) |
all interfaces | Your PBX or carrier | SIP_ALLOWED_IPS and the firewall |
| Call audio (RTP range, UDP) | all interfaces | Your PBX or carrier | Firewall |
Browser widget (WEBSOCKET_AUDIO_PORT, TCP) |
off, then 0.0.0.0 |
Web visitors, through a TLS proxy | Origin list, API key, per-IP and session limits |
The gateway also connects out to the providers you configure (speech recognition, speech synthesis, the AI brain), to your webhook and billing receivers, and to Redis if you use it.
Hardening checklist
- Make
.envreadable by the service user only:chmod 600 .env. It holds every provider key and your licence key. - Keep
HTTP_HOST=127.0.0.1unless another host must call the API. If it must, setHTTP_ALLOWED_IPSto exactly those hosts. - Set
SIP_ALLOWED_IPSto the exact addresses of your PBX or carrier. Open the SIP port and the RTP range to those addresses only. - Leave the browser widget off (
WEBSOCKET_AUDIO_ENABLED=false) unless you use it. If you do, follow Browser widget. - Leave
OUTBOUND_ENABLEDoff unless you place outbound calls. - Set
WEBHOOK_SECRETandBILLING_CDR_SECRET, and check the signature on your receivers. - Keep
WEBHOOK_SSRF_CHECKon. - Restrict who can read the logs, the speech cache and the billing records: they can hold what callers said.
- Run the gateway as an unprivileged user. The Docker image already runs as user id 1000.
- Rotate provider keys and the widget key when someone who knew them leaves.
Check the host from outside after you finish:
nmap -sU -p 5080 <gateway-public-ip>
nmap -p 8080,8081,8082 <gateway-public-ip>
From an address you did not allow, every port should show as filtered or closed.
HTTP API
The API can create sessions, change the bot and voice of a call, place outbound calls and replay billing records. It has no API key, so access is by network position only:
HTTP_HOST=127.0.0.1
HTTP_PORT=8081
HTTP_ALLOWED_IPS=127.0.0.1,::1
HTTP_TRUST_PROXY=false
HTTP_ALLOWED_IPStakes exact addresses and IPv4 CIDR blocks. A blocked request gets 403 and the log showsAUTH: Blocked request.- Unset
HTTP_ALLOWED_IPSturns the whitelist off: anyone who can reach the port is served. - Set
HTTP_TRUST_PROXY=trueonly when a reverse proxy you control sits in front. Otherwise a client can forgeX-Forwarded-Forand pass the whitelist. - If a remote system must call the API across the internet, put an authenticating reverse proxy (TLS plus a token or client certificate) in front and allow only the proxy's address.
In Docker, publish the API on loopback (-p 127.0.0.1:8081:8081), because the image listens on all interfaces inside the container. See Quickstart: Docker.
SIP and call audio
SIP signalling and call audio (RTP) to the gateway's bridge are plain UDP: no TLS and no SRTP. Anyone on the network path can read the signalling and record the audio.
- Inbound calls are trusted by source address. Set
SIP_ALLOWED_IPS: with it empty, any host that can reach the port can place calls that spend your speech and AI budget. A call from any other address gets 403 and logsBlocked request from unauthorized IP. SIP_ALLOWED_IPStakes exact addresses only. A CIDR entry matches nothing.- Outbound registration (
SIP_REGISTER_URI) uses digest authentication withSIP_AUTH_USERandSIP_AUTH_PASSWORD. Use a long, unique password for that account. - Set
SIP_MAX_CALLSto cap concurrent SIP calls below your licence limit if a flood of calls would hurt you.
When the PBX is on another network:
- Best: carry SIP and RTP over a private link or a site-to-site VPN.
- Or: put an Asterisk in front of the gateway on the same host. Connect your PBX to that Asterisk with TLS and SRTP, and keep the Asterisk-to-gateway leg on loopback. See Asterisk over AudioSocket.
- Do not expose the SIP port to the whole internet even with
SIP_ALLOWED_IPSset. Filter in the firewall as well, so scanners never reach the gateway.
SIP headers on an inbound call can set the bot, language and voice for that call (Per-call settings reference). This is why only trusted PBXs may send to the gateway. Header values are checked before use: an unknown provider, a malformed voice id or a bot id with stray characters is dropped.
AudioSocket
AudioSocket has no authentication or encryption of its own. It listens on loopback by default (AUDIOSOCKET_HOST=127.0.0.1); keep it that way when Asterisk runs on the same host. If Asterisk is on another host, set AUDIOSOCKET_HOST to the private interface, allow only that host in the firewall, and keep the link on a private network. In Docker, publish the port only to the address Asterisk uses.
Browser widget
The widget endpoint is public by design, so it carries most of the controls:
WEBSOCKET_AUDIO_ENABLED=true
WEBSOCKET_AUDIO_HOST=127.0.0.1
WEBSOCKET_AUDIO_ALLOWED_ORIGINS=https://www.example.com
WEBSOCKET_AUDIO_API_KEY=<long-random-string>
WEBSOCKET_AUDIO_MAX_PER_IP=5
WEBSOCKET_AUDIO_ALLOWED_BOT_IDS=support
WEBSOCKET_AUDIO_IDLE_TIMEOUT_MS=300000
WEBSOCKET_AUDIO_MAX_SESSION_MS=3600000
- Serve it only through a TLS proxy (
wss://), with the widget port on loopback (WEBSOCKET_AUDIO_HOST=127.0.0.1) behind it. Browsers need HTTPS for the microphone anyway. - The origin list is the main control. The API key sits in your page where any visitor can read it, so treat it as a speed bump, not a secret.
- Every widget session holds a call slot shared with phone calls. Without per-IP and session limits, a script can occupy every slot.
- A page cannot set the speech provider, model or voice, and the gateway strips identity keys (
userId,sessionId,uuid,apiKey,authorizationand others) from what a page sends.
Details are in Secure it.
Webhooks and billing records
- With
WEBHOOK_SECRETset, every lifecycle event carriesX-Webhook-Signature, an HMAC-SHA256 of the raw body. Billing records useBILLING_CDR_SECRETthe same way. Reject unsigned or mismatched requests. The procedure is in Verifying the signature. - Use
https://receiver URLs. WEBHOOK_SSRF_CHECK(on by default) refuses a webhook URL that resolves to a private or loopback address, so a URL passed in an outbound request cannot reach internal services. Turn it off only when your receiver is on your own private network.WEBHOOK_TOOL_PAYLOADS(off by default) adds tool arguments and results tocall.ended. They may contain caller personal data; turn it on only if your receiver is allowed to hold that data.- Custom metadata keys you post for a call are copied into the webhook payload. Do not post secrets as metadata.
Data the gateway keeps
| Data | Where | Holds | How long |
|---|---|---|---|
| Logs | pm2, journald or docker logs |
Caller numbers, transcripts of what callers said and the bot's replies, errors | Your log rotation |
| Session metadata | Memory, or Redis | Caller number and name, the per-call settings and any custom keys | SESSION_TTL seconds (default 3600) |
| Speech cache | cache/tts (TTS_CACHE_DIR) |
Audio of bot sentences, which can include names or numbers the bot read out | TTS_CACHE_MAX_AGE_DAYS since last use (default 90) |
| Billing records | cdr/ (BILLING_CDR_DIR) |
Per-call usage and cost, call ids and numbers | Until delivered; kept only with BILLING_CDR_KEEP_DELIVERED=true |
| Latency log | logs/performance.csv (with LOG_LATENCY=true) |
Timings per turn | Until you remove it |
The gateway does not record call audio. Recording, if you need it, happens in your PBX.
To reduce what is kept:
- Rotate logs daily and keep them only as long as you must. Restrict the log directory to the service user and your operators.
- Set
GEMINI_LIVE_TRANSCRIPTS=falseto stop realtime transcripts in the logs and webhooks. - Use Redis with a password (
REDIS_PASSWORD), on loopback or a private network. - Clear the speech cache after changing a greeting that contained personal data.
Third-party providers
Caller audio goes to the speech recognition provider, the text of the conversation goes to the AI brain, and the bot's replies go to the speech synthesis provider. Each provider's own terms decide how long it keeps that data and where it processes it.
- Choose providers and regions that meet your data rules. Several providers take a region, for example
GOOGLE_TTS_REGION,GEMINI_TTS_REGIONandAZURE_TTS_REGION. - A self-hosted recogniser and voice (
vexyl-stt,vexyl) and your own brain endpoint keep the conversation on infrastructure you control. - Store a Google service-account file outside the web root, readable by the service user only, and give that account only the speech roles it needs.
Keys and the licence
- Keep every key in
.env, never in a dialplan, a web page (except the widget key) or a support ticket. - A licence key is bound to one machine; it is useless elsewhere, but treat it as confidential.
- Use separate provider keys per environment (test and production) so a leaked test key cannot bill production, and set spending limits in each provider's console.
Hosted customers
- Send calls from your fixed public IP only; that address is what we allow. Tell us before it changes.
- Keep the widget API key in your page and list every origin that embeds it; we allow those origins only.
- Verify
X-Webhook-Signatureon every event with the secret we gave you, and keep that secret on your server. - Do not put personal data you do not need into per-call metadata or SIP headers.
Next: Troubleshooting.