Kill switch and revocation#
The kill switch stops an agent, a session, a user's agents, a token, a namespace or everything. A revocation is one kill-switch entry: what is revoked, by whom, why, until when. Revocations reach the MAQPNA gateway in two ways, and the operator acts on the sessions themselves. Code: internal/controller/revocation_controller.go, session_governance.go, cmd/maqpna-gateway/revocation.go, configsync.go, token_vault.go, pkg/revocation.
| Path | Created by | Effective at the gateway |
|---|---|---|
| Declarative | AgentRevocation (kubectl, GitOps, maqpna kill --emit-yaml) |
Within about 1–3 seconds with configSync (polls the ConfigMap every second); 60–90 seconds with plain kubelet volume refresh |
| Break-glass | POST /v1/revocations (maqpna kill) |
At once on the receiving replica; on other replicas within stateBackend.pollMillis (Postgres) |
Actions: block (default; calls are refused), suspend (the operator also suspends matching sessions), terminate (the operator fails them, deletes the sandbox and the token Secret, and the gateway revokes the user's vault tokens for cluster-wide user matches).
Sequence#
sequenceDiagram
autonumber
actor SRE as On-call engineer
participant GW as Gateway (replica A)
participant GW2 as Gateway (replica B)
participant DB as Postgres state
participant K as Kubernetes API
participant Op as Operator
participant Ag as Agent sandbox
participant L as Audit ledger
SRE->>GW: maqpna kill -n team-a --agent coder --terminate<br/>POST /v1/revocations
GW->>GW: role killswitch or admin, add break-glass entry
GW->>DB: journal revocations (or fsynced file)
GW->>L: admin record revocation.create
GW-->>SRE: 201 id breakglass/hex, mirrored team-a/breakglass-hex
GW->>K: create AgentRevocation breakglass-hex (mirror)
DB-->>GW2: poll, entry visible
Ag->>GW2: tools/call
GW2->>L: deny, rule revocation/id, ext.revocation
GW2-->>Ag: -32001 reason revoked
K-->>Op: AgentRevocation event
Op->>K: render maqpna-revocations ConfigMap
Op->>K: AgentSession Failed (Revoked), delete Sandbox and token Secret
Op->>K: AgentRevocation status affectedSessions, appliedAt
Where the check runs#
flowchart TD
R["Request with a verified token"] --> L{"Revocation list readable?"}
L -- no --> F["deny · revocation_list_unavailable<br/>/readyz 503"]
L -- yes --> M{"Entry matches?<br/>namespace and every non-empty matcher:<br/>sessions, agents, users globs, jtis exact,<br/>or all"}
M -- yes --> D["deny · revoked<br/>policy revocation/id"]
M -- no --> C["continue the pipeline"]
C --> AW{"Held for approval?"}
AW -- yes --> RC["re-check after the wait"]
RC --> M
AW -- no --> FW["forward"]
Step by step#
- Declare or break glass.
- Declarative: an
AgentRevocationwithmatch {sessions, agents, users, jtis, all}(at least one),action,reasonand optionalexpiresAt. It matches only its own namespace, except in the gateway namespace (--gateway-namespace, defaultmaqpna-system), where it is cluster-wide andall: trueis the global kill. - Break-glass:POST /v1/revocationswith{namespace?, sessions?, agents?, users?, jtis?, all?, action?, reason?, expiresAt? | ttlSeconds?}, rolekillswitchoradmin. With OIDC,createdByis the verified username. The entry ID isbreakglass/<16 hex digits>. - Render. The revocation reconciler writes every non-expired revocation to the ConfigMap
maqpna-revocations, keyrevocations.json(entries sorted by<namespace>/<name>; an empty namespace means cluster-wide), setsaffectedSessions,appliedAtandReady, and requeues at the next expiry (at least every 30 seconds). - Deliver. With
gateway.configSync(on by default in the chart), the gateway reads the ConfigMap from the Kubernetes API everypollMillis(1000) and writes the file atomically; the reloader picks it up withinpolicyReloadSeconds(2). - Persist break-glass entries. They are fsynced to
revocationsJournalPathor the shared Postgres journalrevocations, so they survive restarts, and, withrevocationMirror.enabled(on in the chart), mirrored to anAgentRevocationnamedbreakglass-<id>(labelmaqpna.com/breakglass=true) so the operator and every replica apply them. - Match. An entry matches a token when it has not expired, its namespace matches, and either
allis true or every non-empty list matches (AND across fields, OR within one).sessions,agentsandusersare globs;jtisare exact and also match tokens whosemaqpna_parent_jtiis listed (exchanged tokens). For agent-to-agent (A2A) calls, every agent of the delegation chain and the callee are checked. - Enforce at the gateway. Right after token verification on
/mcp(POST, GET and DELETE),/llm,/a2aand browser egress, and again after an approval wait. MCP: JSON-RPC-32001,{reason: "revoked", policy: "revocation/<id>"};/llm: HTTP 403, typemaqpna_revoked. Each denial is audited withrule=revocation/<id>andext.revocation=<id>. - Act on sessions. The session reconciler watches revocations and applies the strictest matching action before minting any token or registering any release:
terminatefails the session (Revoked) and deletes its sandbox and token Secret;suspendsetsspec.suspend=true;blockkeeps itRevoked=Truewith nothing minted. - Revoke delegated tokens. Every
policyReloadSeconds, the gateway revokes at the provider, and deletes, the token-vault accounts of users matched by an active cluster-wideterminaterevocation withallor a user-only match, unlesstokenVault.keepOnKillSwitch. - Notify and lift.
revocation.createdgoes to notifiers. Deleting theAgentRevocation(orDELETE /v1/revocations/{id}for break-glass entries) lifts it; asuspendleavesspec.suspend=trueuntil you resume.
What you see#
The maqpna kill help, from cmd/maqpna/testdata/help-kill.golden:
Kill switch: revoke sessions, agents, users or tokens at the gateway (break-glass)
Usage:
maqpna kill --gateway URL [-n NS] [--agent A]... [--session S]... [--user U]... [--jti J]... [--all] --reason TEXT [--suspend|--terminate] [--ttl 1h] [--emit-yaml] [--yes]
Flags of kill:
--agent string agent name or glob (repeatable)
--all every session in scope (with -n: the namespace; without: the whole cluster)
--emit-yaml print the AgentRevocation manifest (pipe to kubectl apply -f -)
--gateway string gateway base URL (env MAQPNA_GATEWAY_URL; default: the context's gateway)
--gateway-namespace string gateway namespace (cluster-wide revocations live there) (default "maqpna-system")
--json print the gateway response as JSON
--jti string token ID (repeatable)
-n, --namespace string namespace (default: every namespace)
--name string AgentRevocation name for --emit-yaml (default kill-<timestamp>)
--oidc-token-file string file holding an OIDC access token for the admin API (env MAQPNA_OIDC_TOKEN_FILE); wins over --token
--reason string reason (recorded in the audit ledger and session events)
--session string session name or glob (repeatable)
--suspend also suspend matching sessions
--terminate also terminate matching sessions (sandbox and token deleted)
--token string static admin token (env MAQPNA_ADMIN_TOKEN; dev/break-glass)
--ttl duration revocation lifetime (0 = until removed)
--user string on-behalf-of user or glob (repeatable)
-y, --yes do not ask for confirmation (required when stdin is not a terminal)
Output: -o table|json|yaml, --jq EXPR
Examples:
# Revoke one agent in a namespace and terminate its sessions
maqpna kill --gateway "$GW" -n team-a --agent coder --reason "INC-123" --terminate
# The same as a manifest, for GitOps or when the gateway is unreachable
maqpna kill -n team-a --agent coder --reason "INC-123" --terminate --emit-yaml | kubectl apply -f -
After a kill, the CLI prints revoked (terminate, namespace team-a): breakglass/<id> and mirrored to AgentRevocation team-a/breakglass-<id> (formats from cmd/maqpna/kill.go). maqpna revocations list prints ID SOURCE ACTION NAMESPACE EXPIRES REASON, with source file or breakglass and * for cluster-wide entries. See maqpna kill, maqpna revocations list and maqpna revocations delete.
Failure modes#
| Situation | Behaviour |
|---|---|
| Revocation file missing, unreadable or invalid | Every /mcp and /llm call denied, revocation_list_unavailable; /readyz 503; maqpna_gateway_revocation_list_ok 0; alert MaqpnaRevocationListUnavailable |
| One invalid entry or a duplicate ID | The whole file is rejected (fail closed as above) |
| Gateway unreachable | Use --emit-yaml and kubectl apply; the operator still acts on sessions |
Mirror disabled and action not block |
The CLI warns that the operator cannot suspend or terminate, and suggests applying an AgentRevocation too |
| Slow propagation | Alert MaqpnaRevocationLag; maqpna_gateway_configsync_errors_total counts failures |