Revocation

Revocation is STACK's Layer 5. A revoked passport stops verifying immediately. Every outbound call routed through the proxy with a revoked passport receives401 PASSPORT_REVOKED. Revocation propagates to all proxy nodes within 60 seconds and always cascades to delegated children.

Revocation is authoritative state, not a cache miss. Once a passport is marked revoked in the database and the corresponding revoked:<jti> key is written to Redis, the verify path rejects it on the next call.

Five revocation types

  • single - one jti; cascades to descendants via parent_passport_id
  • bulk_agent - every live passport for one agent; cascades from each
  • bulk_session - every passport sharing a session_id (which descendants already inherit)
  • bulk_operator - every live passport for the operator; the nuclear option
  • bulk_claim - every passport carrying a specific identity claim_id

Parent → children cascade

A delegated passport has no independent authority; it borrows from its parent. When a parent is revoked, STACK walks parent_passport_idbreadth-first and revokes every live descendant in the same operation. The cascade is cycle-safe and covers up to four hops (the maximum delegation depth).

Cascade fires on single andbulk_agent revokes. Session, operator, and claim-scoped revokes do not need it - descendants already inherit those identifiers and are caught by the broader filter.

Redis pub/sub broadcast

Every revocation publishes an event to stack:revoked. Proxy workers or other subscribers can listen to invalidate in-memory caches eagerly rather than waiting for the next revoked:<jti>lookup. The payload is a discriminated union keyed by type:

json
{ "type": "single", "operator_id": "op_acme", "jti": "pp_8f3a",
  "cascaded": ["pp_8f3a_child"], "reason": "scope_drift_fire: ...", "at": 1747913532614 }

{ "type": "bulk_agent", "operator_id": "op_acme", "agent_id": "agt_support",
  "revoked_count": 4, "cascaded_count": 2, "reason": "Agent deleted", "at": 1747913532614 }

{ "type": "bulk_session", "operator_id": "op_acme", "session_id": "ses_xyz",
  "revoked_count": 3, "reason": "Session revoked by operator", "at": 1747913532614 }

{ "type": "bulk_operator", "operator_id": "op_acme",
  "revoked_count": 48122, "reason": "Emergency revoke all", "at": 1747913532614 }

{ "type": "bulk_claim", "operator_id": "op_acme", "claim_id": "idc_abc",
  "revoked_count": 7, "reason": "Identity claim expired", "at": 1747913532614 }

Triggers

  • Operator via POST /v1/passports/revoke (jti + reason in body), /v1/passports/revoke-agent/:agentId, /v1/passports/revoke-session/:sessionId, /v1/passports/revoke-all
  • MCP via stack_revoke_passport, stack_revoke_agent_passports, stack_revoke_session, stack_revoke_all_passports
  • Detector auto-revoke in enforced mode - scope_drift is the live path today
  • Automatic cascades - disconnecting a service revokes every passport using it; revoking a team member revokes their active passports; identity claim expiry with auto_revoke on triggers bulk_claim

What happens after revoke

  • DB: passports.revoked_at and passports.revoked_reason are set in one UPDATE per row
  • Redis: revoked:<jti> written with TTL equal to the passport&apos;s remaining life
  • Pub/sub: one message on stack:revoked describing the revoke shape
  • Verify path: the next call reads revoked:<jti> and throws PASSPORT_REVOKED
  • Audit: one row at layer=vault, action=passport.revoke (or .revoke_all, .revoke_agent, .revoke_session)

60-second propagation

REVOCATION_PROPAGATION_SECONDS = 60 is the worst-case guarantee window. In practice a revoke is visible to the next verify call within the Redis round-trip - typically sub-second. The 60s bound covers cache invalidation on any proxy workers running their own verify cache.

Revocation affects STACK-mediated traffic. Tokens that a third-party service already holds outside STACK (long-lived OAuth tokens, API keys configured directly) are outside the scope of a passport revoke. Rotate those at the upstream service.

Example: emergency revoke

bash
curl -s -X POST https://api.getstack.run/v1/passports/revoke-all \
  -H "Authorization: Bearer $STACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "confirm": true, "reason": "CVE-2025-30066 exposure" }'
json
{
  "success": true,
  "revoked_count": 48122
}
stack | docs