Attestation-gated secrets#
For tier-2 (confidential VM) sessions, the infrastructure operator is not trusted with confidentiality. A Kubernetes Secret would be readable by anyone with node or etcd access, so for these sessions the session token is never stored in a Secret, written to etcd or seen by the operator. The attestation service (maqpna-attest) releases it only after the sandbox proves, with a report signed by the CPU (AMD SEV-SNP or Intel TDX), that it is a genuine confidential VM running a measurement you approved with debug disabled. The token is then sealed to a key that exists only inside that VM.
Attestation is required when the tier sets attestationRequired: true, or when its isolation is listed in SovereigntyPolicy.requireAttestationFor (the Helm default lists confidential).
The release#
sequenceDiagram
autonumber
participant Op as Operator
participant AT as maqpna-attest :8082
participant AG as attest agent (init, in the VM)
participant TSM as configfs-tsm
participant TR as Trustee AS (yours)
participant IB as Identity broker
Op->>AT: POST /v1/releases {ns, agent, session, user, scopes, tier,<br/>ttlSeconds, nonce, renewUntil} Bearer MAQPNA_BROKER_TOKEN
AT-->>Op: 201 {releaseId} (single use, 15 min)
Op->>Op: status.attestation = {required, releaseId, nonce, renewUntil}<br/>create sandbox with init container maqpna-attest
AG->>AG: ephemeral X25519 key<br/>reportData = SHA-512(nonce + public key)
AG->>TSM: write inblob, read outblob (SNP report or TDX quote)
AG->>AT: POST /v1/releases/{id}/attest {evidence}
AT->>AT: release pending and unexpired, nonce matches
AT->>TR: POST /attestation {tee, evidence, runtime_data}
TR-->>AT: signed JWT with TCB claims
AT->>AT: verify JWT, report_data binding,<br/>appraise against reference values
AT->>IB: POST /v1/token (same claims)
IB-->>AT: token
AT->>AT: seal(token) to the ephemeral key, AAD maqpna-release:{id}<br/>journal the release as consumed
AT-->>AG: 200 {sealed, renewalId}
AG->>AG: open, write /var/run/maqpna/token (0400), exit 0
Note over AG: The agent container starts with the token on a tmpfs
Step by step:
- Register. For an attested session the operator generates a 32-byte random nonce and calls
POST {--attest-url}/v1/releaseswith the session's claims, the nonce andrenewUntil(the session'sexpiresAt). The release lives at most 15 minutes and can be used once. The operator recordsstatus.attestationand setsTokenReadywith reasonAttestationGated. - Build the pod. The sandbox gets an init container
maqpna-attest(image--attest-agent-image) with--release-id,--nonceand--out=/var/run/maqpna/token, sharing a 1 MiB memory-backedemptyDirthat the agent container mounts read-only. A native sidecarmaqpna-attest-renewerkeeps running beside the agent. The per-session NetworkPolicy also allows egress to the attestation service. - Produce evidence. In the VM, the agent generates an ephemeral X25519 key pair and computes
reportData = SHA-512(nonce ‖ public key). Through Linux configfs-tsm (/sys/kernel/config/tsm/report, providersev_guestortdx_guest) it asks the CPU for a report with that value inside. Hardware signs the report, so neither the nonce nor the key can be swapped without a new signature. - Submit. The agent posts the evidence to
POST /v1/releases/{id}/attest. - Verify. The service checks the release is pending and unexpired and the nonce matches, then sends the evidence to your Trustee Attestation Service. It verifies the returned JWT (signature against
-trustee-jwks,exprequired,iss, maximum age-trustee-max-token-age10 minutes, no futureiat) and that itsreport_dataequalsSHA-512(nonce ‖ public key). - Appraise. The claims are compared with your reference values: TEE type in
teeTypes, launch measurement inallowedMeasurements, debug off (a missing debug claim counts as debug unless-trustee-allow-missing-debug-claim), SVN at leastminSvn. The workload is appraised too: TDXRTMR[0..3](tdxRtmr) orMRCONFIGID(tdxMrConfigId), SNPHOST_DATA(snpHostData). A report with no workload reference value is rejected unlessallowLaunchMeasurementOnly: true. An empty measurement list rejects everything. - Mint. Only now does the service ask the identity broker for the token (
POST /v1/token). - Seal. The token is sealed to the ephemeral public key: X25519 ECDH, HKDF-SHA256 (salt = both public keys, info
maqpna-attest-v1), AES-256-GCM with additional datamaqpna-release:<id>. The plaintext is zeroed and the release is journaled as consumed before the response is sent. - Open. The agent opens the envelope, writes the token with mode 0400 atomically, zeroes its copy and exits. The agent container starts.
Optionally, maqpna-attest signs its responses with Ed25519 (-response-signing-key); the operator injects the public key (--attest-response-pubkey-file), and the agent refuses unsigned or foreign-signed responses. Serve the service over TLS (-tls-cert, -tls-key; agent --ca-file) in production.
Renewal#
Sessions can outlive one token. If the release carried renewUntil, a successful attestation also returns a renewalId, bound to the attested public key and to the measurement of the first attestation. The init container keeps the ephemeral private key in a private, memory-backed state directory (/var/run/maqpna-attest) that only the two attest containers mount.
sequenceDiagram
autonumber
participant SC as attest renewer (sidecar)
participant AT as maqpna-attest
participant TR as Trustee AS
participant IB as Identity broker
SC->>SC: sleep until 80% of the token lifetime
SC->>AT: GET /v1/renewals/{rid}/challenge
AT-->>SC: {nonce, expiresAt} (single use, 5 min)
SC->>SC: fresh report with SHA-512(nonce + SAME public key)
SC->>AT: POST /v1/renewals/{rid} {evidence}
AT->>AT: now before renewUntil (else 410)<br/>key equals bound key (else 403)<br/>30 s since last attempt (else 429)<br/>take the challenge (single use)
AT->>TR: verify evidence
AT->>AT: same TEE and measurement as the first attestation, appraise again
AT->>IB: POST /v1/token, ttl = min(ttlSeconds, renewUntil - now)
AT-->>SC: 200 {sealed} (AAD includes the challenge)
SC->>SC: replace the token file atomically, repeat
The renewer never deletes the current token. It retries failures with exponential backoff (1 second up to 2 minutes, honouring Retry-After), exits 0 on 410 Gone after renewUntil, and exits 1 on 404 (unknown renewal, for example after an attestation service restart without a durable store).
Warm pools (claim mode)#
In claim mode the operator cannot inject containers. The SandboxTemplate runs maqpna-attest-agent --mode=bootstrap as a native sidecar. It waits for adoption, gets MAQPNA_ATTEST_URL, MAQPNA_RELEASE_ID and the nonce from the identity broker's /v1/bootstrap (attested sessions get no token there), redeems the release as above and keeps renewing.
Failure modes#
| Situation | Result |
|---|---|
Any verification or appraisal failure (malformed, nonce, binding, signature, tee, measurement, debug, svn, expired) |
403 {reason}, no token. The release is consumed, so it cannot be retried with other evidence. The decision is logged with the measurement |
| Verifier unavailable | 503; the release returns to pending and the agent retries for up to about 60 seconds (--max-wait) |
| Identity broker unavailable | 502; same as above |
| Release burned | The operator polls GET /v1/releases/{id} while the session is Pending or Provisioning. State failed fails the session with TokenReady=False, reason AttestationFailed, and deletes the sandbox |
State backend unreachable (postgres) |
Attestation calls are refused with reason store; renewals answer 503 |
| Attestation service restarted with memory-only state | Pending releases and renewals are lost: the operator must re-register releases and renewers exit 1. Use -store-path (file journal) or -state-backend postgres |
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. The chart alert MaqpnaAttestationFailureRate fires on failures.
What you see#
A failed attestation shows on the session (maqpna session describe) as condition TokenReady=False with reason AttestationFailed, a Warning event AttestationFailed, phase Failed and status.outcome.result Failed. Session list output (format from cmd/maqpna/session.go, values illustrative):
NAME AGENT USER PHASE OUTCOME READY-IN AGE
kyc-0193 reviewer alice@acme.eu Failed Failed - 2m
maqpna doctor reports an attestation service still using the sample verifier, and unsigned attest responses, as posture findings. See maqpna session describe and maqpna doctor.