MAQPNADocs

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#

  1. Mint. The operator calls POST /v1/token for 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.
  2. 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 in status.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.
  3. Exchange. A caller presents a MAQPNA token and an audience on the -exchange-config allow-list; the broker returns a shorter-lived token with narrower scopes and maqpna_parent_jti. An exchanged token cannot be exchanged again.
  4. 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) as subject_token and its own session token as actor_token. The broker returns a token for B whose act chain records A and then the user, capped at -max-delegation-depth (5), never across tenants.
  5. 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"}]}