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 — production requires a dedicated NOINHERIT, non-owner login that SET LOCAL ROLE’s into a non-superuser, non-BYPASSRLS application role per transaction. Pre-deploy rejects privileged runtime credentials, and readiness fails closed (503) if the effective role can 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. The export is bounded honestly: a hard cap of 10,000 events per export — above it the route refuses and names the resumable SIEM stream, rather than silently truncating.
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.
Scoped claim, stated precisely: for the mobile app’s magic-link sign-in artifacts, 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, and the session subject is an opaque internal user id. The console account profile itself does store your sign-in email so we can operate your account — see /privacy.
The human-readable approval-hold display is HPKE-encrypted to the approver device’s own key and persisted as the immutable encrypted_display member of noa_console_hold_artifacts. The signed-in handset fetches that tuple through the direct m1-authenticated mobile approval API; the retired private relay-message table is not the current transport.
infra/postgres/030_noa_console_hold_artifacts.sqlapp/v1/mobile/approvals/route.tslib/console-mobile-approval-auth.tsA 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 PostgreSQL 18 instance in CI before merge. The migration runner refuses every other server major before execution.
scripts/migrate.mjsscripts/migrate-smoke.shA fail-closed logical restore procedure with checked-in drill tooling: restore-drill.sh plus dedicated verifiers for the restored migration ledger, the restored audit chain, and the core-8 invariants. The latest genuine drill (2026-08-29) restored a hash-bound production logical backup into isolated PostgreSQL 18 at exact head 060: 60 migrations and 92 public tables, with every MVCC table fingerprint and the audit and RLS/ACL probes passing. Its bounded verdict is SEMANTIC_PASS_WITH_NON_MVCC_SEQUENCE_BOUNDARY because PostgreSQL sequences were not quiesced. It does not prove provider-native backup restorability, provider PITR configuration or live health, or production RPO/RTO.
scripts/restore-drill.shscripts/verify-restored-migration-ledger.mjsrelease-evidence/production-logical-restore-snapshot-20260829T125200Z.jsondocs/PRODUCTION-RUNBOOK.mdAn automated readiness pager runs from checked-in code: a scheduled workflow probes production, opens one deduplicated P1 incident issue on failure, and closes it after verified recovery. Its cadence is stated honestly — GitHub’s scheduler is best-effort, so every run measures and reports its own gap since the previous probe (observed gaps up to 2h35m are disclosed in the workflow itself) instead of claiming a five-minute interval it cannot keep; a checked-in external-probe entrypoint provides a guaranteed interval when driven by an operator-supplied scheduler. Still open, named plainly: a guaranteed-interval scheduler in production and a public status page. The SIEM export above remains the path into a customer’s own monitoring stack.
.github/workflows/production-readiness-pager.ymlmonitoring/external-probe/probe-and-page.mjsscripts/production-monitor.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.