Skip to content

ADR-0006 Zero-Shared-Secrets Edge

Accepted — implemented and live. (Recorded in the repository as ADR-011.)

An edge node (OpenSIPS + FreeSWITCH + rtpengine + node agent + call engine) runs on the platform’s most exposed surface — public SIP/RTP — and in the multi-edge future may sit on a customer-controlled VPS. Originally the edge .env held plaintext credentials to shared, cross-tenant infrastructure: the OpenSIPS auth DB URL, the call engine’s DB_*, the shared REDIS_PASSWORD, and an unauthenticated NATS_URL. Any single leak was a full multi-tenant breach — every tenant’s subscribers, CDRs, call state and events. Postgres row-level security did not help (the get2dial role bypassed RLS), and NATS had no authentication at all.

Remove every cross-tenant secret from the edge; the edge holds only scoped, per-node / per-tenant credentials:

  • Node identity is a hashed API key (issued at registration, matched against nodes.api_key_hash) rather than a shared DB/Redis password — chosen over mTLS for the first iteration.
  • OpenSIPS reads its subscribers (and TLS certs) over a local db_http service instead of a direct shared-Postgres connection; usrloc/dialog are in-memory.
  • The call engine runs with READ_SOURCE=api, CDR_SINK=api and DISPOSITION_SINK=api, so all reads and write-backs go through node-authenticated control-plane endpoints — no shared DB handle.
  • Redis is scoped with a per-tenant ACL user (keys ~t:<tenant>:*) rather than removed.
  • NATS gains per-tenant accounts whose publish/subscribe is limited to t.<tenant>.>.

A CI guard (check-edge-no-shared-secrets.sh) asserts the committed edge config carries no required shared-infra credential.

  • A compromised edge can only ever touch its own tenant’s data — the blast radius of a leak drops from “all tenants” to “one tenant”.
  • The edge is pool-less: it depends on the control plane’s node API for config, which becomes a runtime dependency for cold reads (mitigated by an edge cache).
  • Credential rotation requires the edge .env to be redeployed to take effect — rotation alone is not enough.