Security
Last updated: 2026-08-21
This page describes how Tenet is actually built and operated. It is written for the person doing the technical evaluation, not for the person collecting the certificate.
It includes a section on what we do not yet have. A trust page without one is a sales page, and you should read it as such — including ours, if we ever remove it.
The architectural claim, and why it is the whole security story
Most identity vendors' security story is we hold your users' most sensitive data, and here is how well we guard it. Ours is different in kind:
We do not keep the sensitive material at all.
Selfies and identity-document images are processed stream-through — held in memory for the seconds a check takes, then gone. Nothing is written to disk, nothing is logged, nothing is backed up, nothing trains a model.
The security consequence is the point: a breach of Tenet cannot leak a face or an ID document, because there is no store of them to breach. Every control below matters, but none of them matters as much as the data that is not there.
The honest limits on that claim
Three, stated here rather than in a footnote:
- Age estimation and document checks run at a provider. By default that is our own model on infrastructure we control; a third-party fallback also exists. Both receive a selfie transiently. We do not claim images never leave our infrastructure.
- We retain a date of birth for document-based checks. The images are gone; the extracted date of birth is kept in the decision record for seven years. Name, address, and document number are discarded at receipt.
- The audit archive is immutable and cannot be deleted — including by us, including on request. That is what makes it worth anything, and it is disclosed in the Privacy Policy § 3.4.
Hosting and infrastructure
| Application hosting | Hetzner Cloud — Hillsboro, Oregon, United States |
| Databases | PostgreSQL 16, self-hosted in the same deployment |
| Audit archive | Amazon S3 with Object Lock, COMPLIANCE mode (us-east-1) |
| Edge | Cloudflare — DNS, and, when proxying is enabled, DDoS mitigation and WAF |
| Model inference | Modal (our own age model); Yoti as a third-party fallback |
| Document verification | ID Analyzer |
Data residency: United States. Application services, databases, and the audit archive are all held in the United States — production runs in Hillsboro, Oregon. Development is a separate, non-production tier in Helsinki holding no real data; if that ever stops being true, this claim is the first thing that breaks.
Backups. Both databases are dumped daily to object storage on a 30-day retention. That copy is deliberately not under an immutability lock — an unalterable seven-year copy of every audit row is a liability rather than a control, and the archive described below already holds what has to survive. A backup nobody has restored from is a belief rather than a backup, so the restore has been rehearsed against the same mechanism.
Services are independently deployable, and the whole deployment is defined as code in version control — compose files and a committed inventory of every configuration key — rather than assembled by hand in a vendor dashboard that nothing can diff or review.
Environment parity is enforced, not aspirational. Development and production run identical code paths with identical required configuration — only the values differ. A continuous-integration check rejects environment-conditional behaviour: no "generate a secret if missing in development", no check that silently no-ops outside production. The reason is that a guard which does not run in development is a guard nobody has ever seen work.
Encryption
In transit. TLS 1.2 or higher on every external connection, with HSTS enforced
(max-age two years). Certificates are issued and renewed automatically. Internal
service-to-service calls between the orchestrator and a verification vertical are
authenticated with a shared secret.
At rest. All databases and object storage are encrypted at rest by the platform.
At the application layer. Two categories get a second layer of AES-256-GCM encryption with keys held outside the database, so that database access alone does not yield plaintext:
- the opaque subject reference a customer supplies
- end-user contact details captured for credential delivery
Key material. Signing keys are loaded from the environment, never committed, and never logged. The wallet's holder key is a special case — see below.
Authentication and access control
Customer API access uses rotatable bearer keys (ak_live_…). We store only a SHA-256
hash — we cannot recover your key, only replace it. Keys are rotatable by you, without
our involvement.
The hosted verification flow authenticates with short-lived ES256 JWTs scoped to a single verification session, carrying no personal data.
Result delivery happens two ways, neither of which puts results in a URL:
- Webhooks signed with HMAC-SHA256 with rotation support, at-least-once delivery, idempotency keys, and a bounded retry budget with dead-lettering.
- A result token on the return redirect that is opaque and carries no result data. It is exchanged over an authenticated back-channel for a binary status. Granular detail is never put in a redirect.
Internal access is least-privilege and multi-factor. No shared accounts. Access is reviewed when someone joins or leaves, and everyone who has it is bound by written confidentiality obligations that survive the end of their engagement.
Tenant isolation is enforced on every query path, not by convention.
Application security
Origin. TLS is terminated at our own reverse proxy with automatically renewed certificates, and a service is reachable only if it is explicitly published — the proxy routes to what opts in, not to whatever happens to be listening.
There is no edge. Our domains use Cloudflare for DNS only: requests reach our origin directly, so there is no web application firewall, no managed DDoS mitigation, and no origin-secret gate in front of us. All three are built and configurable in our stack; none is switched on. We would rather say that than describe a control that is not enabled — see What we do not yet have for what it means and what we do instead.
Rate limiting. Both services enforce token-bucket limits at the origin: one budget per
client address, and a second per tenant once a request carries an API key. A caller may burst
up to its budget and thereafter sustains the refill rate; exhausting it returns 429 with a
Retry-After header. Liveness endpoints sit outside the limit deliberately, so a health probe
is never throttled. This is the half of the problem an edge cannot solve for us, because an
attacker who resolves our origin address talks past every edge rule — and it matters here
because abuse does not cost us bandwidth. Every accepted verification spends a paid vendor
call and writes a record to a seven-year immutable archive, so a flood is money and permanent
storage rather than load. This is live, and was confirmed against production on
2026-08-21 rather than assumed: a concurrent burst was allowed up to the budget and then
refused, on the orchestrator and the age vertical alike.
Security headers. X-Content-Type-Options: nosniff, frame-deny, a restrictive
Content-Security-Policy, and cross-origin opener and resource policies.
Log redaction. Structured logging with a redaction list applied at the logger, covering images, document payloads, authorisation headers, tokens, and secrets. A developer cannot leak an image by logging a request object carelessly, because the layer that would print it strips it first.
Audit integrity. Decision records are append-only, enforced by database triggers that
reject DELETE outright and permit UPDATE only on the two export-tracking columns, and
only once. Enforcement is structural and role-independent, so it holds against a compromised
application. This half is live.
Records export nightly to S3 Object Lock in COMPLIANCE mode, where no credential — including the account root — can delete or shorten retention. The lock is applied per object at write time for exactly seven years, so it is a property of each record rather than a bucket setting somebody could relax later.
Third-party-verifiable results. Each concluded verification can be fetched as a signed
attestation — an ES256 JWS that anyone can verify against our published did:web key at
/.well-known/did.json, with no participation from us. This matters because the
alternative is worse than it looks: a shared-secret signature on a webhook proves authenticity
to the recipient and to nobody else, so it cannot help a customer who has to demonstrate
something to a regulator. An attestation can.
This was verified end to end against production on 2026-08-20, because a claim about what a
third party can check is worth nothing until a third party has checked it. A concluded
verification's attestation was fetched, then verified with no API key, no database access and
no shared secret — only the JWS and the DID document at
https://api.choosetenet.com/.well-known/did.json, signed by
did:web:api.choosetenet.com#key-1 over evidence schema 2026-08-17. The verifying script is
about a hundred lines and depends on nothing but node:crypto, so it can be handed to a tenant,
a regulator or opposing counsel as reference code, or rewritten in any language in an afternoon.
The payload carries what this page says it carries and nothing else: the outcome, the method, the model version, the threshold, a one-way subject pseudonym, and the version and timestamp of the biometric notice the person was shown. No image, no document, no date of birth, no name.
Supply chain. Every change runs dependency and secret scanning in continuous integration. Secret scanning has no path-based exclusions — including for test files, which is a common way live credentials reach a repository. There is deliberately no scanner configuration file: an exemption outlives the situation that justified it, so findings are fixed at the source instead.
Automated policy enforcement. A set of mechanical checks blocks merge on violations of our engineering principles — banned analytics dependencies, cross-service import boundaries, environment-parity violations. These are not code review conventions; they fail the build.
The wallet: non-custodial by construction
The Tenet wallet holds credentials on a person's own device.
The private key is generated inside the device's secure element and is non-exportable. Every signature is the operating system asking the security chip to sign. The key never appears in application memory, in storage we control, or on any server.
The consequence is deliberate and worth being blunt about: we cannot produce a user's key. Not for them, not for us, not under a court order. There is no escrow and no backup. If a device is lost, the user re-verifies and is issued a fresh credential — recovery is re-issuance, never key restoration.
Related properties:
- No product analytics in the wallet. No Amplitude, Mixpanel, Firebase Analytics, or Segment, and no fingerprinting attribution SDKs. This is enforced by a dependency deny-list that fails the build. Diagnostics are PII-free and IP-dropped.
- Device policy. Devices without hardware-backed key storage are refused. Devices without a biometric or passcode are required to set one up. Rooted and custom-ROM devices are warned, not blocked — a user who has taken control of their own device is not our adversary.
Payments
Tenet does not process, transmit, or store cardholder data. Customer invoicing runs on Stripe, out of band from the platform: an invoice is issued and paid on Stripe's own pages, so card details are entered there and never reach us. There is no checkout, no card form, no stored payment method and no payment SDK anywhere in the Tenet platform, and that is what makes the sentence above true rather than aspirational — the way to break it would be to build a card form of our own, and we will not.
No customer has been billed yet. Stripe is named here as the decided mechanism, and it is listed as a sub-processor below before it processes anything, rather than after. It receives business contact details — who to invoice and where — and never verification data: no image, no document, no decision, and nothing about a person being verified.
Sub-processors
The authoritative list, with what each one actually sees:
| Sub-processor | Purpose | Location | Receives verification images? |
|---|---|---|---|
| Hetzner Online GmbH | Application and database hosting | United States (Hillsboro, Oregon) | No |
| Amazon Web Services, Inc. | Immutable audit archive | United States | No |
| Cloudflare, Inc. | DNS, and edge protection when enabled | US / global edge | In transit only |
| Google LLC | Business email (Google Workspace) — the mailboxes on this site | United States | No |
| Stripe, Inc. | Customer invoicing and payment. Decided, not yet in use | United States | No |
| Modal Labs, Inc. | Tenet's own age-estimation model | United States | Yes — transient |
| Yoti Ltd | Third-party age estimation (fallback) | United Kingdom | Yes — transient |
| ID Analyzer | Document verification, face matching | United States | Yes — transient |
We give at least three days' notice before adding or replacing a sub-processor, by updating this page and emailing every active customer at the address given during onboarding. There is nothing to subscribe to. That notice period is also your window to object, and the DPA sets out what happens if we cannot resolve it.
Incident response
We commit to notifying affected customers within 48 hours of becoming aware of a security incident affecting their data — including what we know, what we do not yet know, and what we are doing. We would rather send an incomplete notification quickly than a polished one late.
Post-incident, we publish what happened and what changed.
Reporting a vulnerability
Email security@choosetenet.com. That mailbox receives mail from outside our organisation and is monitored — verified with a real message on 2026-08-17.
Our commitments: acknowledgement within 2 business days; an assessment within 10; credit if you would like it; and we will not pursue legal action against good-faith research that respects user privacy, avoids degrading the service, and gives us reasonable time before disclosure.
Please do not test against real users' data, run automated scanners at volume against production, or access an account that is not yours. If you need a test environment, ask.
There is no paid bug bounty. What is on offer is the response above — acknowledgement, an assessment, credit if you would like it, and no legal action against good-faith research. Said plainly, because a disclosure policy written well can be mistaken for one.
What we do not yet have
We are a pre-launch company. Here is what an evaluator would reasonably ask for and what the honest answer is today.
| Status | |
|---|---|
| A web application firewall, and edge DDoS mitigation | None. Cloudflare serves our DNS and nothing more, so traffic is not inspected or filtered before it reaches us. Turning it on is not a switch: proxying alone is bypassable by resolving the origin address, so it only becomes a defence together with an origin-secret gate and a narrowed firewall — and a half-configured edge is worse than none, because it looks like protection. We would rather do it deliberately, when there is traffic worth the attempt, than in time for a launch |
| Rate limiting that survives more than one instance | Partial. The origin limits described under Application security are live and enforced as of 2026-08-21. Each budget is held in the process that serves the request, so it is a per-instance budget — we run a single instance of each service, which makes that the same thing today and stops making it the same thing the moment we scale out. The store sits behind an interface for exactly that reason, so a shared one replaces it without changing how the limits behave |
| Redundancy | None. A single host, deliberately — a cost decision at current volumes. This is not a resilience posture and should not be read as one |
| SOC 2 Type II | In progress. No target date — we will not name one we cannot hold |
| ISO 27001 | No. Not planned near-term |
| Independent penetration test | In progress. No target date, for the same reason |
| Third-party audit of non-retention | No for Tenet's own pipeline. Our third-party age-estimation fallback maintains its own external audits |
| Bug bounty | No. Responsible disclosure only, per above |
| Cyber insurance | None. Not carried today |
| Formal security training programme | None at current headcount |
| Background checks on personnel | None at current headcount — two people, both founders |
| Status page | None. Incidents reach affected customers by email; see Incident response above |
We would rather tell you this now than have you discover it in a questionnaire. If a control here is a blocker for you, tell us — knowing which one is what tells us where to spend first.
Questions
security@choosetenet.com for anything on this page, including security questionnaires. We will complete a reasonable questionnaire rather than insist you accept this page instead.