Attestation service#
The attestation service (maqpna-attest) is the key broker for tier-2 sessions. It releases a session token to a confidential VM (AMD SEV-SNP or Intel TDX) only after the VM proves, with a hardware-signed report, that it runs a measurement you approved with debug disabled. The token is sealed to an ephemeral key that exists only inside that VM, so it never touches etcd, a Kubernetes Secret, a TLS terminator or the host.
Its partner, maqpna-attest-agent, runs inside the sandbox in three modes: init (redeem the release before the agent starts), sidecar (re-attest to renew the token) and bootstrap (warm-pool claims).
Code: cmd/maqpna-attest, cmd/maqpna-attest-agent, pkg/attest.
Neighbours#
flowchart LR
OP["Operator"] -- "POST /v1/releases<br/>GET /v1/releases/{id}" --> AT
subgraph CVM["tier-2 sandbox (confidential VM)"]
INIT["attest-agent --mode init"]
REN["attest-agent --mode sidecar"]
TSM["configfs-tsm<br/>/sys/kernel/config/tsm/report"]
AG["agent container<br/>/var/run/maqpna/token (read-only)"]
end
INIT -- "POST /v1/releases/{id}/attest" --> AT
REN -- "GET /v1/renewals/{id}/challenge<br/>POST /v1/renewals/{id}" --> AT
INIT & REN --> TSM
subgraph ATS["maqpna-attest :8082"]
AT["API"]
J[("releases and renewals<br/>memory, file journal or Postgres")]
end
AT -- "POST /attestation" --> TR["Trustee AS<br/>(customer-run)"]
AT -- "POST /v1/token" --> IB["Identity broker"]
AT --> J
Responsibilities#
| Endpoint | Caller | What it does |
|---|---|---|
POST /v1/releases |
Operator (bearer MAQPNA_BROKER_TOKEN) |
Register a single-use release {namespace, agent, session, user, scopes, tier, ttlSeconds, nonce, renewUntil}, valid 15 minutes |
POST /v1/releases/{id}/attest |
attest-agent in the VM |
Verify evidence, appraise, mint, seal and return the token; the release is consumed |
GET /v1/releases/{id} |
Operator | {state: pending, attesting, released, failed, reason}, so a failed attestation fails the session (AttestationFailed) |
GET /v1/renewals/{id}/challenge |
Renewal sidecar | A fresh single-use nonce (5 minutes) |
POST /v1/renewals/{id} |
Renewal sidecar | Re-attest with the same key and measurement; a new token, capped at renewUntil; 410 afterwards |
GET /healthz, GET /metrics |
Probes, Prometheus |
How a release works#
- The operator generates a 32-byte nonce and registers a release for the session.
- The sandbox starts with the init container. It generates an ephemeral X25519 key and puts
SHA-512(nonce ‖ public key)in the report data of a fresh TEE report (configfs-tsm, providerssev_guestortdx_guest). - It posts the evidence to
/v1/releases/{id}/attest. - The service checks that the release is pending and unexpired and the nonce matches, then sends the evidence to Trustee and verifies the returned token's signature, expiry, issuer and report data.
- It appraises the claims against the reference values: TEE type, launch measurement, workload values (SNP
HOST_DATA, TDXMRCONFIGIDandRTMR), debug off, minimum SVN. - Only then it asks the identity broker for the token, seals it (X25519 → HKDF-SHA256 → AES-256-GCM, additional data
maqpna-release:<id>), journals the release as consumed and returns the envelope. - The init container opens the envelope, writes the token with mode 0400 to the tmpfs at
/var/run/maqpna/tokenand exits; the agent container starts. - The renewal sidecar re-attests at 80% of the token lifetime until
renewUntil.
Any verification or appraisal failure returns 403 {reason} and consumes the release, so it cannot be retried with different evidence. Only verifier (503) and broker (502) outages return the release to pending. The full sequence is in attestation-gated secrets.
Configuration#
maqpna-attest flags:
| Flag | Default | Purpose |
|---|---|---|
-listen |
:8082 |
API |
-broker-url |
http://maqpna-identity.maqpna-system.svc:8081 |
Identity broker |
-verifier |
trustee |
trustee, or sample (DEV ONLY) |
-trustee-url, -trustee-path, -trustee-jwks, -trustee-issuer |
—, /attestation, —, — |
Trustee attestation service |
-trustee-max-token-age, -trustee-allow-missing-debug-claim |
10m, false |
Token freshness; a missing debug claim is a rejection |
-reference-values |
required | Allowed measurements and workload values (empty list rejects everything) |
-renew-min-interval |
30s |
Rate limit on renewals |
-response-signing-key, -tls-cert, -tls-key |
— | Signed responses and TLS (recommended) |
-store-path |
— (memory) | File journal |
-state-backend, -state-dsn-file, -state-schema, -state-max-conns |
file, —, maqpna, 10 |
Shared PostgreSQL state |
maqpna-attest-agent: --mode init|sidecar|bootstrap, --tee auto|snp|tdx|sample (auto never falls back to sample), --out (/var/run/maqpna/token), --state-dir (/var/run/maqpna-attest, a memory-backed volume mounted only into the attest containers), --keep-key-for-renewal, --renew-at (0.8), --max-wait (60 s), --ca-file, --attest-response-pubkey. Environment: MAQPNA_ATTEST_URL, MAQPNA_RELEASE_ID, MAQPNA_RELEASE_NONCE.
Helm: attestation.enabled, attestation.replicas, attestation.verifier, attestation.trusteeURL, attestation.trusteeJWKS, attestation.referenceValues, attestation.responseSigning, attestation.pdb.
Failure modes#
| Failure | Effect |
|---|---|
| Wrong measurement, debug on, low SVN, bad signature, nonce mismatch or replay | 403; release consumed; session Failed (AttestationFailed) |
| Trustee unreachable | 503; release back to pending; the init container retries for up to about 60 s |
| Identity broker unreachable | 502; same as above |
| Attestation service restart, memory store | Pending releases and renewals lost; the operator must re-register; renewal sidecars exit 1 on 404 |
| PostgreSQL unreachable (shared state) | Attestation calls refused with reason store (renewals 503) |
Renewal after renewUntil |
410 Gone; the sidecar exits 0 and keeps the current token |
Scaling#
With the default file or memory store the service must run as one replica: the chart refuses attestation.replicas > 1 unless state.backend: postgres. On Postgres, releases, in-flight claims (2-minute lease), challenges and renewals are journaled in attest-releases and claimed under a cluster-wide lock, so any replica serves any step and each release is used once. values-production.yaml and values-sovereign-eu.yaml run two replicas.
Metrics#
maqpna_attest_success_total, maqpna_attest_failure_total{reason}, maqpna_attest_releases_created_total, maqpna_attest_releases_pending, maqpna_attest_renewal_success_total, maqpna_attest_renewal_failure_total{reason}, maqpna_attest_renewals_active. Alert: MaqpnaAttestationFailureRate. Failure reasons are malformed, nonce, binding, signature, tee, measurement, debug, svn, verifier-unavailable and expired.
What you see#
maqpna doctor reports the attestation posture with the checks attest-verifier (fails on the sample verifier) and attest-response-signing. A failed release shows on the session:
$ maqpna session describe -n team-a fix-4821
Name: fix-4821
Namespace: team-a
Agent: coder
User: alice@acme.eu
Phase: Failed
SPIFFE ID: -
Scopes: tools:github
Created: 2026-10-02T14:05:09Z
Ready: -
Expires: 2026-10-02T16:05:09Z
Outcome: Failed (AttestationFailed)
Attestation: required, release 9c1f2e7a4b8d03c6a1e5f7b20d9c4e81
Taint: -
Conditions:
TYPE STATUS REASON MESSAGE
TokenReady False AttestationFailed attestation failed: measurement
...
The field layout comes from cmd/maqpna/session_gateway.go; values are illustrative and the output is abbreviated. See maqpna session describe and maqpna doctor.