SECURITY.md: Sentin8 Platform — Security Model & Post-Quantum Architecture
This document is canonical and identical across every repository of the Sentin8 tree. It describes the security posture of the platform, not of any single service: every service — platform or vertical — inherits this model by construction, not by configuration.
Audience: security engineers, auditors, compliance officers, architects, and anyone evaluating what Sentin8 guarantees. For per-service architecture read that service's
README.md; for wire-level detail read itsDESIGN.md.
1. Executive Summary — The Security Model in One Page
Sentin8 is a post-quantum, zero-static-secret, live-authorization platform. It was designed for regulated, multi-tenant, multi-vertical operation (finance, hospitality, health) where the threat model includes nation-state harvest-now-decrypt-later collection, insider privilege escalation, and credential theft — not just opportunistic web attacks.
The model rests on five pillars:
- Post-quantum cryptography, exclusively. Every TLS connection in the platform — client→ingress, proxy→service, service→service, service→data — negotiates X25519MLKEM768 key exchange with ML-DSA-65 certificate signatures, anchored by an SLH-DSA-SHA2-128s (hash-based) root CA. The default TLS mode is exclusive: no classical fallback exists. Recorded traffic is quantum-resistant today; there is nothing to "migrate later".
- One identity plane. Sentin8:Auth is the only token issuer, the only JWKS authority, and the only authentication authority in the platform. No service mints its own sessions, tokens, or credentials — so there is no second issuer to compromise and no fragmented trust to reconcile.
- Live authorization — Shield. Authorization is decided per request by one embedded policy engine (Shield) that unifies RBAC, ABAC, and ReBAC. Roles are never embedded in tokens: they are resolved live from the identity plane on every request. A demoted administrator loses access on the next request — not at token expiry, not at midnight.
- Mandatory tenant isolation in the database itself. Every tenant table
carries
ENABLE+FORCE ROW LEVEL SECURITY; no role on the cluster carriesBYPASSRLS— not the event relays, not the schedulers, not anyone. Services connect as OAuth-authenticated per-service roles scoped to their own database and schema. Isolation is not an application convention; it is enforced by PostgreSQL on every row read. - Certificate-only workload identity — zero static secrets. A workload's
only identity is its Acme-issued client certificate (
CN=m2m-{workload}) presented in the TLS handshake. There are no client secrets, no static API keys, no password files anywhere in the service-to-service plane. A stolen configuration file grants nothing; a stolen database dump decrypts nothing.
Wrapped around the pillars is a fail-closed discipline: an unresolvable
hostname is a 421, not a guess; an unparsable policy set at boot is a
refused startup, not a warning; a role-resolution outage is a deny, not an
allow; an unauditable action is an action that never happened.
2. Security Architecture at a Glance
Every request into the platform walks the same eight-step pipeline. The steps are mandatory and ordered; no handler may skip one:
The properties this buys:
- The proxy never sees application plaintext. It is an L4 SNI router; TLS terminates inside the owning service, in-process.
- Context headers cannot be spoofed. The TLS layer strips every
client-supplied
X-Sentin8-*header before injecting its own authoritative values; an unresolvableHostis rejected with421 Misdirected Request. - The decision is never stale. Steps 5–6 consult live identity state and live policy data on every request.
- The database is the last wall, not the only one. Every handler also
carries explicit
WHERE env_id = $Nclauses — RLS is the backstop, defense-in-depth is primary.
3. Pillar I — Post-Quantum Cryptography
3.1 The Algorithm Suite
| Layer | Algorithm | Standard | Security basis |
|---|---|---|---|
| TLS 1.3 key exchange | X25519MLKEM768 (hybrid: classical X25519 ⊕ ML-KEM-768) | FIPS 203 | Lattice KEM + classical ECDH; breaking the session requires breaking both |
| Service & client certificate signatures | ML-DSA-65 | FIPS 204 | Lattice signatures (Category 3) |
| Root CA signature | SLH-DSA-SHA2-128s | FIPS 205 | Hash-based — secure even if lattices fall |
| Token / claims signing (JWKS) | PQ signing key families, coordinated rotation | FIPS 204 lineage | JWTs, JAR request objects, JARM responses, SD-JWT credentials |
| Symmetric envelope encryption | AES-256-GCM (key-versioned) | FIPS 197 / NIST SP 800-38D | Secrets store, BFF session store |
| Disclosures & bindings | SHA-256, PKCE S256, RFC 9449 DPoP | — | Selective-disclosure digests, code challenges, proof-of-possession |
| MFA (TOTP) | HMAC-SHA1, RFC 6238 (30s step, 20-byte secrets, stored encrypted) | — | The RFC standard; secrets never stored in the clear |
Two deliberate design choices deserve emphasis:
- The trust anchor is hash-based, not lattice-based. SLH-DSA-SHA2-128s (the SPHINCS+ family) derives its security from hash functions alone. Even a future cryptanalytic breakthrough against lattice assumptions leaves the root CA sound. The root is signed rarely, so its slower signing cost is irrelevant — the conservative choice costs nothing.
- The key exchange is a true hybrid. X25519MLKEM768 combines classical
X25519 with ML-KEM-768 such that an attacker must defeat both to recover
session keys. In
exclusivemode this hybrid is the only group offered — there is no classical-only downgrade to attack.
3.2 Exclusive by Default — No Classical Fallback
The platform's TLS posture is controlled by a single variable,
SENTIN8_TLS_PQ_MODE:
| Mode | Key exchange | TLS versions | Purpose |
|---|---|---|---|
exclusive (default) | X25519MLKEM768 only | TLS 1.3 only | Full PQ; no fallback group exists to negotiate down to |
hybrid | X25519MLKEM768 first, classical groups offered | TLS 1.3 only | PQ-preferred, legacy-client compatibility |
classical | Classical groups only, no ML-KEM | TLS 1.2 + 1.3 | Emergency rollback only — explicit, loud, temporary |
The default posture means the classic downgrade attack — get the handshake to agree on a classical group, record, decrypt later with a quantum computer — has no surface. A client either speaks post-quantum or fails the handshake.
Harvest-now-decrypt-later is dead on arrival against Sentin8 traffic.
3.3 The Certificate Authority — Sentin8:Acme
The platform runs its own RFC 8555 ACME certificate authority, because
public CAs cannot sign {service}.{env}.{org}.{tld} internal hostnames —
and because the post-quantum certificate ecosystem is young enough that the
platform refuses to wait for it:
- Two-tier hierarchy — SLH-DSA root → ML-DSA intermediate → leafs, for both server certificates (HTTPS on every domain) and client certificates (workload identity, below).
- Trust distribution — the root CA is installed into the OS trust store and the Chromium/NSS store at bootstrap; every process in the tree, and every headless browser test, verifies the same anchor.
- External domains — proxied to a public ACME provider; internal names never leave the boundary.
- Admission control — mint endpoints are shed-load protected: a fleet-wide
cold start answers
503 + Retry-Afterinstead of silently timing out. Saturation is loud, deterministic, and self-draining — never a soft security failure. - No
danger_accept_invalid_certs, anywhere. Verification failure is a failure; there is no test-only bypass switch to leave switched on.
3.4 Memory-Safe Cryptography
The entire TLS stack is pure Rust — rustls over the Graviola provider,
no C, no assembly blobs, no cmake-built vendor code. The historical lesson of
OpenSSL-class CVEs (Heartbleed-class memory disclosure in the handshake
path) is addressed by language, not by patching discipline: the crypto layer
cannot have a buffer overflow because the language forbids it. unsafe
blocks in the security path would be a review-blocking defect.
4. Pillar II — The Identity Plane (Sentin8:Auth)
4.1 One Issuer or None
Sentin8:Auth is the platform's only issuer of tokens, the only JWKS authority, and the only authentication authority. Every other service verifies what it issues; no service mints sessions, API keys, service credentials, or JWKS documents. The compromise question "which issuer was forged?" has one answer and one place to harden, rotate, and audit.
4.2 Token Families — the Right Credential for the Right Job
| Family | Use | Security properties |
|---|---|---|
| User access token | Any authenticated human access | Shield-decided; scopes are a coarse pre-filter only |
| ID token | OIDC authentication | Issued by the single authority |
| Refresh token | Session continuity | Rotated and revocable; held server-side by the BFF |
| M2M (workload) | Machine-to-machine | Certificates are the workload identity; tokens, where used, are certificate-bound (RFC 8705 cnf) — a stolen token without the private key is useless |
| API-client | Third-party integrations | Scoped, registered, auditable |
| Actor token | Administrative impersonation | Time-boxed and audited; support actions are attributable |
| Delegated token | Cross-context trust via the Delegation Gateway | Carries an act chain claiming the originating user; downscoped per target service by the BFF — never broad-scope passthrough |
Two structural rules govern all families:
- No realm tag. A user is a user. What distinguishes a platform operator from a bank teller from a hotel guest is RBAC assignment, not a token flavor — so there is no "realm confusion" class of vulnerability.
- No roles in the JWT. The token carries identity and coarse
scopeonly. Roles are resolved live at authorization time (§5). Thescopeclaim is a pre-filter and an RLS context feed — never the decision.
4.3 Security Profiles — FAPI-Grade OAuth by Default
Sentin8:Auth enforces per-client security profiles — a named bundle of
required mechanisms, evaluated at the authorize, token, and introspection
endpoints. Profiles are data (rows in the auth store), not build flags, so a
regulated tenant can run fapi_2_message_signing while a partner runs a
baseline profile — on the same deployment.
| Profile | PAR | JAR | DPoP | mTLS | JARM | iss | Signed introspection (RFC 9701) |
|---|---|---|---|---|---|---|---|
standard / fapi_1_baseline | — | — | — | — | — | — | — |
fapi_1_advanced | required | required | mTLS or DPoP (sender-constraining) | optional | required | — | |
fapi_2 (default posture) | required | covered by PAR | required | optional | required | required | — |
fapi_2_message_signing | required | covered by PAR | required | optional | required | required | required |
Enforcement details that make the profiles real rather than cosmetic:
- PKCE
S256is mandatory on every profile;plaindoes not exist. - Response types are
codeonly — the hybrid flow (and its entire historical CVE family) is not implemented. - Client authentication methods are allow-listed per profile. FAPI
profiles admit only
tls_client_auth,self_signed_tls_client_auth, orprivate_key_jwt—client_secret_basic/client_secret_postare rejected for confidential clients. A shared secret is not sender-specific; under FAPI the platform refuses to pretend otherwise. - Grant types are constrained per profile — authorization-code + refresh + device-code (RFC 8628) + CIBA + token exchange (RFC 8693, the delegation grant).
- PAR (RFC 9127) pushes authorization parameters server-side, killing the parameter-tampering surface of the plain front-channel.
- JAR/JARM sign the request objects and authorization responses;
RFC 9701 signs introspection responses — the
fapi_2_message_signingtenant can prove, cryptographically, what the server said.
4.4 Sender-Constraining — a Stolen Token Is a Useless Token
Every access path is bound to a proof of possession:
- DPoP (RFC 9449) — per-request proof JWTs bound to the client's key and the HTTP method+URI; replay on another channel fails.
- mTLS certificate binding (RFC 8705
cnf) — the token is bound to a certificate; presentation without the private key fails. - HTTP message signatures (RFC 9421) — signed requests for integrations that need them.
Combined with the BFF (§6.4), the consequence is structural: token exfiltration does not yield replayable access.
4.5 Authentication Strength
- Passkeys (WebAuthn) — phishing-resistant credentials are a first-class sign-in path.
- MFA (TOTP, RFC 6238) — secrets are 20-byte random keys stored encrypted; enrollment is challenge-confirmed; disabling requires password re-confirmation.
- Enterprise federation — OIDC and SAML connections (SCIM 2.0 for user/group provisioning), so workforce identity lives where the enterprise controls it.
- CIBA — decoupled approval flows for devices and assistants.
- Bot protection & lockout — sliding-window rate limits per IP
(default 10/min) and per email (default 5/min) return
429 + Retry-After; after 5 consecutive failures the account locks (default 15 min), with state durable across restarts. Credential stuffing gets loud, slow, and expensive. - Back-channel logout (and webhooks) — sessions die at the server, not when the browser next asks.
4.6 Selective Disclosure & Verifiable Credentials
Sentin8:Auth verifies SD-JWT (selective-disclosure JWT) presentations
for wallet-based sign-in and issuance: issuer-signature verification over
per-claim SHA-256 disclosure digests, key binding (cnf.jwk + optional
KB-JWT), and Status List 2021 revocation checks (compressed bitstring,
cached). A relying party receives exactly the claims the user disclosed —
nothing more — and can prove non-revocation without an online call to the
issuer for every attribute.
5. Pillar III — Shield: One Authorization Engine, Three Models
Shield is the platform's embedded, declarative, fail-closed policy engine — the single authoritative decision point in the pipeline. One engine, three authorization models, evaluated together on every request:
| Model | Data | Mechanism |
|---|---|---|
| RBAC | Roles/groups (with nesting) in the identity plane | Role names resolved LIVE from Sentin8:Auth per request (short-TTL cache, env+subject keyed, invalidated by role-change events) — never read from the JWT |
| ABAC | Principal attribute rows (policy_principal_attrs), env-scoped | Attribute conditions in policy rules (risk tiers, clearances, time windows, device posture) |
| ReBAC | Relation tuples (policy_relations) | Entity-hierarchy edges — document in folder in project owned by team of user — size-capped, loaded as parent edges |
5.1 Live Role Resolution — Authorization That Cannot Go Stale
The classic JWT weakness is that embedded role claims freeze authorization at issuance: a demoted admin stays an admin until the token expires. Sentin8's answer is structural — the JWT carries no roles. On every request Shield resolves the caller's roles from the identity plane (re-verifying the raw bearer server-side; it never trusts a caller-supplied role list). A role change takes effect on the next request. Role-resolution failure is a deny, not an allow (fail-closed).
5.2 Policy-as-Data — Auditable, Versioned, Hot-Reloaded, Shadowable
- Policies, schemas, relations, and attributes are rows in RLS-protected
tables, not compiled-in rules:
platformrows ship from each service's registry at enrollment;tenantrows are authored by environment admins (strict mode: tenant sets require a schema). - Shadow mode: a new policy set evaluates and logs
policy.would_deny— production traffic shows you the blast radius — before a singleUPDATEpromotes it toenforced. Promotion and rollback are one statement, versioned and audited. - Hot reload: policy changes stream to every service over the platform bus (one long-lived consumer per process); no restarts, no drift, no "forgot to redeploy the rules".
- M2M parity: token type never bypasses policy. A workload credential hits the same Shield evaluation as a human's.
5.3 Fail-Closed Semantics
| Condition | Result |
|---|---|
| No matching permit in an enforced set | Deny |
| Policy evaluation / cache / role-resolution error | Deny + audit |
| Unparsable policy text at boot | Service refuses to start |
Set disabled or in shadow | Would-deny is logged; never silently allows |
Authorization in Sentin8 fails the way a bank vault fails: shut.
6. Pillar IV — Tenant Isolation & Data Defense
6.1 Row-Level Security Is Mandatory, Not Recommended
Every tenant table in every service carries:
ENABLE ROW LEVEL SECURITY;
FORCE ROW LEVEL SECURITY; -- even the table owner is subject to policies
Policies match the globally-unique env_id, set per connection via the
tenant-context function on the same connection the query runs on
(transaction-local by construction). Two distinct scoping patterns are
provided and enforced: visibility tiers (usr/env/public) and
holder scoping (customer_id-held records).
6.2 No BYPASSRLS Exists — Anywhere
No role on the cluster carries BYPASSRLS. Not the platform relays,
not the schedulers, not the maintenance accounts. The sentin8 superuser
exists for DDL/bootstrap only. Every runtime connection is:
- OAuth-authenticated (PostgreSQL 18) with per-service scopes,
- mapped to a per-service role in that service's own database/schema only (database-per-vertical, schema-per-service),
- RLS-subject, and
- additionally gated by explicit
WHERE env_id = $Nclauses in every handler (RLS is the backstop; the explicit clause is primary).
Cross-schema reads inside a vertical database are declared grants, never blanket ones. A compromised service binary can, at worst, read its own schema in its own tenants — lateral movement to another service's data fails at the database, the last and hardest wall.
6.3 Secrets — Reference-Only, Envelope-Encrypted, Audited
- Application tables store references (
backend://path), never credential material. A leaked database or backup contains no usable provider keys, OAuth secrets, PAN, PIN, CVV, or tokens — by schema. - Values live in Sentin8:Secrets as AES-256-GCM envelopes with a key version, rotatable without downtime (rewrap + version-prune via scheduled maintenance).
- Plaintext exists only inside request handling, wrapped in a zeroize-on-drop value type; metadata projections can never serialize it.
- Every read is audited — machine resolves (M2M-scoped) and human reveals both write audit rows naming who, what, when. An unaudited reveal is not a reveal that can happen.