SECURITY.md
Rama master · réplica de lectura del repositorio original.
# 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`).