Overview
Tenancy, RBAC, value-checked ownership, secrets handling, and the unauthenticated surfaces.
Tenancy & isolation
Every object belongs to a tenant. Tenant-scoped endpoints filter by the caller's
tenant, and all /applications/{id}/* mutations verify the application belongs
to the caller before acting. Cross-tenant access returns 404 — never another
tenant's data, and never a 403 that would confirm an object exists.
Authorization (RBAC)
API keys carry scopes. Admin mutations require
apps:admin. A key can only mint another key (or a widget token) with scopes it
itself holds — no privilege escalation. Minting a widget JWT requires
connections:write, so a read-only key can't gain write via the widget surface.
Ownership is value-checked
Ownership and propagation checks require the exact desired value (TXT equals the challenge; A/AAAA contain the desired IP). Existence alone is never accepted, because the DNS record set is controlled by whoever is being verified.
Secrets handling
- API keys are stored as SHA-256 hashes; the secret is shown once and never
returned again (not by list endpoints,
/metrics, or/healthz). - Delegated DNS credentials are encrypted at rest (AES-256-GCM) under
CREDENTIAL_ENC_KEY; production refuses to store them without a key. - The widget JWT signing secret (
TOKEN_HMAC_SECRET) must be set to a non-default value in production or the service refuses to start. - Application secret refs and internal tenant IDs are never serialized into API responses.
Log hygiene
Key material is never logged (only last-4 for correlation). Ownership challenge
values are never logged. Internal errors are logged server-side under a
correlation ref; clients receive a generic message — raw driver/SQL errors are
never returned, including on the pre-authentication path.
Unauthenticated surfaces
/healthz, /metrics, and /internal/ask require no auth and must be
network-isolated in production:
/internal/askanswers only{allow, origin}— it does not expose the tenant id./metricsexposes only aggregate counts (no tenant identifiers or secrets).
CORS
The API is bearer-authenticated with no ambient cookies, so origins are
reflected by default. Set CORS_ALLOWED_ORIGINS to restrict to a fixed
allowlist as defense-in-depth. Per-application embed control is enforced at the
widget layer via allowed_origins.
Exactly-once side effects
The live transition (and its webhooks + usage metering) is guarded by a compare-and-swap so the API and the background worker can't double-fire. Connect-quota reservation is atomic under a Postgres advisory lock.
Reporting
This is an open-source project; report suspected vulnerabilities privately to the maintainers before public disclosure.