Security

What we protect, how to reach us, and what we do not claim.

Signet guards other people’s users, so a discovered vulnerability obligates a response: fix, disclose to affected customers, re-certify. This page is how we meet that obligation: how to report a vulnerability, the facts a security review needs, and a plain list of what we do not yet claim.

DisclosureDataHow we testWho can reach itIncidentsSubprocessorsWhat we refuseCode and youNot claimed

Your data stays in a database that is yours.Every user, session and secret is a row in a database you can inspect: yours on your own metal, a dedicated one on hosted.
Tokens are stored as digests.A stored session row is not a usable credential, and passwords exist only as salted password hashes.
Nothing phones home.No telemetry, no licence pings, no calls to us in the sign-in path. The same build runs air-gapped.

Reporting a vulnerability

Email security@signetauth.com. A plaintext report is fine and is not a reason to delay. Include what you found and its impact, how to reproduce it, and the Signet version and deployment shape if you know them. Do not open a public issue or route it through an ordinary support ticket; a support ticket is read by more people, sooner, than a security report should be.

StageOur commitment
AcknowledgementWithin 2 business days, from a human.
First assessmentWithin 7 business days: severity reading and whether we can reproduce it.
Fix or planFor anything assessed high or critical, a fix or a dated plan within 30 days.
Customer disclosurePart of the fix, per the standing obligation, not at our convenience.
Windows a small operation can hold on a bad week. A published window we miss would be worse than no window.

These numbers are deliberately ones a small operation can hold on a bad week; a published window we miss would be worse than no window. A report classified security is handled by a person: never auto-replied, never agent-triaged, never auto-closed.

Safe harbour. Good-faith research under this process will not be met with legal action. Good faith means you stop at proof, do not access or retain data that is not yours, do not degrade service for others, and give us a reasonable chance to fix the issue first. No NDA is required to report. There is no bug bounty; we offer credit in the release notes and a named acknowledgement if you want them.

In scope: the engine and every endpoint it serves, the licence-verification path, and our hosted instances. Out of scope: scanner output with no demonstrated impact, volumetric denial of service, social engineering. Resource-exhaustion bugs, where a cheap request costs us disproportionately, are in scope and wanted. The machine-readable advertisement lives at /.well-known/security.txt.

Where your data lives

Where a user, a session and a secret actually live, and what they look like there.

Every user, session and secret is a row in your own database, PostgreSQL or SQLite, from the first sign-in. Transport is TLS on every served surface. On SQLite, Signet encrypts the whole file with SQLCipher. On PostgreSQL, encryption at rest belongs to the database and volume layer. Whoever runs that layer sets it up: you on your own metal, us on hosted instances. Signet stores passwords only as salted password hashes, and session tokens only as digests. A stored session row is not a usable credential.

Nothing phones home: no telemetry, no licence pings, no calls to us in the sign-in path. Sign-in reaches only the identity providers and services you configure. The same build runs air-gapped under an enterprise licence.

How changes are tested before they ship

Here is what we check changes against before they ship. The last full run of better-auth’s own 280-check conformance suite passed all 280. All 14 end-to-end acceptance flows passed against a live instance. Those results are dated 2026-07-22. The running engine serves them, machine-readable, at /certification. A re-run on 17 September 2026 found differences we are still working through. Most come from a deliberate change in where Signet mounts SCIM. Until we settle the rest, this page does not claim that every release passes the suite in full.

Beyond conformance, our own adversarial assessment is on record and is replayed without a human: every night a probe suite stands up a throwaway instance and re-tests the security behaviours that assessment established, from enumeration resistance and token fences to session revocation on password reset and second-factor throttling. This is self-run testing, named as such on purpose. It exists so a regression is caught the night it ships; it is not assurance from anyone independent.

Who can reach your deployment

Hosted: signup provisions the instance automatically. Operators who run the platform can reach the instance they operate for you; that is stated here rather than discovered later. Access is credentialed, logged and limited to operating the service.

On your own metal: we cannot reach your deployment. There is no remote administration path, no callback channel, and nothing in the engine that lets us in. Support happens with you, on evidence you choose to share.

