MAQPNADocs

Identity checks

Make the identity broker verify where a session runs (a sandbox, a pod, a cloud VM or a confidential VM) before it mints the session token, and use the result in policy.

The identity broker issues each session a short-lived, signed session token. By default it mints for whoever holds the broker token. An identity check closes that gap: before minting, the broker verifies where the session will run, and writes what it verified into the token as the claim maqpna_substrate. The gateway then uses that claim in policy and writes it to every audit record of the session.

There are two kinds of check:

  • Sandbox driver checks. The broker asks the sandbox platform itself about the sandbox (kosa8, agent-sandbox, e2b). See sandbox drivers.
  • Signed-credential checks. The broker verifies a credential the platform signed, against trust anchors you configure: kubernetes, spiffe, aws-iid, gcp-identity, azure-imds and tee.

When you need it#

  • You want a policy rule such as "shell tools only from a verified microVM" or "CRM tools only from sessions verified in Germany or France".
  • You want evidence of where each governed call ran, next to the decision, in the audit ledger.
  • You run your own launcher or controller that mints tokens, and you want the broker to refuse tokens for workloads it cannot verify.

How it works#

sequenceDiagram
    participant C as Caller (launcher, maqpna dev)
    participant B as Identity broker
    participant S as Substrate or trust anchor
    participant G as Gateway
    C->>B: POST /v1/proof/nonce {"check":"tee"}
    B-->>C: {"check","nonce","expiresAt"}
    Note over C,S: the substrate signs the nonce into its credential
    C->>B: POST /v1/token {..., "proof": {...}}
    B->>S: verify (sandbox API, TokenReview, JWKS, Trustee)
    alt verified
        B-->>C: token with maqpna_substrate
        C->>G: tool call with the session token
        G->>G: policy on substrate, audit record
    else refused
        B-->>C: 403, no token
    end
  1. The caller gets a proof from the substrate. For a check that binds a nonce, it first asks the broker for one with POST /v1/proof/nonce (broker token) and has the substrate sign it.
  2. The caller sends POST /v1/token with a proof object. proof.check names the check.
  3. The broker verifies the proof with that check, never on the caller's word. Only then does it mint. The response carries a substrate object, and the token carries maqpna_substrate with check, sandbox, isolation, attestation, egress, digest, platform, workload and jurisdiction.

The proof fields are check, sandbox, nonce, token, document, signature, format, x5c and evidence. Each check accepts only the fields it reads.

The checks#

