Security
Botpasses is a grant-vault. Credential values stay inside the vault process and are decrypted only at an approved inject: the moment Botpasses attaches the value to an outbound API call, hands it to a trusted runtime over POST /runtime/resolve, or (on the local CLI) places it in a child process environment. No MCP tool, HTTP route, console screen, or email returns a value. There is no get_secret.
Trust model
This is not zero-knowledge. To attach a key to a request the hosted process has to decrypt it, so the hosted process (and anyone holding both the AWS KMS role and the database) can read values. The model, the transcript, logs, and staff without both of those cannot. See the design decision inADR 0006.
| Surface | Sees secret value? |
|---|---|
| MCP tools | No |
| Operator console / HTTP JSON (except trusted resolve) | No after submit |
CLI list / grant / audit | No |
| Audit table / email / inbox | No |
Unauthenticated GET /collect/:id | No (shell only) |
vault run child env / POST /runtime/resolve / connector origin | Yes. That is the inject |
| Model context / chat transcript | Must not. Tests fail if a canary appears |
Encryption
Each credential is encrypted with AES-256-GCM under a data key that belongs to its organisation. Additional authenticated data is the bound tuple orgId|itemId|allowed_hosts_json|inject, not the organisation id alone. Organisation keys are wrapped by a platform key. At boot, AWS KMS unwraps that platform key (VAULT_KEK_WRAPPED) through a role the Fly Machine assumes with Fly OIDC; the KMS key itself stays in AWS and does not handle a credential. Operator inboxes and live OAuth refresh, auth, device, and session tokens are stored the same way (email-at-rest and OAuth-token-at-rest): a keyed HMAC for lookup and an AES-GCM wrap under the identity key. A database dump without the platform KEK does not yield credential values, inboxes, or those bearer tokens. Item names, last four characters, allowed hosts, and internal Grant ids stay in the dump. Machine tokens are stored as SHA-256 hashes. The local CLI uses a master key you hold, with the credential name bound into the ciphertext.
Connector hardening
When an agent calls http_request, the outbound request is constrained before the value is attached:
- Exact host allowlist. The request host must equal one of the credential's allowed hosts. No wildcards, no subdomains.
- No IP literals. Hosts are names. Botpasses resolves the name itself, rejects private, loopback, link-local, and other blocked ranges, and connects to the pinned address with the correct SNI, so a DNS rebind cannot redirect the call.
- No redirects. A 3xx is returned to the agent, not followed.
- 10 second timeout. The socket stops at 1 MiB of raw origin bytes, then the body is redacted, then the text sent to the model is cut at 256 KiB.
- Redaction. The credential value and any access token Botpasses minted are replaced with
[redacted]in the body before it reaches the model. The last four characters of a credential are not stripped from connector bodies.
Every call is written to the audit log by agent name and credential name. Approvals are per agent and credential; use one-call (prompt) approvals for anything sensitive.
Operator identity
Accounts are first-party: an 8-digit email code plus a required authenticator app (TOTP) with backup codes. There is no password and no third-party identity vendor. Sessions are HttpOnly cookies; state-changing requests need a signed CSRF token. Operator actions require the authenticator step; a session that skipped it is refused with mfa_required. The exception is a bootstrap bearer honoured only when VAULT_BOOTSTRAP_ALLOW_PLANE=1 is set on that plane.
OAuth for agents
botpasses.com is its own OAuth 2.1 authorization server for MCP clients. PKCE S256 is required; dynamic client registration accepts https, loopback, and desktop-app redirect schemes and rejectsjavascript:, data:, and file:. Access tokens are RS256 JWTs bound tohttps://botpasses.com/mcp with a 600 second lifetime and a unique jti; refresh tokens rotate. Clients that cannot do OAuth use a one-time bearer token issued in the console. Model tokens cannot call operator routes or resolve values; trusted runtime tokens cannot call MCP.
Access ledger and revoke
Every issued session, bearer token, and OAuth token is recorded before it is handed out, as a hash. The console Access panel lists who holds access, when it was first and last used, and which credential names were fetched. Revoking an agent marks its tokens denied so the next call is 401; revoking an approval stops one credential; revoking a session signs out one device. Revoke is operator-only. Nothing over MCP can approve or revoke.
Tests for secret exposure
The repository stores a canary value in tests and asserts that it never appears in an MCP tool result, a REST payload for a model, the inbox JSON, an email, a collect page, an audit row, or the SQLite file. Another test fails if the phrase "zero-knowledge" appears in the README or the source, since that is not a claim Botpasses can make. These run in CI on every push.
Reporting a vulnerability
Email [email protected]. The same contact is published in/.well-known/security.txt. What to include and what to expect: Security disclosure.
Decisions
- ADR 0006: Grant-vault, not zero-knowledge
- ADR 0007: KMS-wrapped platform key
- ADR 0008: Security hardening after the 2026-09 audit
- ADR 0009: Email at rest and OAuth tokens at rest
- ADR 0003b: First-party operator identity
- ADR 0004b: Same-origin OAuth authorization server
- ADR 0005: Access ledger
- Threat model (source of the table above)