Inside the product, the admin API follows the same rules. It is protected by an instance-level operator credential that is compared in constant time. An organization can also hold a delegated administrative credential, scoped to its own tenant and limited to a short, explicit allowlist of operations; every administrative route outside that allowlist refuses the delegated credential before any handler runs, and that refusal set is enforced by a test matrix covering every route and method, not by review. Delegation cannot escalate: a delegated credential never grants authority it does not itself hold, and authority never widens along a delegation chain. Credentials are bound to their tenant when issued, not inferred from the request, and a cross-tenant request is refused without revealing whether the named target exists.

If we have an incident

If we determine that a security incident on our side affects your data, we notify you within 72 hours of that determination, with what we know, what we have done, and what we recommend. Contractual response targets beyond this live in signed agreements; this window is the floor we publish for everyone.

Subprocessors

On your own metal under an enterprise licence: none. Nothing leaves your infrastructure.

For the hosted service and this website, the parties that touch traffic or data are: Hetzner (instance compute and storage, Ashburn, Virginia, United States), Cloudflare (DNS and inbound email routing), Resend (outbound transactional email relay), and Stripe (payment processing and subscription billing). Checkout is hosted by Stripe: your card details go to Stripe and never touch our systems. The list changes only with notice on this page.

What we refuse to do

Agent access is where an identity provider is most tempted to be helpful. These are the places Signet refuses instead, and each one is a refusal a reviewer can test rather than a posture we describe.

What we refuseStandingWhat that means for you
Creating a person from an agent’s tokenneverIf the human behind the agent has no account here, we refuse the request rather than making one. Your list of users stays your list of users.
A long-lived agent credentialnone issuedAn agent arriving through your customer’s identity provider gets no refresh token at all. The token also dies with the grant behind it.
A token for a client that identifies itself by a URLrefusedIt can prove who it is, and we stop there. One gap in the standard causes it: neither the MCP profile nor the grant draft says which customer a self-registering client belongs to, and we would rather refuse than guess wrong about a tenant. Clients your customer registers work today.
Keeping a usable copy of a bearer you re-presenthashedEvery bearer credential a client holds and presents again lives in our database as a hash. Emailed one-time codes are the exception, and they live for minutes.
Minting a token a scanner cannot spotprefixedTokens we mint now carry a class prefix, so a secret scanner flags one in a repository. Tokens minted before that change carry none, and they keep working.

What the code enforces, and what relies on you

A security review asks two questions. What does the software do on its own? And what works only if someone keeps a promise? The first list below is code. The second list is the rest: settings, habits and things we have not built. A claim we could not trace to code went in the second list. We checked every line against the engine’s main branch on 4 October 2026.

The engine source is not public. The file names tell a reviewer with access where to look.

Enforced by codeRelies on policy or on you

Enforced by code

