Security model
seekrit's guarantee is simple: the server never has the information needed to read your secrets. This page states that precisely — including the one feature you can opt into that changes it, and exactly how far that change reaches.
What the server stores
- Secret ciphertext (AES-256-GCM blobs).
- Environment DEKs, but only wrapped to principals' public keys — never in the clear.
- Users' public keys, and their private keys only in passphrase-encrypted form.
- For service tokens: a SHA-256 hash of the token and the token's public key.
- Metadata: org/app/environment/secret names, versions, timestamps, roles, and the audit log.
A value that references another secret is no
different: the server holds the ciphertext of the literal ${OTHER_SECRET} text
and never resolves it. Expansion happens in the client that already holds the
key, alongside decryption.
What the server never sees
- Plaintext secret values — with one exception you turn on yourself, described in the next section.
- Any DEK in the clear.
- Any user's private key in the clear.
- Any passphrase.
- Any sign-in password — email/password auth is verified by Stytch; seekrit never receives or stores it, and it is unrelated to the passphrase that decrypts secrets.
- The full service token string (only its hash).
The one exception: third-party sync
Third-party sync pushes an environment's secrets to a platform that keeps its own copy — a Vercel project's environment variables or a Cloudflare Worker's secret bindings, for example. Somebody has to hold the plaintext to send it, and a sync runs when nobody is logged in, so the sync engine has to be able to decrypt on its own.
This is the only place seekrit's servers decrypt anything, and it is off unless you turn it on. The boundaries:
- Per environment, per destination. Enabling sync for
productiongrants nothing forstaging, and grants nothing to any destination but the one you named. - You create the grant, not us. Access is an ordinary key grant — the environment's data key wrapped, in your browser, to a public key belonging to that sync connection. The API cannot produce that wrap, so sync cannot be enabled server-side even by someone with full control of our infrastructure.
- The matching private key is not in the database. It lives in the sync engine's own storage. A full database dump still yields only ciphertext. Deleting the connection destroys the key, which makes every grant to it permanently undecryptable — by us too.
- Plaintext is transient. A run decrypts, pushes, and drops the values. Nothing is cached between runs, and the grant is re-read every time, so revoking it stops the next run.
- On the record. Enabling sync requires an explicit acknowledgment, stored in the audit log with who did it and when. Every run writes its own audit entry — a push is the moment your plaintext leaves seekrit, so it is recorded durably rather than sampled.
If you can run code where the secrets are used, you don't need any of this:
seekrit run, the
egress proxy, the
language SDKs, and the
Kubernetes chart all decrypt on your side. Sync is
for platforms whose runtime you cannot get in front of.
Threat model
A full database compromise yields ciphertext and metadata. An attacker learns that
STRIPE_KEY exists in acme/storefront/production and when it changed, but not its value. To
decrypt, they would additionally need a principal's private key, which the database does not
contain.
A compromised or malicious server can deny service, tamper with metadata, or serve altered ciphertext — but AES-GCM authentication means tampered ciphertext fails to decrypt rather than producing wrong plaintext silently. It still cannot read secret values, because it never holds the keys. The exception is bounded by design: an attacker with full control of the control plane still cannot enable sync on an environment, because the key grant that allows decryption can only be produced client-side by someone who already holds the key. They could read environments where you had already enabled sync — which is why the grant is per environment, per destination, and revocable.
A leaked passphrase exposes that user's private key (and therefore every environment granted to them). Treat passphrases like the master credential they are.
A leaked service token grants whatever environments were wrapped to it. Revoke it; rotate the affected environment keys.
A machine with the last-known-good cache enabled holds a copy of the encrypted resolve
response beside the token that opens it. Anyone who can read that file can already read the token
and fetch the same payload themselves, so it adds no new exposure — but it does outlive the
network. While the API is unreachable, a revoked token keeps working until the entry expires
(--cache-max-age, default 24h), and a client served from cache makes no resolve call, so it
raises no env.resolve_denied audit entry. A reachable API that refuses the token deletes the
entry immediately. The cache is off unless you turn it on — see the
CLI reference.
A leaked CLI session (the credential seekrit login saves) acts as that user against the
management API, but decrypts nothing on its own: it holds no key material, so reading a secret's
value still requires that user's passphrase. Revoke it from account → security or with
seekrit logout.
Names are metadata, not secrets. Don't encode sensitive information in org, application, environment, or secret names — only in secret values.
Audit trail
Every write, grant, revocation, and token action is recorded in an append-only audit log, attributed to the acting user or service token, with contextual metadata (never secret material). The API exposes no update or delete path for audit entries. This gives you an after-the-fact record of who touched what, even though the server can't read the secrets themselves.
High-volume environment resolves (GET /v1/resolve, called on every app or CI
startup) are the deliberate exception: they are metered for usage and billing
rather than written to the audit log per call, so the log stays a signal of
change and access-control events, not a firehose of identical machine reads. A
denied resolve — a principal without a key grant — is still audited.
Exporting to your SIEM
An organization admin can stream the audit trail to an external OTLP/HTTP logs endpoint (Datadog, Grafana, Splunk, an OpenTelemetry Collector, etc.) for security monitoring and long-term retention. seekrit ships each new audit event as an OTLP log record, with at-least-once delivery: a per-org watermark advances only after your collector accepts a batch, so a transient outage re-ships rather than drops events. The endpoint's auth header is a control-plane integration credential — encrypted at rest with a server key and never returned by the API — not a customer secret, so the zero-knowledge invariant is unaffected: exported records carry only the audit log's redacted metadata, never secret values. See the audit log export guide.
Transport and authentication
- All API traffic is authenticated: a Stytch session JWT (verified locally against Stytch's JWKS), a CLI session, or a service token.
- Session JWTs are validated for issuer, audience, and expiry.
- Service tokens and CLI sessions are matched by hash and checked for revocation and expiry on every request. Neither is ever stored in a form that could be replayed from a database dump.
Multi-factor authentication
Members can protect their account with a second factor — a time-based one-time code (TOTP) from an authenticator app such as 1Password, Google Authenticator, or Authy. Enroll from account → security in the dashboard: scan the QR code, confirm a code, and save the one-time recovery codes shown once (each signs you in once if you lose your device).
Organization admins can require it for everyone from organization → settings → access & security. With the policy on, any member without a second factor must set one up before they can access the organization, and every sign-in is challenged for a code after the primary factor.
Because MFA is enforced by the identity provider before a session is issued, the second factor gates the session token itself — the API only ever sees an already-MFA-satisfied session JWT. No secret material or private key is involved, so the zero-knowledge invariant is unaffected. Enabling, disabling, and changing the org MFA policy are recorded in the audit log.
One action asks for the second factor again, mid-session: authorizing a CLI
device (seekrit login). That single click mints a 90-day credential, so a member
with MFA enrolled must have entered a code within the last 10 minutes for the
approval to be accepted — checked server-side against the session's authentication
factors, not just prompted for in the UI. A lifted browser session therefore isn't
enough to walk away with a terminal credential.
MFA guards who can start a session. It is not a substitute for your passphrase, which is what actually decrypts secrets — see encryption.
Responsible disclosure
If you find a vulnerability, please report it privately to security@seekrit.dev rather than disclosing it publicly. We'll acknowledge your report and keep you updated on the fix.