Security
How Quathos Auth isolates tenants, protects credentials and keys, and records what happened — starting with what we do not claim yet.
What we do not claim (yet)
Security pages are usually read by people trained to spot the gap between a badge and a control. So the gaps first, in writing:
- No SOC 2 report — neither Type I nor Type II — and no auditor engaged yet. A readiness programme exists on paper; the report does not.
- No availability figure. We will publish an uptime commitment only after failover has been rehearsed under load, and then in a written SLA — not as a number on a marketing page. Until then, the Terms say what they say: no uptime commitment.
- No external penetration test published yet, and no bug bounty programme. Coordinated disclosure, described below, is what exists today.
- No OpenID Foundation certification yet. The conformance suite runs against a test tenant; certification is a criterion for general availability, not something already obtained.
- No rehearsed backup and restore yet. The backup pipeline — encrypted, continuous, stored outside the application host, 30-day retention, point-in-time recovery — is designed and being put in place; until a restore has been rehearsed end to end, we do not claim recoverability, and we say so here rather than in an incident.
- Passkeys and SMS one-time codes are on the roadmap and described in our policies for when they ship; they are not available today.
Tenant isolation
- Every identity table carries a tenant identifier, and PostgreSQL row-level security is enabled and forced on all of them. The application connects through a role that cannot bypass the policy: a query that forgets to scope by tenant returns nothing, not everything.
- The tenant is resolved from the hostname that received the request, matched against a verified list of tenant domains. It is never read from a parameter, a header the client controls, or a token. An unknown host is a 404.
- The tenant is bound to each database transaction as a transaction-local setting, so it cannot leak between requests even behind a connection pooler. A session cookie from one tenant simply finds no session in another.
- Our own products use the platform as a tenant like any other. There is no code path reserved for us.
Credentials and secrets
- Passwords are hashed with argon2id. A sign-in attempt against an email that does not exist spends the same time as a real hash check, so the response time does not reveal which accounts exist.
- TOTP secrets are encrypted at rest with a key that lives outside the database, derived separately per purpose, so a database dump does not yield usable second factors. Each code is accepted once: the time step it belongs to is recorded and cannot be replayed.
- Recovery codes, session tokens, refresh tokens, authorisation codes, device codes and client secrets are stored only as SHA-256 hashes. The clear value appears once, in the response that created it, and never again.
- Secrets, tokens, one-time codes and message bodies are never written to logs or to the audit payload. This is enforced in the code paths that log, not left to discipline.
Tokens and signing keys
- Each tenant signs its own tokens with its own RSA-2048 key pair and publishes its own JWKS at its own issuer. The private key is encrypted at rest; a key compromised in one tenant proves nothing about another.
- Keys rotate automatically every 90 days. The previous key keeps verifying — and stays in the JWKS — for 48 hours after its successor takes over, then leaves. No restart, no downtime, no manual step.
- Authorisation codes are single-use and expire in 60 seconds; PKCE is mandatory for every client. Access tokens live 15 minutes by default and are not stored server-side.
- Refresh tokens rotate on every use, in families. Presenting a token that was already rotated — the signature of a stolen copy — revokes the whole family and records the reuse. Signing out of the hub session revokes the families it issued.
Sessions and the browser
- The session cookie is HttpOnly and host-only, and the browser never talks to the API directly: a server-side layer forwards the cookie, so no script on the page can read it. State-changing requests carry a double-submit CSRF token.
- Sessions end after 24 hours without activity and never last more than 14 days. The session token is rotated every 24 hours of use, with a 60-second grace so requests in flight do not fail. You can see and revoke every session from the Sessions page.
- A strict Content Security Policy allows scripts only from our own origin, forbids framing entirely, and restricts form submissions to our own pages. The typeface is self-hosted; no page loads anything from a third-party host.
- Sign-in, registration and second-factor verification are rate-limited per client, and the limiter fails closed on the paths that send email.
Audit trail
- Every security-relevant action — sign-in, second factor, session revocation, membership changes, application changes, operator actions — is recorded with actor, target, time, IP address and user agent.
- The table is append-only in two independent layers: a database trigger rejects any update or delete, whoever issues it, and the application role has had those privileges revoked. A bug in application code cannot rewrite history.
- Audit rows are tenant-scoped like everything else: a tenant sees only its own. Operator actions that belong to no tenant are visible only from the operator plane.
Our own access
- Cross-tenant access exists only in the operator plane, behind three proofs: a valid portal session, an explicit and active operator record, and a session that was authenticated with a second factor. Anyone failing any of the three gets a plain 404 — the surface is not even acknowledged.
- The production build refuses to start with an insecure configuration: development secrets, insecure cookies, a mock payment or email provider, a missing encryption key, or a wildcard CORS origin each stop the boot with the full list of problems.
Infrastructure
- Data is stored at rest with Hetzner Online GmbH in Germany (Falkenstein and Nuremberg), in the European Union. Traffic is encrypted in transit.
- Backups are the item at the top of this page that we do not claim yet. The target is stated so you can hold us to it: encrypted, continuous, kept outside the application host, retained for 30 days, with restores rehearsed — and the field-encryption key kept apart from the backup, so that a backup alone is not the data. Deleted data will leave backups as they rotate; we will not restore one to resurrect what a customer asked us to delete.
- Test and mock providers are technically blocked from running in production, and the subprocessors that can touch data are listed on their own page with 30 days’ notice of any change.
Coordinated disclosure
If you found a vulnerability, we want to hear about it, and we will not threaten you for telling us. Write to security@quathos.com with what you found, how to reproduce it and, if you have one, a proof of concept. What we ask of you, and what we commit to in return:
- Test only against your own tenant and your own accounts. Do not access, modify or exfiltrate data that is not yours, do not degrade the service for others, and do not run automated scans against the shared sign-in hosts without asking first.
- Give us reasonable time to fix before publishing. We acknowledge every report within two business days and triage it within five; these are targets of a small team, not a contractual SLA, and if we miss one we tell you why and give a new date. The full policy, with fix timelines by severity, is in the SECURITY.md file of the repository.
- Research conducted in good faith under these terms is authorised, and we will not pursue legal action over it. If you want credit, you will get it; if you prefer to stay anonymous, that is respected.
Documents a reviewer asks for
Published in full, in this order of usefulness:
Reporting a vulnerability
Write to security@quathos.com — a person reads it, and we acknowledge within two business days.