What the engine does by itself. Some lines are defaults that a setting can change. The second list names them.

  • Signet hashes passwords with scrypt. The settings are N=16384, r=16 and p=1, with a 64-byte output and a new 16-byte random salt for each password. These match better-auth, so its hashes keep working. The last step compares the hashes in constant time. App-user accounts, a separate kind of user, get Argon2id instead.Where to look: crates/signet-core/src/crypto/password.rs, crates/signet-server/src/app_user.rs
  • Signet upgrades imported passwords at the next sign-in. It accepts bcrypt, Argon2, PBKDF2 and PHC scrypt hashes from a migration. After a correct sign-in, it rewrites the hash as scrypt.Where to look: crates/signet-core/src/crypto/password.rs, crates/signet-server/src/lib.rs
  • A stolen session row cannot sign anyone in. The database keeps only a SHA-256 digest of each session token. Every lookup hashes the token it was given first. A test reads the source and fails if a lookup skips that step.Where to look: crates/signet-server/src/lib.rs, crates/signet-server/tests/r118_session_token_sites.rs
  • Signet keeps only digests of API keys, OAuth client secrets and OAuth codes too. The digest is SHA-256. The server makes these values at random, so a slow hash would add nothing.Where to look: crates/signet-core/src/crypto/opaque.rs, crates/signet-server/src/api_key.rs, crates/signet-server/src/oauth_client.rs
  • Tokens and codes come from a secure random source. Signet draws session tokens, API keys, client secrets and one-time codes from a cryptographic random number generator.Where to look: crates/signet-core/src/crypto/token.rs, crates/signet-server/src/api_key.rs
  • Signet signs the session cookie and hides it from page scripts. The cookie carries an HMAC-SHA256 signature and the flags HttpOnly and SameSite=Lax. Signet adds Secure when your public address starts with https://.Where to look: crates/signet-core/src/crypto/cookie.rs, crates/signet-server/src/lib.rs
  • Signet refuses requests from other websites. Sign-in, sign-up, password change and many other routes check where a request came from. A request with a cookie, an Origin or a Referer must come from your own address or one you listed. Signet refuses a cookie with no Origin or Referer, and an Origin of null. A server call that sends none of these skips the check.Where to look: crates/signet-server/src/lib.rs, crates/signet-server/tests/l2_routes.rs
  • Rate limits are on by default. Sign-in, sign-up, password change and email change allow 3 requests per 10 seconds from one address. Reset emails, verification emails and SMS codes allow 3 per 60 seconds. Other routes allow 100 per 10 seconds.Where to look: crates/signet-server/src/rate_limit.rs, crates/signet/src/lib.rs
  • Accounts lock after repeated wrong passwords. This is on by default. Five failures in 10 minutes lock the account for 15 minutes. Each new lock doubles, up to 24 hours. The count follows the email, not the network address. A locked unknown email gets the same answer as a locked real one, so the lock does not reveal which accounts exist.Where to look: crates/signet-server/src/abuse.rs, crates/signet-server/tests/t23_abuse.rs
  • Signet encrypts two-factor secrets in the database. It seals the authenticator secret and the ten backup codes with XChaCha20-Poly1305. It compares authenticator codes in constant time.Where to look: crates/signet-server/src/twofactor.rs, crates/signet-core/src/crypto/secret.rs
  • Signet caps two-factor guessing. One sign-in attempt allows 5 wrong codes, then Signet cancels it. Ten wrong codes across attempts lock two-factor for 15 minutes.Where to look: crates/signet-server/src/twofactor.rs, crates/signet-server/tests/l7_twofactor.rs
  • One organization cannot read another. Routes that read or change an organization look up the caller’s membership in it, and refuse without one. Signet ties each credential to its organization when it issues it. A request cannot name a different one.Where to look: crates/signet-server/src/org.rs, crates/signet-server/tests/r180_tenant_binding.rs
  • The audit log refuses edits. Admin changes and each new session write a row. A database trigger refuses any change to those rows, or their removal, on PostgreSQL and on SQLite.Where to look: migrations/0068_credential_lifecycle_audit.sql, migrations-sqlite/0001_current_logical_schema.sql, crates/signet-server/src/admin_ops.rs
  • You can rotate signing keys, and Signet encrypts their private halves. An admin prepares a new key, activates it, then revokes the old one. Each step writes an audit row.Where to look: crates/signet-server/src/admin_api.rs, crates/signet-server/src/jwks.rs
  • Signet keeps only a digest of the admin key, and you can rotate it. The database holds a SHA-256 digest of each admin credential. An admin can issue a new one and revoke the old one.Where to look: crates/signet-server/src/admin_api.rs
  • Signet will not start without a long main secret. The secret must be at least 32 characters. So must each secret in a rotation list.Where to look: crates/signet/src/lib.rs
  • A password reset ends every session of that user. This is on by default. The old sessions end in the same database transaction that changes the password.Where to look: crates/signet/src/lib.rs, crates/signet-server/src/lib.rs
  • Signet will not fetch from your private network for an outsider. It may fetch from an SSO provider, an outside key set or a client’s published details. Then it refuses private, loopback and cloud metadata addresses. It connects to the exact address it checked, so a changed DNS answer cannot send it somewhere else.Where to look: crates/signet-server/src/outbound_fence.rs, crates/signet-server/src/sso.rs, crates/signet-server/src/cimd.rs
  • On SQLite, Signet encrypts the whole database file. It opens a SQLite database only with SQLCipher and a 64-character key. It refuses a key written into the config file.Where to look: crates/signet/src/lib.rs, crates/signet-core/src/db/sqlite.rs

Relies on policy or on you

