# Security checklist

What the platform does by itself, and what the operator must check before go-live.
Deployment steps: [deploy-production.md](deploy-production.md). Runbook: [operations.md](operations.md).

## Built in
| Area | Control |
|---|---|
| Carrier credentials, webhook secrets | AES-256-GCM envelope encryption (per-secret data key, wrapped by a rotatable KEK). Never returned by the API, redacted from logs, rotatable (`PUT /v1/carriers/{id}/credentials`, `keys:rewrap`), revocable. Tests assert that responses, logs and raw rows never contain a carrier key. |
| API keys | Stored as hashes, shown once, scoped (least privilege), rotatable and revocable; every use is attributable in the audit log. |
| Dashboard | Better Auth sessions (12 h, HttpOnly cookie via a same-origin proxy), Origin check against `DASHBOARD_URL`, no self sign-up (admins are created with the CLI). |
| Softphones | Short-lived SIP tokens (default 1 h, configurable) instead of passwords in the browser; SIP passwords stored as HA1 only; WSS + DTLS-SRTP. |
| Internal endpoints | `/internal/*` (FreeSWITCH config) needs `INTERNAL_API_SECRET` **and** a source IP in `INTERNAL_ALLOWED_CIDRS` (127.0.0.1 in production); Caddy also returns 404 for it publicly. `/metrics` is restricted the same way. |
| FreeSWITCH | ESL on 127.0.0.1 with password and ACL; carrier profile accepts INVITEs only from carrier ACLs; agents profile requires digest auth; failed auth is logged for fail2ban. |
| Toll fraud | Destination country allow-list and blocked prefixes, concurrency and per-hour/per-day call limits, daily outbound-minutes alert and hard stop (spend guard). |
| Abuse of the API | Rate limits per key (Redis-backed), idempotency keys for call creation, body limits. |
| Webhooks out | Standard Webhooks HMAC signatures; HTTPS only in production; private/loopback destinations refused (SSRF). |
| Webhooks in (carriers) | Telnyx Ed25519 / Twilio HMAC signatures verified before processing. |
| Recordings | Private bucket; access only through short-lived signed URLs; retention deletes them. |
| Audit log | Every admin and configuration change, with actor and time. |
| Secrets in git | None: `.env*` ignored, `.env.example` / `.env.production.example` only; CI greps the E2E logs for test secrets. |

## Before go-live (operator)
- [ ] All secrets in `.env.production` generated freshly (`openssl rand -base64 32`), all different; file `chmod 600`.
- [ ] `ENCRYPTION_KEYS` stored in a password manager (it is not in any backup).
- [ ] `PROVIDER_API_OVERRIDES` absent, `ALLOW_MOCK_CARRIER=false`, `NODE_ENV=production` (the compose file sets the last two).
- [ ] Firewall exactly as in the deploy guide; `ss -tlnup` shows 3000, 3001, 6379, 8021, 9090, 3003 on 127.0.0.1 only.
- [ ] Port 443 limited to Cloudflare's IP ranges. The API trusts `X-Forwarded-For` from Caddy, and Caddy
      copies Cloudflare's `CF-Connecting-IP`; a client reaching Caddy directly could otherwise choose its own IP for rate limiting.
- [ ] Cloudflare SSL mode Full (strict); `sip.` and `turn.` DNS-only.
- [ ] `curl https://api.example.com/internal/freeswitch/xml` and `/metrics` return 404.
- [ ] fail2ban jails `freeswitch-voice` and `freeswitch-voice-scan` active.
- [ ] Workspace limits set for the real business: allowed destination countries (only what is needed —
      no premium/international by default), concurrency, calls per day, spend-guard alert and hard stop.
- [ ] Carrier-side limits as a second line: Telnyx outbound voice profile (allowed destinations, daily
      spend limit), Twilio Voice geographic permissions and usage triggers.
- [ ] Dedicated carrier API keys for this platform (not the account's main key), so they can be revoked alone.
- [ ] The CRM uses an API key with only the scopes it needs, and verifies webhook signatures.
- [ ] Admin accounts only for people who need them; strong passwords.
- [ ] Supabase: `voice` schema not exposed through the Data API; database password strong; network restrictions if the plan allows.
- [ ] R2 token limited to the one bucket; bucket not public.
- [ ] Recording consent configured (Settings → Recording) where the law requires it.
- [ ] SSH: keys only, no root password login; unattended security upgrades enabled.

## Known limitations
- One VM is a single point of failure for calls (database and recordings are managed). A second
  FreeSWITCH node behind DNS SRV is future work.
- Carrier-side SIP traffic to Telnyx/Twilio is plain SIP/RTP over the internet unless TLS/SRTP is set up
  with the carrier (see `STATUS.md`, "Known gaps").