Check Proof fields Verified against Records
kosa8, agent-sandbox, e2b sandbox the sandbox platform's own view of the sandbox sandbox, isolation, attestation, measurements digest, egress
kubernetes token (projected ServiceAccount token), optional nonce TokenReview, or the cluster issuer and JWKS (offline) pod, system:serviceaccount:<ns>:<sa>
spiffe token (JWT-SVID); or x5c + nonce + signature; or the TLS client certificate the trust domain's SPIFFE bundle the SPIFFE ID
aws-iid document + signature, format signature or rsa2048 AWS's public certificate for the region instance, account, region, zone
gcp-identity token (instance identity token, format=full) Google's JWKS instance, project, region, zone
azure-imds signature (attested data PKCS #7) + nonce the signing certificate's chain to your roots VM ID, subscription
tee evidence + nonce a Trustee attestation service and reference values TEE type and SVN, measurements digest

Isolation and attestation say only what the check can vouch for. The signed-credential checks report isolation process and attestation none: the broker knows the pod or VM, not how the session is isolated on it. An appraised TEE quote reports cvm and measured.

Every check enforces token and document expiry with a clock skew (default 60 seconds). A nonce is single-use, valid for one check, and expires after nonceTTLSeconds (default 300). An aws-iid document carries no nonce, so it proves which instance, account and region, not that the caller is on that instance.

Prerequisites#

  • A running identity broker (maqpna-identity) you can pass flags to.
  • The trust anchors for the checks you enable: certificates, JWKS or bundle files, or API access.
  • For the kubernetes check with tokenReview: the broker runs in the cluster with permission to create tokenreviews. The chart grants it when identity.bootstrap.enabled is true (the default).
  • A caller that sends proof in POST /v1/token.

Step 1: Write the identity checks file#

The file is JSON. Each top-level section enables one check. Unknown fields are errors. This example enables the kubernetes check through TokenReview and requires the agent's own ServiceAccount:

{
  "audience": "maqpna-identity",
  "clockSkewSeconds": 60,
  "nonceTTLSeconds": 300,
  "kubernetes": {
    "tokenReview": true,
    "serviceAccounts": ["{agent}"],
    "jurisdiction": {"provider": "aws", "region": "eu-central-1"}
  }
}

audience defaults to the broker's -issuer (default maqpna-identity). In serviceAccounts and SPIFFE ids, {agent}, {namespace}, {user} and {session} come from the mint request.

The other sections and their required fields:

Section Required Optional
kubernetes tokenReview: true, or issuer with jwks (file) or jwksUrl audience, serviceAccounts, requireNonce (default false), jurisdiction
spiffe trustDomains (trust domain to bundle file), ids audience, requireNonce (default true), jurisdiction
aws-iid certificates (region, or "*", to PEM file), accounts (12-digit IDs) —
gcp-identity projects jwks or jwksUrl (default Google's JWKS), serviceAccounts, audience, requireNonce (default true)
azure-imds roots (PEM), subscriptions intermediates, signerNames (default metadata.azure.com), jurisdiction
tee verifier (trustee or sample), referenceValues; trusteeUrl and trusteeKeys for trustee sampleKey (for sample), jurisdiction

Top-level countries maps "<provider>/<region>" to a country code and wins over the built-in region table. maxNonces bounds outstanding nonces (default 100,000).

Step 2: Start the broker with the checks#

Pass the file with -identity-checks:

maqpna-identity -identity-checks /etc/maqpna/identity-checks.json

Add these flags as needed:

Flag What it does
-identity-checks FILE Enables the signed-credential checks in FILE.
-require-proof Refuses every POST /v1/token without a verified proof (403). Needs at least one check.
-sandbox-check NAME Enables a sandbox driver check: agent-sandbox, e2b or kosa8.
-sandbox-check-gateway URL Required with -sandbox-check: the gateway URL the sandbox's egress must be pinned to.
-sandbox-check-min-isolation Weakest isolation accepted (default microvm). Use gvisor for gVisor tiers.
-sandbox-check-min-attestation Weakest attestation accepted: none, inventory (default) or measured.
-sandbox-check-allowed FILE JSON map of accepted measurement digests, for example {"kernel": ["<sha256>"]}.
-tls-cert, -tls-key Serves the broker over TLS. Clients may present an X.509-SVID for the spiffe check.

The broker refuses to start when a section misses its trust anchors or allowlist. On start it logs identity check enabled with each check's name.

In a Helm installation#

The chart has no values for identity checks yet (Planned). Today, pass the flags with identity.extraArgs and deliver the file through identity.secretsStoreCSI, which mounts a Secrets Store CSI volume at identity.secretsStoreCSI.mountPath (default /mnt/secrets-store):

identity:
  secretsStoreCSI:
    enabled: true
    secretProviderClass: maqpna-identity-checks
  extraArgs:
    - -identity-checks
    - /mnt/secrets-store/identity-checks.json

Step 3: Send a proof when you mint#

For the kubernetes check, project a ServiceAccount token with the broker's audience into the workload's pod:

volumes:
- name: maqpna-identity
  projected:
    sources:
    - serviceAccountToken: {audience: maqpna-identity, expirationSeconds: 600, path: token}

Your launcher then mints with that token as the proof. The ServiceAccount must be in the namespace you mint for:

curl -s -X POST "$MAQPNA_BROKER_URL/v1/token" \
  -H "Authorization: Bearer $MAQPNA_BROKER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"namespace":"team-a","agent":"coder","session":"s1","user":"[email protected]",
       "scopes":["tools:echo"],
       "proof":{"check":"kubernetes","token":"<projected-serviceaccount-token>"}}'

For a check that binds a nonce (spiffe, gcp-identity, azure-imds, tee, and kubernetes with requireNonce), get the nonce first and have the substrate sign it, for example a JWT-SVID or GCP identity token for the audience maqpna-identity#<nonce>:

curl -s -X POST "$MAQPNA_BROKER_URL/v1/proof/nonce" \
  -H "Authorization: Bearer $MAQPNA_BROKER_TOKEN" \
  -d '{"check":"gcp-identity"}'

Step 4: Use the substrate in policy#

A rule's substrate matcher limits the rule to sessions by where they run. A session no check ran for has isolation and attestation none and no jurisdiction.

Field Meaning
isolation, notIsolation none, process, gvisor, wasm, microvm, cvm, device-enclave
attestation, notAttestation none, inventory, measured
checks names of the checks that verified the session
providers, regions, notRegions, countries, notCountries jurisdiction (native policy files only)

isolation, attestation, checks, providers, regions and countries mean the rule applies only to those values. The not fields mean it does not apply to them. An unknown jurisdiction is in no list, so a deny rule written with notCountries also denies sessions with no verified country.

apiVersion: maqpna.com/v1alpha1
kind: ToolPolicy
metadata:
  name: shell-needs-microvm
  namespace: team-a
spec:
  defaultAction: allow
  rules:
    - name: shell-needs-a-verified-microvm
      tools: ["*__bash_run"]
      substrate:
        isolation: ["none", "process", "gvisor", "wasm"]
      action: deny
{"policies": [{"name": "data-residency", "defaultAction": "allow", "rules": [
  {"name": "pii-stays-in-de-or-fr", "tools": ["crm__*"],
   "substrate": {"notCountries": ["DE", "FR"]}, "action": "deny"}
]}]}

A ToolPolicy (spec.rules[].substrate) supports isolation, notIsolation, attestation, notAttestation and checks. The jurisdiction fields work only in native policy JSON files. maqpna policy compile refuses a rule that uses them when exporting to Cedar or CEL, and refuses any substrate matcher for AgentCore.

Verify it worked#

  1. Check the mint response. A verified mint returns a substrate object next to token. A refused proof returns 403 with the reason, and no token.
  2. Inspect the token and look for maqpna_substrate:
maqpna token inspect "$TOKEN"
  1. After the session makes a call, find its audit records with substrate fields:
jq -c 'select(.ext.substrate) | {seq, ext}' /var/lib/maqpna/audit/ledger.jsonl | tail -n 3

Each record of a verified session carries ext.substrate (for example kosa8 microvm inventory), substrateSandbox, substrateDigest, substrateWorkload and substrateJurisdiction (for example provider=aws region=eu-central-1 zone=eu-central-1b country=DE basis=signed).

To try it locally, maqpna dev up --kosa8 starts the broker with -sandbox-check kosa8, and maqpna dev run --kosa8-image alpine:3.20 -- <command> sends the proof. maqpna dev run then prints the broker verified kosa8 sandbox ... before minting. See kosa8 sandboxes.

Jurisdiction#

jurisdiction has provider, region, zone, country and basis:

  • signed: the cloud provider signed the region in the verified credential (aws-iid, gcp-identity).
  • configured: you declared it in the check's jurisdiction block (kubernetes, spiffe, tee, and the region for azure-imds). It is only as good as your configuration.

country comes from countries in your file, then from a built-in table of public AWS, Google Cloud and Azure regions (aws/eu-central-1 is DE, gcp/europe-west9 is FR, azure/westeurope is NL). A region in neither records no country. Azure's attested document does not sign the VM's location, so without a configured region the jurisdiction is azure with no region.

Limits#

  • The claim says where the session's workload was verified, not that every request comes from inside it. A holder of the broker token can mint another token for the same session and use it elsewhere. Sender-bound tokens (DPoP) close part of this gap.
  • process is a floor, not a measurement. Only tee gives hardware-rooted measurements. kosa8 reports inventory: digests its host reports.
  • Nonces live in one broker replica. Use a single replica or session affinity for nonce-bound checks.
  • AWS's DSA pkcs7 signature is not supported. Use signature or rsa2048.
  • Planned: Helm values for identity checks, proofs from the operator, SPIFFE bundle endpoints and federation, jurisdiction in ToolPolicy and in the Cedar, CEL and AgentCore exports, and combining several checks in one proof.

Troubleshooting#

Symptom Cause and fix
Broker does not start: identity checks <file>: ... A section misses a required field or trust anchor, or has an unknown field. Fix the field the message names.
-require-proof needs at least one identity check Add -identity-checks or -sandbox-check.
-sandbox-check needs -sandbox-check-gateway Pass the gateway URL the sandbox's egress is pinned to.
400 unknown proof check "..." The broker has no check of that name. The message lists the checks it has.
400 check "..." issues no nonces That check binds no nonce (aws-iid, sandbox drivers). Mint without one.
403 service account ... is not in namespace ... The token's ServiceAccount must be in the namespace you mint for.
403 this broker mints only for a verified substrate -require-proof is set and the request has no proof.
503 on mint The check could not reach its trust anchors (TokenReview, Trustee, a JWKS URL). The broker refuses rather than guesses. Check network access from the broker.
Nonce-bound proof refused after it worked once Nonces and nonce-bound tokens are single-use. Get a new nonce for each mint.

Next steps#