Security Overview
Tenant isolation is a Keycloak realm, not a naming convention — everything else in this section builds on that.
The tenant boundary: one Keycloak realm per account
RealmSSO's core security boundary is that every customer account gets its own Keycloak realm. An earlier model provisioned all accounts into one shared realm, distinguished only by naming (org-<slug> clients, saml-<slug>-<id> identity providers) — and a naming convention is not an authorization boundary. Because identity-provider aliases, client IDs, and session cookies are all scoped to a realm in Keycloak, moving to one realm per account closes off brokered-login and client-authorize cross-tenant token leakage at the protocol level, rather than trying to police it with application code. See Accounts (Tenants) for the full history, the live-tested attack legs, how a domain claim is proven against DNS, and what is still open (per-domain identity-provider routing inside one realm).
API hardening
Every request into the RealmSSO API passes through a fixed plugin and hook stack before it reaches a route handler:
- Helmet sets a restrictive Content-Security-Policy and related headers on every response.
- CORS is allow-listed from the
CORS_ORIGINSenvironment variable (comma-separated), with credentials enabled only for those origins. - Rate limiting is applied per client IP (
RATE_LIMIT_MAXrequests perRATE_LIMIT_WINDOW_MS, defaulting to 100 per 60 seconds). The server trusts exactly one proxy hop, as a predicate —trustProxy: (_address, hop) => hop === 0— so a caller can't spoof its own rate-limit bucket by forgingX-Forwarded-For. The numerictrustProxy: 1is gone: fastify 5.12.1 dropped it from the type union and compiles a number to() => false(GHSA-3m5p-2c4r-xxw2), which fails closed and would put every caller in the ingress's bucket. - A shared
onRequesthook stamps every response withX-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: strict-origin-when-cross-origin, a locked-downPermissions-Policy, and HSTS.
Two independent auth mechanisms gate everything past that: a JWT (@fastify/jwt, issued on vendor login, 24-hour expiry, carried in an httpOnly cookie or bearer header) for the vendor dashboard, and a hashed API key (X-API-Key header, looked up by SHA-256 hash — the raw key is never stored) for programmatic access. Both are then checked for authorization on the target account before any account-scoped data is returned — for a vendor session that means an account membership and a role at least as high as the route's minimum; for an API key, the single account it was minted for plus its scopes. See Access Control for exactly what each does and doesn't enforce today.
Where to go next
Encryption at Rest — what's encrypted in the database today, and what isn't.
Audit Logging — the AuditLog model and how to query it.
Access Control — vendor roles and API key scopes, and their actual enforcement.