Passport
All documentation

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

ComponentTrustHolds
LLM / agent reasoningUntrusted. 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 serverTrusted decision point.Server signing key, encrypted Vercel token, database credentials
Human approverTrusted 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

ThreatMitigation
Forged or modified requestEd25519 signature over canonical JSON; every field participates
Replay of a captured requestUnique nonce per agent enforced by DB constraint; expiresAt ≤ 15 min; clock-skew bound
Stolen one-shot tokenToken is random 32 bytes, only its hash stored, scoped to one request, single execution, short window
Agent exceeding its remitCapability grants with constraints (allowedGitRefs, maxAmount, allowedVendors…), individually revocable, with expiry
Cross-tenant accessOrganization 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 approvalsApproval opens a fresh, short execution window; expired requests cannot be approved
Executing with a different deployment than approvedExecutor receives only the persisted, signed parameters; Vercel deployment must be READY and owned by the connected project
Agent workflow leaking the Vercel tokenVercel token lives only in Passport, encrypted with a server-only key; the agent never receives it
Log tampering by a DB adminHash chain with per-org serialized appends; exports carry a server-signed head; offline verifier recomputes everything
Silent truncation of an exportSigned 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 rotationkeyId 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_X is permitted, not that dpl_X is 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 returns 429, and an unavailable, invalid or timed-out counter result returns 503 without processing the protected request. Both include Retry-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.connect hold a PEM in process memory; use a KMS/HSM-backed Signer for 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/verify reflects 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_approval to allow. 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_result is recorded separately.