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:
{ "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'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
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" }'{
"success": true,
"revoked_count": 48122
}