Summary
Tenant isolation
Every tenant-owned table carriestenant_id with ENABLE and FORCE ROW LEVEL SECURITY and a policy on the
request’s tenant, so the table owner cannot bypass it and a query that forgets its filter returns zero rows, never
another tenant’s data. Tables declare this through the scoped-table pattern (belongsToTenant()); hand-written RLS is
not allowed. A cross-tenant miss answers “not found”, never “permission denied”, so a caller learns nothing about
other tenants. The check that every tenant table in a database has both flags is planned.
Authorization
- One authenticated principal per request, resolved once; handlers never read the caller from the body or client headers.
@stackra/iamanswers deny when nothing matches, and an explicit deny beats every grant. Every action has a permission (code generation refuses one without), and every link of an actor chain (a service and the user it acts for) must hold it (0462).- The action pipeline order is fixed (tenancy, permission, precondition, entitlement, flag, idempotency); an app configures the steps but cannot reorder or skip them.
- Level L3 and L4 actions need a step-up of authentication.
Identity
ZITADEL is the identity provider;@stackra/identity is a provider-neutral OIDC client
(0206). Web sessions use a BFF
with an HttpOnly cookie, code flow and PKCE; mobile uses PKCE with a rotated refresh token in secure storage; access
tokens are short-lived JWTs.
Verification at the edge
The design (0437, proposed):- Access tokens are verified against the identity provider’s JWKS on every request.
- The edge Worker forwards tenant and product headers with a timestamp, a key id and an HMAC over the headers, method, path, body hash and request id. The API trusts them only when the signature is valid, younger than the validity window (60 seconds by default) and its request id unseen; otherwise only public endpoints answer. Two keys are live at once, so the secret rotates without a cut-over.
- The origin accepts traffic only from the edge (Cloudflare Tunnel or authenticated origin pulls); the signature sits on top of that lock, never in place of it.
Audit evidence
- Every action and data change is audited, and the evidence enters the outbox in the same transaction as the change.
@stackra/access-logkeeps each tenant’s records in a SHA-256 hash chain: a changed, removed or reordered record breaks verification.stackra audit:verifychecks an exported proof bundle offline (the chain and its checkpoints).- Built (STA-1579):
ExportAuditActionstreams a tenant’s write audit as NDJSON (chain fields included, so the receiver can verify it) to a SIEM’s HTTPS collector, under the L2 permissionaccess_log.audit_log_entry.export. Sign-in, failed sign-in, second-factor, lockout and sign-out events from ZITADEL enter the same per-tenant chain through a signed webhook (ZitadelAuthEventsService). - Audit retention: access records are kept 400 days by default (
AccessLogConfig.retentionDays, at least 90). The write audit’s retention is the settingAccessLogConfig.auditRetentionDays; until the operator states it, the write audit is kept without limit, and no job deletes from it yet. - Designed (0212): a KMS-signed checkpoint daily, append-only storage under S3 Object Lock, 13 months hot, then cold storage; money events kept seven years. A tenant can view and export its own audit log.
Encryption and keys
- In transit everywhere; at rest through the cloud provider.
- Tenant data keys are generated by AWS KMS and stored only wrapped, bound to the tenant by the encryption context, so a wrapped key copied to another tenant does not unwrap. A tenant can bring its own KMS key (0415).
- A per-subject key is derived from the tenant key and a per-subject salt: destroying the salt makes that one person’s sealed fields unreadable. Deleting a tenant’s wrapped keys crypto-shreds the tenant (0188).
- Operator access to production is break-glass only: MFA, one hour, alerted and audited (designed, 0188).
Secrets
Code names a secret; it never holds a value. Production values live in Infisical and reach runtimes by sync on deploy; there are no.env files, and CI reads secrets through short-lived OIDC logins
(0173). Every commit is scanned for secrets.
Supply chain
What every package version has, from the release workflow and the registry (0453, 0174):
Verify the SBOM signature of a release:
Data protection
Personal data is classified per field and redacted from logs, events and errors; telemetry carries none. Exports, erasure and retention defaults follow 0189. Where data lives is Data residency.Reporting a vulnerability
Do not open a public issue. Use “Report a vulnerability” on the repository’s Security tab, or emailsecurity@figentra.com, with the affected package and version, steps to reproduce and the impact. We acknowledge
within three business days and credit you in the advisory if you want (SECURITY.md). Never put a secret value in a
report.