MAQPNADocs

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:

  1. Register. For an attested session the operator generates a 32-byte random nonce and calls POST {--attest-url}/v1/releases with the session's claims, the nonce and renewUntil (the session's expiresAt). The release lives at most 15 minutes and can be used once. The operator records status.attestation and sets TokenReady with reason AttestationGated.
  2. Build the pod. The sandbox gets an init container maqpna-attest (image --attest-agent-image) with --release-id, --nonce and --out=/var/run/maqpna/token, sharing a 1 MiB memory-backed emptyDir that the agent container mounts read-only. A native sidecar maqpna-attest-renewer keeps running beside the agent. The per-session NetworkPolicy also allows egress to the attestation service.
  3. 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, provider sev_guest or tdx_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.
  4. Submit. The agent posts the evidence to POST /v1/releases/{id}/attest.
  5. 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, exp required, iss, maximum age -trustee-max-token-age 10 minutes, no future iat) and that its report_data equals SHA-512(nonce ‖ public key).
  6. Appraise. The claims are compared with your reference values: TEE type in teeTypes, launch measurement in allowedMeasurements, debug off (a missing debug claim counts as debug unless -trustee-allow-missing-debug-claim), SVN at least minSvn. The workload is appraised too: TDX RTMR[0..3] (tdxRtmr) or MRCONFIGID (tdxMrConfigId), SNP HOST_DATA (snpHostData). A report with no workload reference value is rejected unless allowLaunchMeasurementOnly: true. An empty measurement list rejects everything.
  7. Mint. Only now does the service ask the identity broker for the token (POST /v1/token).
  8. 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 data maqpna-release:<id>. The plaintext is zeroed and the release is journaled as consumed before the response is sent.
  9. 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.