Two breaches and still no credential: how Vault's custody works
Vault
A platform using Seekrit Vault to hold its users' credentials can be breached and leak none of them. So can seekrit. Both at once still leaves the attacker short one key, and that key exists only inside a Durable Object in the platform's own Cloudflare account, where it was generated and from which it is never exported.
That is a strong claim, and security claims deserve a mechanism rather than a sentence. This post is the mechanism: who holds what, and why each boundary holds.
Who holds what
| Party | At rest | In use | Why |
|---|---|---|---|
| seekrit (API, database, staff) | Cannot read | Cannot read | Stores ciphertext and metadata only — the same posture as every secret in seekrit. |
| Your backend, database, logs | Hold nothing | Hold nothing | Your backend holds a platform key that opens connect links and reads metadata. It cannot release a credential. |
| Your agent (the LLM runtime) | — | Never receives a value | The executor applies the credential to the outbound request and redacts any echo from the response. |
| Your executor | Holds the only private key | Decrypts | A Worker in your own Cloudflare account. Trusted by construction — it is your code, in your account. |
The design rule behind the table: every component gets exactly the authority its job needs, and the component with the authority to decrypt is the one you run. Everything below is a consequence of that rule.
Why can't seekrit read it?
Because nothing seekrit holds can open anything seekrit stores. A credential is
encrypted with a fresh data key, and that data key is wrapped to the
executor's public key — in the user's browser for a typed key or password, or
in the executor itself for an OAuth grant it just exchanged. Seekrit receives
the two ciphertexts and the key's thumbprint. The private half of that key was
generated inside the executor's ExecutorIdentity Durable Object and has
never left it: not in an environment variable, not in a response, not in a
backup seekrit takes.
This is the same zero-knowledge model every secret in seekrit already lives
under. The executor is a client in exactly the sense seekrit run, the
proxy, and the SDKs are — customer-side code decrypting in the customer's
runtime. Vault adds no exception to it. No code path in seekrit's API
decrypts, exchanges an OAuth code, or sees a password, a cookie, or a token.
Why can't your backend read it?
Because the platform key your backend holds, skv_…, authenticates a
different surface than the one that releases credentials. It can open a
connect link, poll its status, list a user's connections, revoke one,
register a webhook. It cannot ask seekrit for a ciphertext — that route
accepts only the executor's own credential, skx_…, which the executor
received once at registration and keeps in Durable Object storage.
So the thing your backend does hold, if it leaks, lets an attacker open connect links in your name and read metadata. Loud, unpleasant, and rate-limited per key. Not a credential.
Why can't the agent read it?
Because it is never given one. vault.fetch carries a request — method, URL,
headers, body — and no credential. The executor evaluates the request against
the provider's allow rules first: which host, which methods, which paths,
default-deny, the same engine seekrit's credential
broker uses. Only a permitted request reaches the
decrypt step. The credential is injected into the outbound call, and the
response is scanned on its way back for any echo of the injected value — raw,
percent-encoded, JSON-escaped, base64 — and redacted before the agent sees it.
Every call also names the userRef the agent is acting for, and the executor
refuses unless the connection belongs to that user. An agent serving one user
cannot reach another's connection by guessing its id.
This is custody, not usage: the agent still decides what to do and uses its own tools. Vault makes the tool's request authenticated without the tool's process holding the authentication.
Two defences against a compromise of us
Zero-knowledge storage covers the database being read. Two further pieces cover the database or API being changed, which is the more useful thing an attacker inside seekrit could try.
The link pins your executor. A connect link is
https://connect.seekrit.dev/#t=…&x=…&k=…: a connect token, your executor's
origin, and the thumbprint of its wrapping key. Your SDK builds that fragment
from your own executor's identity, not from anything seekrit returns, and
browsers never send fragments to a server. The Connect page talks to the
executor at x and refuses to continue unless the key it presents has
thumbprint k. An attacker who controls seekrit's API cannot redirect your
users to a different executor or key, because the pin was built from a source
they do not control.
Ciphertext is bound to its place. Every ciphertext is authenticated against the project, connection, slot, provider, and revision it belongs to. Relabel a Gmail connection's row as a different provider so the executor would send its token to a different host, and the ciphertext fails to decrypt. Swap in a stale revision as the current one, and it fails the same way. The database can lie about metadata; the executor will not act on the lie.
A third rule removes the authority entirely: the hosted API cannot change
behaviour. Which hosts a credential may be sent to, how it is injected, and
where the OAuth endpoints are all live in your executor's vault.config.ts,
compiled into the Worker. Seekrit stores a display copy — names and the
consent sentence — and nothing executable. Serving policy from an API that
could be compromised would hand that API control over where plaintext goes,
which is the same class of harm as reading it. The agent access
policy follows the same rule for the same
reason.
Where the consent lives
The person connecting an account has no seekrit account and never will. They
are identified by your opaque userRef, which the docs tell you must not be
an email address, because it is the one field about them Vault holds in the
clear. Before any capture, the Connect page records consent along with the
version of the copy they saw. Later, a manage link shows them every
connection you hold for them and lets them disconnect any of it; that
revocation is recorded as theirs, the ciphertext is deleted, and every further
release is refused.
Why this shape
We could have shipped a simpler product: seekrit holds the credentials, encrypts them well, and promises not to look. Most of the market works that way, and it would have let us skip the executor and the fragment pin and the AAD design. We did not, because the reason a platform wants this product is "a system that holds this credential will eventually leak it," and that reasoning does not stop at the vendor boundary. If it applies to your database, it applies to ours.
The result is a product where the answer to "what does a breach of seekrit expose?" is the same for Vault as for every other secret we store: ciphertext, and the metadata around it. The guide has the setup; the API reference has the three surfaces and which key opens each.