What the code cannot promise. Each line says who has to act.

  • By default, each server counts its own rate limits. The counters live in memory. If you run more than one server, set rate_limit.storage = "database", or each server keeps its own count.Where to look: crates/signet/src/lib.rs
  • Rate limits trust the first address in the X-Forwarded-For header. If you run Signet yourself, put it behind a proxy that overwrites that header. If you do not, a client can write its own value and dodge the per-address limit. The per-email lockout still holds. On hosted Signet, our gateway sets the header. On 4 October 2026 we tested that a forged value does not get around the limit.Where to look: crates/signet-server/src/rate_limit.rs, scripts/host/caddy-signet.Caddyfile
  • You can loosen or turn off rate limits and lockout. Both are settings in signet.toml. A custom rule can replace a built-in limit.Where to look: crates/signet/src/lib.rs, crates/signet-server/src/rate_limit.rs
  • Cookies get the Secure flag only on an https:// address. Set your public address to https://. Signet does not use the __Secure- or __Host- cookie names.Where to look: crates/signet-server/src/lib.rs
  • You choose which other sites Signet trusts. Signet trusts every address in trusted_origins fully. Browsers can call Signet from it, and an SSO provider at that address may sit on your private network.Where to look: crates/signet-server/src/lib.rs, crates/signet-server/src/outbound_fence.rs
  • Two-factor is a choice each user makes. Signet has no setting that requires it for everyone. If your policy needs it for all users, your app has to enforce that.
  • The breached-password check is off by default. So is the password strength score. Turn them on if you need them. When the check is on, Signet sends the first five characters of the password’s SHA-1 hash to Have I Been Pwned, or to a mirror you name. The default length limit is 8 to 128 bytes.Where to look: crates/signet/src/lib.rs, crates/signet-server/src/hibp.rs
  • On PostgreSQL, Signet does not encrypt most data. It encrypts two-factor secrets and signing keys. Emails, names, password hashes and token digests are plain rows. So are the access and refresh tokens that social sign-in providers such as Google send back. Disk and database encryption belong to whoever runs the database: you on your own servers, us on hosted.Where to look: crates/signet-server/src/secret_rotation.rs, crates/signet-server/src/oauth.rs
  • One main secret protects two-factor secrets and signing keys, and signs session cookies. You can rotate it. List the new secret first in SIGNET_SECRETS, stop every server, run signet reencrypt-secrets, then drop the old one. Everyone signs in again once. Signet never rotates it on its own. When to rotate is up to whoever runs the instance.Where to look: crates/signet-server/src/secret_rotation.rs, crates/signet/src/main.rs
  • The main secret sits in a config file or an environment variable. Who can read that file or that process is up to you. Signet checks the secret’s length, not how random it is.Where to look: crates/signet/src/lib.rs
  • A password reset ends sign-in sessions only. OAuth tokens, API keys and app-user sessions keep working. Revoke them yourself after a suspected breach.Where to look: crates/signet-server/src/lib.rs
  • The audit log is safe from Signet, not from the database owner. Someone with full rights on the database can drop the trigger and change rows. To keep a copy they cannot touch, export it from /admin/v1/audit/export and store it somewhere else.Where to look: crates/signet-server/src/admin_api.rs
  • Signet counts failed sign-ins but does not list them. The audit log records each new session. For refused sign-ins it keeps only a count per 10-minute window, with no address and no account. If you need each failed attempt, log it at your proxy.Where to look: crates/signet-server/src/refusal_summary.rs

Both lists are our own reading of our own code. No outside party has checked them yet. The ledger below says what we do not claim.

The not-claimed ledger

ClaimStatusThe honest date
SOC 2not heldType I targeted 2027; the date is set when the first enterprise deal requires it, and readiness work is priced into that deal.
ISO 27001not heldNot scheduled. If a deployment requires it, that is a conversation before a contract, not after.
Third-party pen testnot yet performedAvailable on request; it will be commissioned alongside the first enterprise engagement that asks, and the report goes to customers under NDA when it exists.
SLAcontract onlyResponse targets are written into signed agreements. Outside one, none is offered or implied.

Ask us what we actually test instead: the conformance receipts above are real, dated, and verifiable against a running instance. When a row in this ledger changes, the change arrives with its artifact, not before it.