Skip to content

Product documentation

Security & compliance dossier

The trust model, threat model, and shared-responsibility split a security or compliance reviewer needs to assess Thoryn — grounded in code and ADRs.

Security & compliance dossier

Draft. This dossier is the annex a security or compliance reviewer reads to assess trust. It goes deeper than the white paper's Security & trust model section: every claim below is grounded in the code or ADR named in each section's source anchor. Where the platform deliberately does not do something, this dossier says so — there are no aspirational claims.

Thoryn is a multi-tenant SaaS OAuth 2.0 / OIDC identity-broker platform, shipped both as a managed sovereign service and as a self-managed distribution built from the identical artefacts. This page frames the trust model, states the threat model, and draws the shared-responsibility line between the two delivery models. The focused pages that follow carry the mechanism-level detail:

TopicWhat it covers
Tenant isolationThe tnt claim, cross-tenant 404 (not 403), the trusted-issuer SSRF boundary, per-tenant crypto version-prefixing, tenant-never-in-URL
Signing & cryptographic custodyES256 in Vault/OpenBao Transit (keys never leave the backend), per-tenant HMAC, version-prefixed identifiers, the verifiable hash-chained audit trail
Token securityRefresh-token rotation & replay/family revocation, fingerprint binding, single-use codes + PKCE, DPoP/mTLS sender-constrained tokens, RFC 9457 error hygiene
SSRF & outbound HTTPOutboundUrlGuard / ReactiveOutboundUrlGuard, the older UrlSafetyValidator, and the SSRF-safe per-tenant JWKS transport
Privacy & complianceGDPR access (Art. 15) & erasure (Art. 17), DSAR fan-out, per-tenant retention, NIS2 incident logging
CI security guardsThe build-time scripts/check-*.sh family that makes these invariants un-regressable

Trust model

The identity-broker pattern — the hub owns no user data

The authorization hub (servers/authorization-hub/) is a pure identity broker. It issues and verifies tokens and speaks the OAuth/OIDC protocols; it never stores user profiles, passwords, or organisation records. All identity comes from federation members — Thoryn's own Identity Service (servers/federation-members/identity-service/), or a customer's own enterprise IdP (Entra ID, Okta, Google Workspace, a SAML provider, …).

This is a structural security property, not a convention:

  • The hub module contains no User, Organisation, or credential entity. Claim enrichment reads claims from the federation member's ID token — it never queries a user table.
  • A compromise of the hub yields protocol state and signing-authority (see signing custody), but not a directory of user PII.
  • Personal data — and therefore the GDPR obligations attached to it — lives in the identity member that owns it, which is where export and erasure are implemented.

Source: CLAUDE.md → "Identity broker pattern"; servers/authorization-hub/.

The four planes and their boundaries

