Security
This page is a condensed, public summary of the enterprise console’s security controls. Every row cites the file or route it comes from — ask under NDA for repository access to verify it yourself rather than taking our word for it.
Not a certification
This page describes controls that exist in checked-in, tested code or in a documented operational procedure — it is not a SOC 2 report, a penetration-test result, or a certification of any kind, and none of those is claimed anywhere on this page. A completed SOC 2 report can only be issued by an independent CPA firm after their own examination. See /regulations for how these controls map to specific EU AI Act, SOC 2, and NIST AI RMF requirements.
Who can do what, and whether one tenant can ever see another tenant’s data.
4 roles (internal_admin, org_admin, approver, auditor) and 43 permissions, enumerated as data rather than scattered across `if` statements, enforced at every console route before the route body runs.
lib/console-permissions.tslib/console-guard.tsPostgres Row-Level Security enforced at the database ROLE level, not only in application `WHERE` filters — a dedicated non-superuser, non-BYPASSRLS role is SET LOCAL ROLE’d per transaction so RLS actually engages even though the platform’s own default connection is a superuser. The readiness check fails closed (503) if the effective role could still bypass RLS.
infra/postgres/005_noa_console_rls.sqlinfra/postgres/014_noa_console_app_role.sql/health/ready (live check)How an operator proves who they are, and how an enterprise IdP plugs in.
WebAuthn/passkey re-authentication required before sensitive mutations (audit export, key rotation, trust-root management, device pairing), with a 10-minute freshness window.
lib/console-guard.tslib/console-webauthn.tsOIDC (PKCE + nonce + state) and SAML (signature-verified, replay-cached).
app/admin/auth/oidc/[connectionId]/{start,callback}/route.tsapp/admin/auth/saml/[connectionId]/{start,callback,metadata}/route.tsSCIM v2, bearer-token authenticated and tenant-scoped, with deprovision-cascade. The bearer token is itself revocable.
lib/console-scim.tsTamper-EVIDENT, not tamper-PROOF: a rewrite is detectable, not impossible for someone who holds both the database and the signing key.
Append-only audit events, each linking to the previous event’s hash (sha256 of a canonical JSON form). A step-up-gated export includes the console’s own recomputed chain-verification result alongside the data, so a buyer’s auditor can re-derive the same check independently instead of trusting our attestation.
lib/console-audit.tslib/console-audit-chain.tsapp/admin/audit/export/route.tsEach chain’s head can be signed with an Ed25519 key held outside the database (optionally timestamped by an external RFC 3161 authority) and streamed to a customer’s own SIEM as NDJSON. This makes a later rewrite of an earlier event detectable to anyone holding a copy of the anchor — it does not by itself prevent a rewrite by an actor who holds both the database and the signing key. Inert until an operator configures a signing key; never fabricates an anchor and never exits silently as if it had.
scripts/audit-anchor.mjsscripts/siem-export.mjsOne verified evidence bundle at a time, streamed as its canonical bytes so `sha256(exported file)` matches a response header — no re-derivation trust required on the reader’s side.
app/admin/evidence/export/route.tsTwo independently re-runnable mechanisms, not a one-time claim.
The canonical Node (node:crypto) Ed25519 verifier vs. the browser-swap module (@noble/curves) this site’s own /playground uses, checked against 6 adversarial vector classes (cofactor-malleated signatures, small-order keys, out-of-range scalars, non-canonical encodings, loose base64 decoding, cross-implementation). Re-runnable yourself.
What is stored at rest, and in what form.
The email is stored only as a keyed hash; the login link token and session bearer are stored as sha256 hashes compared in constant time; the session subject is an opaque internal user id, never the email.
lib/console-mobile-auth-core.tslib/console-mobile-auth.tsThe human-readable approval-hold display is HPKE-encrypted to the approver device’s own key before it reaches the relay; the relay’s storage columns hold only ciphertext, encapsulated key, and AAD — no plaintext display column exists.
infra/postgres/007_noa_console_signed_execution.sqlinfra/postgres/009_noa_console_mobile_hpke.sqlA first (and currently only) consumer of each organization’s configured retention window: purges expired auth artifacts and aged security events on an explicit --apply run (dry-run is the default). Deliberately does not touch the append-only audit chain itself.
scripts/retention-sweep.mjsHow to report a problem, and how change gets shipped.
RFC 9116 security.txt, machine-readable, at the standard well-known path.
Idempotent, checksum-tracked Postgres migrations, run against a real PG16 instance in CI before merge.
scripts/migrate.mjsscripts/migrate-smoke.shA restore-drill procedure — dump production, restore into an isolated throwaway Postgres, then verify migration-ledger checksums, row counts against source, audit-chain continuity, and tenant RLS negative tests — first EXECUTED 2026-07-21: the production dump restored cleanly, all 24 applied migration checksums matched the repository with zero drift, the restored row counts matched source, and tenant RLS isolation held on the restored copy.
docs/PRODUCTION-RUNBOOK.mdNamed alert conditions are documented (readiness failures, 5xx rate, login denial/rate-limit events, RLS errors, audit-chain conflicts, SCIM failures). No fully automated alerting pipeline runs today; the SIEM export above gives an operator-configured path to feed a customer’s own monitoring stack.
docs/PRODUCTION-RUNBOOK.mdscripts/siem-export.mjsResidual risks — named, not hidden
Found a vulnerability, or need repository access under NDA?
Report a security issue via /.well-known/security.txt, or email us directly.