# Security Policy ## Supported Versions Security fixes are released for the latest minor version of the current major release. Upgrade to the latest `1.x` release to receive them. | Version | Supported | | ------- | ------------------ | | 1.5.x | :white_check_mark: | | < 1.5.0 | :x: | ## Reporting a Vulnerability DOMINUS has two editions with different exposure: - **Community** is self-hosted. It authenticates with static API keys from the environment and runs as a single tenant. - **DOMINUS Cloud** is multi-tenant: API keys stored hashed in the database, optional single sign-on (OIDC), team seats, billing and an operator panel. If you discover a security issue in either edition: 1. **Do not open a public GitHub issue.** 2. Send details through a private vulnerability report on GitHub (the repository's **Security** tab). 3. You should receive a response within 7 days. ## Security Design ### No Secrets in Code - API keys and credentials are read from environment variables (`.env`, gitignored) or from files named by `FILE_*` variables. - `.env.example` documents every variable without real values, and a test keeps it in sync with the configuration schema. - NameBio's API key is sent in the query string (the vendor's format); it is redacted from error logs. ### SQL Injection Prevention - Queries are parameterised on both SQLite and PostgreSQL. Dynamic identifiers (table names in migrations) never come from user input. - A CI check keeps SQLite-only SQL out of code that also runs on PostgreSQL. ### Input Validation - Domain names are validated against RFC 1123 rules before any provider call. - CSV imports are validated for schema compliance before processing. - API bodies are validated with Zod. ### Network Exposure - The server binds to `127.0.0.1` by default. The Docker image sets `HOST=0.0.0.0` for container ingress, and **the server refuses to start on an exposed interface without API authentication configured.** - Standard HTTP security headers and a strict CSP are set. - In Cloud mode the server refuses to start without `METRICS_TOKEN`, so `/api/v1/metrics/*` is never public. ### Authentication **API keys** (CLI, scripts, Community edition) - Community: static keys from `API_KEYS` / `FILE_API_KEYS`, compared in constant time. They act as the admin of the single tenant. - Cloud: keys are generated server-side, shown once, and stored only as a salted scrypt hash. Keys carry a role (`admin` or `member`) and can be revoked. - Failed authentication is rate limited per IP. **Single sign-on** (Cloud, `AUTH_PROVIDER=auth0` with OIDC client credentials) - Authorization Code flow with PKCE and `state`; the ID token is verified against the IdP's JWKS (issuer and audience). - The browser session is an `HttpOnly`, `Secure`, `SameSite=Lax` cookie holding a signed JWT (8 hours by default). There are no refresh tokens: a session ends at expiry or at logout, and logout also ends the identity-provider session. - State-changing requests authenticated by the cookie must come from a trusted `Origin`/`Referer` (CSRF guard). Bearer-token callers are unaffected. - The tenant and role in a session come from the user's team seat, not from IdP claims, and the seat is re-checked on every request: removing or demoting a member takes effect immediately, not at session expiry. - Team invitations are single-use links bound to the invited email: the signed-in user's verified email must match. **Roles** - `member` and `admin` are scoped to one tenant; `admin` manages that tenant's team, keys and billing. - `operator` is the cross-tenant platform role (operator panel, suspend, plan override). It is granted only through the `OPERATOR_SUBJECTS` allowlist; no key row or token claim can grant it, and an empty allowlist means nobody has it. ### Billing - Stripe webhooks are verified with the signing secret on the raw body and de-duplicated durably; a failed handler releases its claim so Stripe's retry is processed. Custom-price grants cross-check tenant and amount. ### Multi-Tenant Isolation (DOMINUS Cloud) Isolation is enforced in layers: 1. **Application:** repositories scope queries by `tenant_id`, and domain uniqueness is per tenant. 2. **Database:** PostgreSQL Row-Level Security (forced, for a non-superuser application role) on the entity tables (candidates, scoring runs, portfolio, outcomes, listings, bids, alerts, watchlist, events, onboarding, public scores, ...). The tenant context is reset on every pooled connection, and a connection whose reset fails is destroyed. 3. **Control-plane tables** (API keys, subscriptions, usage, team seats, invitations, tenant flags) are scoped by the application, not by RLS: they are read before a tenant is known (key validation, webhooks) or across tenants (the operator panel). Extending RLS to them needs a dedicated bypass design and is tracked as follow-up work. Cross-tenant behaviour is covered by tests that run against SQLite and, in CI, against a real PostgreSQL. ### Dependency and Image Supply Chain - Dependencies are audited in CI (`npm audit`, secret scanning, CodeQL, image scanning) and Dependabot proposes updates. - Container images are pinned by digest; runtime images drop the npm CLI and run as a non-root user. ### Database Safety - Community: SQLite in WAL mode; automatic daily backups (`VACUUM INTO`). - Cloud: PostgreSQL with WAL archiving and point-in-time recovery (see `docs/operations/rto-rpo.md`).