Identity broker#
The identity broker (maqpna-identity) issues each session a short-lived, signed identity. It mints compact JWS tokens signed with Ed25519, whose subject is a SPIFFE-style ID and whose act claim names the person the agent acts for. It publishes its public keys (JWKS) so the gateway can verify tokens, performs RFC 8693 token exchange and A2A delegation, and serves the late-binding bootstrap that gives warm-pool pods their session.
Code: cmd/maqpna-identity, pkg/identity.
Neighbours#
flowchart LR
OP["Operator"] -- "POST /v1/token<br/>Bearer MAQPNA_BROKER_TOKEN" --> IB
AT["Attestation service"] -- "POST /v1/token<br/>(after attestation)" --> IB
POD["Warm-pool pod<br/>projected SA token"] -- "POST /v1/bootstrap?wait=20<br/>(:8083)" --> BS
GW["Gateway"] -- "GET /.well-known/jwks.json" --> IB
UPS["Upstream credential<br/>tokenExchange"] -- "POST /v1/exchange" --> IB
AGB["Agent B handling an A2A call"] -- "POST /v1/exchange<br/>actor_token" --> IB
subgraph Broker["maqpna-identity"]
IB["API :8081<br/>token, verify, exchange, JWKS"]
BS["Bootstrap :8083"]
end
BS -- "TokenReview, get pod,<br/>Sandbox, SandboxClaim,<br/>AgentSession" --> K[("Kubernetes API")]
KEY[("Signing key<br/>platform + tenant keys")] --> IB
Responsibilities#
| Endpoint | Auth | What it does |
|---|---|---|
POST /v1/token |
MAQPNA_BROKER_TOKEN |
Mint a session token from {namespace, agent, session, user, scopes, tier, ttlSeconds, groups, principalVerified, parentSession, forkRoot, inheritedTaints}; returns {token, expiresAt, spiffeId, jti, kid} |
POST /v1/token/verify |
MAQPNA_BROKER_TOKEN |
{valid, claims} or {valid, error} |
POST /v1/exchange |
The subject token | RFC 8693 exchange to an allow-listed audience (down-scoped, at most 300 s by default, never longer than the parent); with actor_token, A2A delegation |
GET /.well-known/jwks.json |
None | Current key, previous keys and every tenant key (Cache-Control: max-age=300) |
POST /v1/bootstrap |
Projected ServiceAccount token, audience maqpna-bootstrap |
Warm-pool late binding on the separate -bootstrap-listen port |
GET /healthz, GET /readyz |
None | Probes |
The token#
| Claim | Example |
|---|---|
sub |
spiffe://maqpna.local/ns/team-a/agent/coder/session/fix-4821 |
act |
{"sub":"user:alice@acme.eu"}; delegation nests it: {"sub":"<caller SPIFFE ID>","act":{"sub":"user:alice@acme.eu"}} |
scope |
tools:github models:team-a.coder (space-separated) |
maqpna_ns, maqpna_agent, maqpna_session, maqpna_tier |
The session identity; the verifier requires sub to match them |
maqpna_groups, maqpna_principal_verified |
The user's verified groups (at most 32) and how the user was bound: requester, trustedCreator or idToken |
maqpna_parent_jti, maqpna_delegator_jti |
Set on exchanged and delegated tokens |
maqpna_parent_session, maqpna_fork_root, maqpna_inherited_taints |
Set on forks |
iss, aud, iat, nbf, exp, jti |
maqpna-identity, maqpna-gateway, … |
The header is {"alg":"EdDSA","kid":"<RFC 7638 thumbprint>"}; any other algorithm is rejected. The verifier refuses tokens with typ: txntoken+jwt, so a transaction token can never be used as an identity token, and accepts a token without kid only when exactly one key is trusted.
How it works#
- Mint. The operator calls
POST /v1/tokenfor a non-attested direct session. The TTL is the requested one (default-default-ttl, 15 minutes) capped at-max-ttl(1 hour). The operator re-mints at 80% of the lifetime and rewrites the token Secret. - Bootstrap. A warm-pool pod posts its projected ServiceAccount token to
/v1/bootstrap?wait=20. The broker runs a TokenReview, checks the bound pod, waits until agent-sandbox has adopted the pod's Sandbox into a claim, follows pod → Sandbox → SandboxClaim → AgentSession with matching UIDs, binds the first pod instatus.bootstrap.podUID, and returns the session environment plus a fresh token (attested sessions get only the release ID and nonce). Later calls from the same pod refresh the token. - Exchange. A caller presents a MAQPNA token and an audience on the
-exchange-configallow-list; the broker returns a shorter-lived token with narrower scopes andmaqpna_parent_jti. An exchanged token cannot be exchanged again. - Delegate. Agent B, handling an A2A call from agent A, presents the call's transaction token (or A's token, if it grants
agents:B) assubject_tokenand its own session token asactor_token. The broker returns a token for B whoseactchain records A and then the user, capped at-max-delegation-depth(5), never across tenants. - Tenants. For a tenant namespace, the broker signs with that tenant's key and trust domain; a missing or mismatching tenant key fails with 503.
See identity broker flow for the sequences.
Configuration#
| Flag | Default | Purpose |
|---|---|---|
-listen |
:8081 |
Main API |
-key, -generate-key |
/var/run/maqpna/identity/identity.key, false |
Ed25519 PKCS#8 PEM signing key |
-previous-keys |
— | Retired keys still published and trusted (also previous-*.pem beside the key) |
-trust-domain, -issuer, -audience |
maqpna.local, maqpna-identity, maqpna-gateway |
Token identity |
-default-ttl, -max-ttl |
15m, 1h |
Token lifetimes |
-exchange-config |
— | Allow-list of exchange audiences (maxTTLSeconds, allowedScopes, provider) |
-txn-token-keys, -txn-token-audience, -max-delegation-depth |
—, trust domain, 5 | A2A delegation |
-tenants-file, -tenant-keys-dir, -tenant-reload |
—, /var/run/maqpna/tenant-keys, 10s |
Multi-tenant signing |
-bootstrap-listen, -bootstrap-audience, -sandbox-api-version |
off, maqpna-bootstrap, v1beta1 |
Warm-pool bootstrap |
Environment MAQPNA_BROKER_TOKEN is required; the broker refuses to start without it. Helm: identity.replicas, identity.port (8081), identity.trustDomain, identity.key.mode (generate for development, helm or existingSecret), identity.tenants.enabled, identity.bootstrap.{enabled, port} (8083).
Failure modes#
| Failure | Effect |
|---|---|
| Broker down | No new tokens: new sessions wait in Provisioning, warm-pool pods cannot bootstrap, re-minting stops; existing tokens stay valid until they expire. The gateway keeps verifying with the keys it has cached |
| Key rotated without a grace period | Tokens signed with the old key fail (token_signature_invalid) until it is listed as a previous key; maqpna keys rotate identity keeps the old key published for --grace |
| Tenant key missing or mismatching | 503 for that tenant's namespaces (fail closed) |
| Bootstrap refused | not_adopted or not_prepared (425), already_bootstrapped or session_suspended (409), session_gone (410), access_denied |
Scaling#
The broker is stateless apart from its keys. More than one replica needs a shared key: the chart refuses identity.replicas > 1 with key.mode: generate. values-production.yaml runs two replicas with existingSecret. There is no PodDisruptionBudget for the broker in the chart.
What you see#
maqpna token inspect decodes a session token and maqpna token verify checks it against the broker's JWKS; see maqpna token inspect and maqpna token verify. A JWKS response looks like this (values shortened):
{"keys":[{"kty":"OKP","crv":"Ed25519","x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo","kid":"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs","use":"sig","alg":"EdDSA"}]}