VEXYL AI

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

  1. Make .env readable by the service user only: chmod 600 .env. It holds every provider key and your licence key.
  2. Keep HTTP_HOST=127.0.0.1 unless another host must call the API. If it must, set HTTP_ALLOWED_IPS to exactly those hosts.
  3. Set SIP_ALLOWED_IPS to the exact addresses of your PBX or carrier. Open the SIP port and the RTP range to those addresses only.
  4. Leave the browser widget off (WEBSOCKET_AUDIO_ENABLED=false) unless you use it. If you do, follow Browser widget.
  5. Leave OUTBOUND_ENABLED off unless you place outbound calls.
  6. Set WEBHOOK_SECRET and BILLING_CDR_SECRET, and check the signature on your receivers.
  7. Keep WEBHOOK_SSRF_CHECK on.
  8. Restrict who can read the logs, the speech cache and the billing records: they can hold what callers said.
  9. Run the gateway as an unprivileged user. The Docker image already runs as user id 1000.
  10. 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_IPS takes exact addresses and IPv4 CIDR blocks. A blocked request gets 403 and the log shows AUTH: Blocked request.
  • Unset HTTP_ALLOWED_IPS turns the whitelist off: anyone who can reach the port is served.
  • Set HTTP_TRUST_PROXY=true only when a reverse proxy you control sits in front. Otherwise a client can forge X-Forwarded-For and 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 logs Blocked request from unauthorized IP.
  • SIP_ALLOWED_IPS takes exact addresses only. A CIDR entry matches nothing.
  • Outbound registration (SIP_REGISTER_URI) uses digest authentication with SIP_AUTH_USER and SIP_AUTH_PASSWORD. Use a long, unique password for that account.
  • Set SIP_MAX_CALLS to 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_IPS set. 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, authorization and others) from what a page sends.

Details are in Secure it.

Webhooks and billing records

  • With WEBHOOK_SECRET set, every lifecycle event carries X-Webhook-Signature, an HMAC-SHA256 of the raw body. Billing records use BILLING_CDR_SECRET the 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 to call.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=false to 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_REGION and AZURE_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-Signature on 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.