Threat model
Passport sits between an AI agent that proposes actions and systems that perform them. This page states what it protects, what it assumes, and what it does not attempt.
Actors and trust
| Component | Trust | Holds |
|---|---|---|
| LLM / agent reasoning | Untrusted. Treated as attacker-controllable input (prompt injection, hallucinated tool calls). | Nothing secret |
| Agent runtime (CI job, service, sidecar) | Trusted to sign only what it intends to do. | Agent Ed25519 private key |
| Passport server | Trusted decision point. | Server signing key, encrypted Vercel token, database credentials |
| Human approver | Trusted within their organization role. | Dashboard session |
| Executor target (Vercel) | Trusted to honour its API. | — |
The security claim is therefore: an LLM cannot cause a sensitive action to execute unless (a) the runtime signs a request for it, (b) the agent holds an unexpired grant whose constraints the request satisfies, (c) the project policy allows it or a human approves it, and (d) the execution happens through Passport's relay inside the authorized window.
Protections
| Threat | Mitigation |
|---|---|
| Forged or modified request | Ed25519 signature over canonical JSON; every field participates |
| Replay of a captured request | Unique nonce per agent enforced by DB constraint; expiresAt ≤ 15 min; clock-skew bound |
| Stolen one-shot token | Token is random 32 bytes, only its hash stored, scoped to one request, single execution, short window |
| Agent exceeding its remit | Capability grants with constraints (allowedGitRefs, maxAmount, allowedVendors…), individually revocable, with expiry |
| Cross-tenant access | Organization match on agent/project; Supabase RLS on all tables; capability requires same project |
| Prompt-injected "urgent production deploy" | Policy require_approval: a human sees the action summary and exact parameters before approving; the proposer's justification is kept in the audit log |
| Approver fatigue / stale approvals | Approval opens a fresh, short execution window; expired requests cannot be approved |
| Executing with a different deployment than approved | Executor receives only the persisted, signed parameters; Vercel deployment must be READY and owned by the connected project |
| Agent workflow leaking the Vercel token | Vercel token lives only in Passport, encrypted with a server-only key; the agent never receives it |
| Log tampering by a DB admin | Hash chain with per-org serialized appends; exports carry a server-signed head; offline verifier recomputes everything |
| Silent truncation of an export | Signed head names the exact length and hash of the chain |
| Downstream system trusting a fake "allowed" | Signed decision receipts bound to requestDigest, action, parameters, expiresAt; public key discoverable |
| Server key rotation | keyId in every receipt/head; discovery lists all active keys |
Assumptions
- The agent runtime is not compromised. If an attacker can run code where the private key is, they can sign anything that key is allowed to do — which is why grants should be minimal and short-lived.
- Passport's database and signing key are protected by the hosting platform.
- Approvers authenticate through Supabase Auth; account takeover of an approver is out of scope.
- Nobody can bypass Passport for the protected action (e.g. direct Vercel promotion rights are restricted). Passport cannot enforce what it does not mediate.
Out of scope (today)
- Runtime attestation. Passport authenticates a key, not a process. It cannot tell whether the runtime that signed was the intended CI job. Binding keys to workload identity (GitHub OIDC, SPIFFE) is on the roadmap.
- Semantic correctness. Passport verifies that a deploy of
dpl_Xis permitted, not thatdpl_Xis a good build. - Denial of service / rate limiting. Application-level fixed-window limits
apply per client address to sign-in, sign-up, password reset, action
submission and status polling (
src/lib/http/rate-limit.ts). Counters live in Postgres. The limiter fails closed: an exhausted budget returns429, and an unavailable, invalid or timed-out counter result returns503without processing the protected request. Both includeRetry-After. Each bucket is retained until its own expiry; shorter-window traffic cannot erase a live longer-window budget. Volumetric attacks still need an edge layer (Vercel WAF) in front. This protection trades availability for enforcement during a counter-store outage. - Key custody in the runtime.
LocalKeySigner/PassportClient.connecthold a PEM in process memory; use a KMS/HSM-backedSignerfor production-grade custody. When the dashboard generates the pair, the private key exists on the Passport server only for the duration of that one response (never persisted or logged) and in the operator's browser until dismissed; organisations that cannot accept that exposure should bring their own public key. - Transparency. Audit heads are signed by Passport itself; anchoring to a public log would let third parties detect a rewritten history.
- Multi-org sessions, SSO, SCIM.
- Credential caching by relying parties. A portable credential proves a
grant existed when it was signed; only
POST /api/v1/credentials/verifyreflects revocation and key rotation. A relying party that skips the online check accepts a revocation lag equal to its cache lifetime. Credentials embed the agent's public key and a fingerprint, so rotating the key invalidates outstanding credentials without touching the grant.
Residual risks worth naming
- A human approver who rubber-stamps everything reduces
require_approvaltoallow. Summaries and justifications exist to make the decision legible; the audit log names the approver. - A grant with empty constraints (e.g.
deploy:production {}) is as broad as the action itself. Prefer constraints wherever the action supports them. - Receipts prove Passport's decision, not the executor's outcome. For actions
with executors,
execution_resultis recorded separately.