Obexal Docs

Docs/Security and compliance/Security model

Security model

The control families that protect every Obexal flow, cryptography, tenant isolation, abuse resistance, fail-closed defaults and HTTP hardening.

Obexal applies a small set of non-negotiable controls uniformly across every authentication flow. This page summarizes them by family; each linked page covers the operational detail.

Cryptography

Passwords are hashed with Argon2id (memory-hard, default 64 MiB memory, 3 iterations, parallelism 2, tunable via environment). Each hash uses a fresh random salt from a CSPRNG and is stored as a standard PHC string. Verification recomputes with the stored parameters and compares in constant time.

Application secrets at rest (TOTP secrets, the OIDC signing private key, webhook signing secrets) are encrypted with AES-256-GCM, using a 32-byte key supplied by the environment and a random nonce per encryption. GCM authentication means a tampered ciphertext fails to decrypt instead of yielding corrupted plaintext. The encryption key rotates without data loss: see Key rotation.

Tokens (access and ID tokens) are JWTs signed with RS256 (RSA-2048), carrying a kid header. The JWKS publishes public keys only. Verification pins RS256: alg: none and HMAC algorithms are rejected, closing algorithm-confusion attacks. Signing keys rotate with a grace period so tokens still in flight remain verifiable. See Validate tokens.

Transport is TLS terminated at the reverse proxy: TLS 1.2 minimum, TLS 1.3 negotiated when the client supports it. Outbound SMTP connections also enforce TLS 1.2 minimum.

Multi-tenant isolation

For any authenticated request, the tenant is the one bound to the session, never a header: the X-Obexal-Tenant header is read only on pre-authentication flows (login, sign-up) to select the tenant to authenticate against. A signed-in user cannot cross a tenant boundary by forging a header.

Every read and write is scoped to the session tenant. Requesting another tenant's object by its id returns 404, not 403: no existence leak. Token operations (refresh, revoke, introspect) re-check the tenant explicitly, and /oauth/authorize refuses to let a user authorize a client belonging to another tenant.

Suspension is a kill switch. Suspending a tenant blocks every sign-in and makes every existing session of that tenant immediately inoperative. The same applies per user, and each AI agent has its own kill switch.

Abuse resistance

  • Anti-enumeration: sign-up, password reset and passwordless start always return the same generic 202; a failed login is always 401 invalid_credentials. When the account does not exist, a dummy hash is computed so the response time stays near constant, and transactional email is sent asynchronously so timing reveals nothing.
  • Layered rate limiting: per email and per IP on login, per IP on passkey sign-in and social callbacks, per email plus IP on passwordless and recovery flows, with tenant-scoped counters.
  • Account lockout: failures are counted per (tenant, email), so repeated password failures lock the account itself. Because the key is the account, rotating IPs does not bypass it. Failures on unknown emails produce the identical 429. Admins unlock with POST /v1/admin/users/unlock.
  • Breakthrough detection: a login that succeeds after the account had reached the lockout threshold is recorded as a critical security incident (bruteforce_breakthrough): the strong signal that a credential-stuffing campaign finally guessed right.

Fail closed on sensitive paths

  • LDAP: if the upstream directory is unreachable during login, the attempt fails with a generic error. There is no weaker fallback. See LDAP and Active Directory.
  • Agent governance: the agent kill switch and permission ceilings fail closed. A storage error while reading an agent's policy refuses token issuance rather than silently re-enabling a neutralized agent.
Note

Two controls prioritize availability so that a momentarily missing signal never locks an entire tenant out. Country rules under conditional access rely on a local GeoIP database: keep it installed and monitor its presence so the country rules apply. The risk engine is disabled by default and opt-in, and is designed never to block a legitimate sign-in when one of its signals is unavailable. See Conditional access and Risk-based access.

Sessions, CSRF and HTTP hardening

Sessions are opaque 32-byte random tokens. The server stores only their SHA-256 hash, never the raw value. The session cookie is HttpOnly, Secure and SameSite. A password reset or a confirmed email change revokes all of the user's sessions. See Sessions and logout.

CSRF protection is double-submit: unsafe methods must send an X-CSRF-Token header equal to the CSRF cookie, compared in constant time. The /oauth/, /scim/, /saml/ and /.well-known/ prefixes are exempt because they never authenticate by cookie, so there is no CSRF vector to protect.

Every response carries hardening headers: HSTS when served over HTTPS, a deny-all Content-Security-Policy on API responses (default-src 'none'), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, a strict Referrer-Policy, and Cache-Control: no-store on authentication and token responses. CORS is a strict allowlist, and error messages are generic (details go to server logs only).