# Vault trust model

[Vault](/docs/guides/vault) is custody of your **end users'** credentials for an
agent that acts on their behalf: a Gmail token, an API key they pasted in, a
username and password. This page states what it guarantees, how that follows
from where each piece runs, and the limits you should put in your own
documentation.

The one-sentence version, which is the promise the product makes:

> **seekrit never sees your users' credentials, and your systems never store
> them — they are decrypted just in time inside an executor in your own
> Cloudflare account and applied without being handed to your agent.**

## 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. |

Two breaches are therefore each insufficient on their own. A seekrit breach
yields ciphertext nobody can open. A breach of your backend yields a platform
key that cannot release anything. Both together still lack the executor's key,
which exists only in Durable Object storage and is never exported.

## How the pieces fit

1. **Capture.** Your backend opens a *connect session* and sends the user a
   link to the hosted Connect page. Everything sensitive rides in the link's
   **fragment**, which browsers never send to a server — so it is not in
   seekrit's access logs either.
2. **Consent.** The page shows your name and logo, which provider, what the
   agent will be able to do, and the sentence above. Consent is recorded
   before anything is captured, with the version of the copy the person saw.
3. **Encryption on the device.** What the person types is serialized,
   encrypted with a fresh data key, and that key is wrapped to your executor's
   public key — in the browser, before anything leaves the page. The hosted
   API receives ciphertext. For an **OAuth** provider the capture happens on
   your executor's origin instead: it receives the provider's redirect,
   exchanges the code, encrypts the tokens to its own key, and files the same
   kind of ciphertext. Neither route puts a plaintext in front of seekrit.
4. **Release and apply.** When your agent needs the credential, your backend
   asks *your executor* to make the request. The executor fetches the
   ciphertext from seekrit, opens it, checks the request against the
   provider's allow rules, injects the credential, forwards the request, and
   redacts every echo of the value from the response.
5. **Revoke.** You or the user withdraw access; seekrit refuses every further
   release immediately and deletes the ciphertext.

### The link pins the executor

A Connect link is `https://connect.seekrit.dev/#t=…&x=…&k=…`: the connect token,
your executor's origin, and the thumbprint of its wrapping key. Your SDK
builds that link from your *own* executor's identity, not from anything
seekrit returns. The Connect page talks to the executor at `x` and refuses to
continue unless the key it presents has thumbprint `k`. So an attacker who
controls seekrit's API or database still cannot point your users at a
different executor or key — the pin is built from a source they do not
control.

### Ciphertext is bound to its place

Every ciphertext is authenticated against where it lives: the project, the
connection, the slot, the **provider**, and the **revision**. A database
attacker who relabels a Gmail connection as some other provider — so the
executor would inject its token into a host they control — produces a
ciphertext that fails to decrypt. A stale revision replayed as the current
one fails the same way.

### 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 only a display copy — names and the sentence
on the consent screen. Serving the executable half from an API that could be
compromised would hand that API authority over *where plaintext goes*, which is
the same class of harm as seeing it; this is the same rule the
[agent access policy](/docs/guides/agent-proxy/policy) follows.

### A browser session is a credential too

For a site with no API, the person signs in inside a browser the executor
opened — a Browser Run session in your Cloudflare account, fenced to the
site's domains — through a Live View link on their own phone. The executor
captures the cookies and storage, encrypts them to its key, and that
ciphertext is the `session` slot. When the agent needs the site, the executor
restores the session into a fresh fenced browser and hands the agent the
session id; the cookies never pass through your backend or the agent's
context. The browser the agent drives can reach only the site's domains, a
saved password is typed only into an input on the site's login origins, and
the rotated session is written back by compare-and-swap so a revocation during
a lease wins. Live View links grant control of a page: the executor returns
one only to the person's own Connect page, or to you to forward to that
person.

### Refresh is a single writer with a journal

An OAuth access token expires; the executor refreshes it just before use and
writes the new revision back. One Durable Object per connection does this, so
two requests arriving with the same expiring token share one refresh. For a
provider whose refresh tokens are **single-use** (GitHub App user tokens), the
object writes a journal entry *before* calling the token endpoint; a journal
found later with no recorded outcome means the old token may already be spent,
and using it again would be the replay the provider is watching for. The
executor stops, marks the connection `reauth_required`, and a reconnect link
repairs it. Retention stays at the current revision plus one previous, so
refresh tokens never accumulate.

## How this relates to seekrit's zero-knowledge model

seekrit's [security model](/docs/concepts/security) is that plaintext never
reaches the API and decryption happens only in clients. 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 that model. No Vault code path in seekrit's API decrypts,
exchanges an OAuth code, or sees a password, a cookie, or a token.

The Connect page is JavaScript served by seekrit that sees what the person
types before encrypting it. That is the same trust the dashboard already asks
for (browser-side crypto served by seekrit). The defences above are aimed at
a compromise of the database or API.

## Limits

Put these in your own documentation too.

- **A malicious platform can modify its executor.** Vault protects you *from
  holding* your users' credentials; it does not protect your users from you.
- **The agent sees what the account contains.** Vault protects credentials,
  not content.
- **A prompt-injected agent can misuse access within scope.** The executor
  enforces structural guardrails — allowed hosts, methods and paths — and
  cannot judge intent. Keep provider rules narrow.
- **A dispatched request cannot be recalled.** Revocation stops the next
  release, not a request already in flight.

## What this release covers

API-key providers and username/password logins (with an optional
authenticator secret), captured on the Connect page; OAuth providers (Google,
Microsoft, GitHub presets, or any authorization-code provider), captured by
the executor with refresh handled there; browser sessions, captured through
Live View and driven through leases; manage links; signed webhooks. The data
model and the trust boundary above are the same for all of them.