PlaneModuleExposureAuth
Brokerauthorization-hubPublic (OAuth/OIDC endpoints)Per-flow (PKCE, PAR, client auth, DPoP/mTLS)
Identityfederation-members/*Internal + federated callbacksOIDC / SAML; PAR + OIDC to the hub
Customerproduct-api + api-gatewayPublic self-service APIBearer JWT, tnt claim, tenant:* scopes
Operatoradmin/** scope namespaceVPN-gated, no public ingressadmin:* scopes, separate surface

The operator plane (admin:*) is a separate scope namespace that structurally cannot reach the customer plane: no admin:* scope is ever present in a tenant-admin token, so a tenant admin cannot grant themselves cross-tenant reach. Conversely the customer plane's tenant:* scopes never authorise cross-tenant operations. The scope-namespace split is the privilege boundary between "a customer administering their own tenant" and "a Thoryn operator (or, self-managed, the customer's own operator) administering the platform".

Source: CLAUDE.md → "Customer plane"; ADR 2026-04-25-customer-plane-product-api.md.

Multi-tenancy and per-tenant issuers

Each tenant is its own OIDC issuer (https://{slug}.hub.<platformDomain>), signed by a tenant-scoped key (tenant-{slug}); the default tenant issues as https://hub.<platformDomain> signed by sas-ecdsa-jwt-key. A tnt claim minted at the hub rides every token and is re-checked in every downstream service. This gives each tenant the cryptographic isolation of a dedicated issuer while thousands of tenants share one deployment. The mechanics — and the SSRF boundary that keeps a forged issuer from steering an outbound fetch — are on the tenant isolation page.

Threat model

The controls below are the ones a reviewer will ask about. Each row links to the page and the code that implements it. This is not an exhaustive risk register; it is the set of adversaries the architecture is explicitly built to resist.

Adversary / riskControlWhere
Cross-tenant access — tenant A reads or mutates tenant B's datatnt-claim re-check on every request; cross-tenant returns 404 (no existence leak); tenant is taken from the token, never the URL pathTenant isolation
Forged issuer / SSRF via token — attacker mints a token with an iss pointing at an internal hostTrusted-issuer allowlist rejects unknown iss before any JWKS fetch; JWKS is fetched only from a fixed in-cluster Service with the Host header varyingTenant isolation, SSRF
Outbound SSRF — tenant/admin-supplied URL (federation discovery, webhook, SCIM base URL) aimed at cloud metadata or an internal serviceOutboundUrlGuard / UrlSafetyValidator reject private / loopback / link-local / metadata / CGNAT / IPv6-ULA targets before dispatchSSRF
Signing-key theft — heap dump or memory-disclosure bug leaks the private keySigning and audit HMAC/JWS happen in Vault/OpenBao Transit; the JVM never holds private key materialSigning & crypto
Refresh-token theft / replayRotation with replay detection; a replayed (rotated) token triggers family revocation (REVOKE_AUTHORIZATION, RFC 9700 §2.2.2); optional device-fingerprint bindingToken security
Token export / bearer liftOptional sender-constrained tokens — DPoP (cnf.jkt, RFC 9449) and mTLS (cnf.x5t#S256, RFC 8705)Token security
Information leak via errorsOne RFC 9457 problem-details error shape platform-wide; detail strings are sanitised by construction — no tenant ids or token contents in responsesToken security
Audit-log tampering — an attacker with DB access alters or reorders audit historyPer-tenant hash-chained, HMAC + ES256-signed audit rows; a tamper is detected on verify as a HASH_MISMATCH / BROKEN_LINK / BAD_SIGNATURESigning & crypto, Privacy & compliance
Cross-tenant linkability — correlating the same subject/credential across tenantsPer-tenant HMAC (not plain hash) on audit content; per-tenant signing/encryption keysSigning & crypto
Log injection / CRLF forgingMdcSafe allow-list + length cap on every MDC write; a CI guard bans raw MDC.putCI guards
Container escape / lateral movementPods run runAsNonRoot + seccomp RuntimeDefault; automountServiceAccountToken: false on public-facing deploymentsCI guards
Silent security regressionEvery invariant above has a build-time guard that fails the PR; a regression cannot mergeCI guards

Explicitly out of scope (stated honestly)

  • Audit-row deletion detection. The audit chain is tamper-evident against mutation and reordering, and detects a deleted row as a sequence gap when the chain is walked contiguously. A Merkle-anchor scheme that would independently prove no row was ever excised is a documented future option, not shipped (ADR 2026-05-09-verifiable-audit-chain.md).
  • Redirect re-validation after an SSRF check. OutboundUrlGuard validates the target URL at submission time; callers whose target is tenant-controlled must configure the HTTP client to refuse redirects (see SSRF).

Shared responsibility (managed vs. self-managed)

Thoryn runs the identical artefacts in both delivery models; the entire deployed state is rebuildable from git with zero manual steps (Helm charts, Flyway migrations, config as code). What differs is who operates the substrate.

ConcernManaged (sovereign SaaS)Self-managed
Application security (token flows, tenant isolation, SSRF guards, error hygiene)Thoryn — same codeYou run the same code
Cryptographic backend (Vault/OpenBao Transit)Thoryn operates it; keys never leave itYou operate it; keys never leave it
Production seal for the secrets backendKMS/HSM auto-unseal (no operator-readable unseal key)Your responsibility — do not ship the Secret-stored-key model to production; use a KMS/HSM seal
Secrets (DB, Redis ACL, client secrets)Thoryn-provisioned per-service SecretsYour cluster Secrets, same chart pattern
Network policy, TLS termination, ingressThoryn-operatedYours
Tenant configuration (clients, federation, RBAC/FGA, policy)You — self-service via console / CLIYou — same self-service surface
GDPR data-subject workflows (export, erasure, DSAR)You initiate; the platform executesYou initiate; the platform executes
Incident response & regulator reportingShared — Thoryn operates, you own the tenant relationshipYours

The one production-hardening step a self-managed operator must perform themselves is the secrets-backend seal: staging stores the OpenBao unseal key in a Kubernetes Secret, which is explicitly not promotable to production without a KMS/HSM auto-unseal seal (seal "awskms" / seal "pkcs11"). This is called out in ADR 2026-06-07-vault-to-openbao.md and on the signing & crypto page.

Sources: CLAUDE.md → "Reproducible deployments"; white-paper "Delivery models"; ADR 2026-06-07-vault-to-openbao.md.