Verify a deployment

Check a deployed c8s component from the operator side with c8s verify — the CDS, the load balancer, or a workload — pin a launch measurement, cross-check the operator keys and sandbox ID, read the verdict field by field, and wire the exit codes into CI.

c8s verify confirms that a deployed c8s component is a genuine TEE running the exact code you expect — after install, from your laptop or a CI runner. c8s cds verify is the same command with CDS defaults.

Verification only means something once you pin a launch measurement; run it unpinned and it accepts any genuine TEE. For how the verifier reaches its verdict, and for the browser path this one does not cover, see the verification model. Every flag is in the CLI reference.

Requirements

On the machine running c8s verify:

  • Outbound HTTPS to AMD KDS (kdsintf.amd.com), which the verifier uses to fetch the VCEK when verifying a bare SEV-SNP report. Evidence that ships the VCEK inline verifies offline: the LB discovery document, and every az-snp envelope (Azure wraps the VCEK in its HCL report). TDX quotes carry their DCAP certification chain inline and verify offline too.
  • Network reach to the component you're verifying (see Reachability).

No container runtime is required — verification is entirely in-process.

What you can verify

--kindDefault evidence modeWhat you point it at
cdsratls-certthe CDS RA-TLS endpoint (its serving cert carries the evidence)
lbdiscoverythe load balancer's /v1/discovery document (cert + evidence, VCEK inline)
workloadratls-certa workload's RA-TLS serving endpoint
autoauto (discovery doc first, then RA-TLS cert)when you don't pass --kind

An RA-TLS certificate carries either a raw SEV-SNP report (bare-metal SNP) or a self-describing evidence envelope (az-snp, gcp-snp, tdx, az-tdx). A raw TDX report with no envelope is rejected — read that component's discovery document instead.

Override the chosen mode with --mode (ratls-cert, discovery, attestation-endpoint, or auto). You can also skip the network entirely and verify saved evidence with --from-file.

Don't use --mode attestation-endpoint against the tls-lb.

That mode GETs /.well-known/c8s/attestation with a fresh nonce and expects REPORTDATA to commit SHA-384(x25519 ‖ mlkem768 ‖ nonce). The tls-lb attestation sidecar commits an identity transcript instead (see Consumer verification), which this mode does not reconstruct, so the check fails on a binding mismatch. Verify the LB with --kind lb (discovery mode).

Verify the CDS

c8s cds verify is a shorthand for c8s verify --kind cds (default port 8443); --mode stays auto, which resolves to ratls-cert for a CDS target. The CDS serves RA-TLS: its serving certificate carries the TEE evidence in an X.509 extension, and verify trusts that attestation, not the certificate chain.

# c8s-cds is headless under Pod-as-CVM, so read its endpoint IP and dial the pod directly
# from somewhere with cluster-network reach (a node, or over a VPN):
CDS_IP=$(kubectl get endpoints c8s-cds -n c8s-system -o jsonpath='{.subsets[0].addresses[0].ip}')

c8s cds verify "https://$CDS_IP:8443" --measurements <SHA384_LAUNCH_DIGEST>

kubectl port-forward also works: Kata guests bake C8S_MESH_INBOUND_PASSTHROUGH=tcp:8443, which is what keeps the CDS front door reachable by clients with no mesh certificate. Pass --server-name to set SNI when you forward to localhost. (kubectl exec and kubectl logs are denied by the locked guest policy — only the --debug guest image allows them, and it has a different launch measurement.)

A PKI / SAN mismatch when you dial the raw IP is expected and fine — verify trusts the attestation embedded in the serving cert, not the hostname on the certificate.

A successful run prints:

✓ VERIFIED  (attestation-go backend)
  source:       RA-TLS serving certificate at 10.42.0.153:8443
  verified at:  2026-07-31T09:14:22Z
  platform:     snp
  measurement:  f894e79d20ab98ba8fc878425739d1b5900999835fe3f94c90fa8603e0636325c131192eb0005f5d3b8134e363d434a3
  TCB:          bootloader=12 tee=0 snp=28 microcode=28   debug=false smt=false
  cert sha256:  0109d41e0907ab4ecd5823b5d08c3a6b5d69751322338e28c78f8378c56f92f9
  binding:      REPORTDATA binds the certificate public key (no per-request nonce — not a freshness proof)
  note:         freshness NOT proven (no per-request nonce bound)

Check the operator keys CDS pins

For a cds target the verdict also reports the pinned operator keys — SHA-256 fingerprints of the public keys authorized to write the image allowlist — fetched from GET /operator-keys over a connection pinned to the attested serving certificate, so the list cannot be substituted in transit:

  operator keys (allowlist writes; CDS-reported config, NOT covered by the measurement):
    sha256:<FINGERPRINT>

As the output says, the key list is CDS-reported config, not part of the launch measurement. Reporting it alone is visibility, not proof. Turn it into a check by passing your own bundle — the same public keys you pinned at c8s install --operator-keys:

c8s cds verify "https://$CDS_IP:8443" \
  --measurements <SHA384_LAUNCH_DIGEST> \
  --operator-keys operator.pub

Verification then fails unless the key set CDS serves matches the bundle exactly, and it fails closed: an endpoint that errors on /operator-keys cannot dodge the check. A 404 (allowlist writes disabled) is not an error — it compares as the empty set, so --operator-keys still catches a CDS that quietly dropped its keys.

The fingerprints are over the PKIX/SPKI DER, so you can reproduce one locally:

openssl pkey -pubin -in operator.pub -outform DER | sha256sum

This check only protects the verifier that runs it.

CDS's arguments are host-supplied: a control plane can restart CDS with a different operator key set, and nothing in the cluster notices. Run c8s cds verify --measurements --operator-keys continuously from CI — not once at bootstrap — and gate public ingress on it passing.

Without --kind cds the fetch is skipped rather than silently omitted, and the verdict says so: operator-keys cross-check skipped: target kind is not cds (use --kind cds to enable). With writes disabled it reads operator keys: endpoint reports no pinned operator keys (allowlist writes disabled).

Verify the load balancer

The LB exposes its evidence over a plain HTTPS discovery document (the same one the browser library reads), so no special network path is needed:

c8s verify "https://lb.example.com:443" --kind lb --measurements <SHA384_LAUNCH_DIGEST>

--kind lb selects discovery mode, which GETs /v1/discovery (override the path with --discovery-path). The discovery doc ships the VCEK inline, so this works even without KDS egress.

Verify a workload

A workload that serves RA-TLS is verified like the CDS — point at its serving endpoint and pin its launch digest:

c8s verify "https://<WORKLOAD_HOST>:<PORT>" --kind workload --measurements <SHA384_LAUNCH_DIGEST>

Under Node-as-CVM every pod on a node shares that node's launch digest, so the measurement alone does not tell you which pod answered. Two extra pins narrow it:

c8s verify "https://<WORKLOAD_HOST>:<PORT>" --kind workload \
  --measurements <SHA384_LAUNCH_DIGEST> \
  --mesh-ca mesh-ca.pem \
  --sandbox-id <CRI_SANDBOX_ID>
  • --mesh-ca takes a PEM bundle; the target's leaf must chain to it. That chain is the only thing that authenticates the sandbox ID, because the ID rides the leaf's signed area and is never folded into REPORTDATA.
  • --sandbox-id pins the CRI pod sandbox the leaf names (containerd emits 64 hex characters). It requires --mesh-ca and errors out without it, rather than pinning a string the presenter chose.

Both need a certificate to inspect, so they only work in a cert mode — not with --mode attestation-endpoint.

Whenever the evidence verifies and the leaf carries an ID, the verdict reports it with a note naming what stands behind it, so an unqualified ID never reads as attested:

  sandbox id:   4c9a...e17b
                verified: the leaf chains to the supplied mesh CA

Without --mesh-ca the same line reads not verified: CDS's signature on the leaf vouches for this ID; pass --mesh-ca to check it.

Verify saved evidence

To verify evidence you've already captured — a saved RA-TLS PEM certificate or an attestation-response JSON — pass --from-file and skip the dial:

c8s verify --from-file cds-cert.pem --measurements <SHA384_LAUNCH_DIGEST>

For a bare evidence file with no transport to bind REPORTDATA to, supply the expected value with --expected-report-data <HEX> (1–64 bytes). Pass the anchor exactly as the producer bound it — for c8s bindings that is the unpadded 48-byte SHA-384. Do not zero-pad it to 64 yourself: the hardware-report verifiers pad per platform, and the Azure vTPM verifiers compare the value raw, so a pre-padded anchor fails there. The flag does not apply to a certificate (its binding is the certificate key) and is rejected on one.

Pin the launch measurement

This is the part that makes verification mean anything. With no --measurements, the command still runs and reports the report's digest, but prints an UNSAFE warning — any genuine TEE is accepted:

WARNING:      no --measurements pinned — any genuine TEE is accepted (UNSAFE for production)

Pin one or more allowed SHA-384 launch digests so only your exact, audited image passes:

# repeatable / comma-separated
c8s cds verify "https://$CDS_IP:8443" --measurements <DIGEST_A>,<DIGEST_B>

# or from a file, one hex digest per line
c8s cds verify "https://$CDS_IP:8443" --measurements-file digests.txt

These are the same digests described under Obtaining launch measurements — for the CDS, pin the CDS's own launch digest. You can additionally require minimum TCB component versions (--min-tcb-bootloader, --min-tcb-tee, --min-tcb-snp, --min-tcb-microcode, each 0255) and reject debug-enabled guests by leaving --allow-debug off (the default).

Use it in CI

The exit codes are a stable contract, so a wrong measurement is distinguishable from an unreachable endpoint:

CodeMeaning
0verified
1usage error
2evidence obtained, but verification / policy failed (e.g. wrong measurement)
3evidence unavailable (unreachable / unparseable / collateral not fetchable)

Pair that with -o json for a machine-readable verdict:

c8s cds verify "https://$CDS_IP:8443" --measurements-file digests.txt \
  --operator-keys operator.pub -o json
{
  "verified": true,
  "verified_at": "2026-07-31T09:14:22Z",
  "backend": "attestation-go",
  "source": "RA-TLS serving certificate at 10.42.0.153:8443",
  "fresh": false,
  "binding": "REPORTDATA binds the certificate public key (no per-request nonce — not a freshness proof)",
  "platform": "snp",
  "measurement": "f894e79d20ab98ba8fc878425739d1b5900999835fe3f94c90fa8603e0636325c131192eb0005f5d3b8134e363d434a3",
  "current_tcb": "bootloader=12 tee=0 snp=28 microcode=28",
  "cert_sha256": "0109d41e0907ab4ecd5823b5d08c3a6b5d69751322338e28c78f8378c56f92f9",
  "measurement_pinned": true,
  "operator_keys": ["9d1f...c204"]
}

operator_keys carries the hex SHA-256 fingerprints, or operator_keys_note explains why they were not fetched. A leaf with a sandbox ID adds sandbox_id and sandbox_id_note. On failure, verified is false and error names the check that failed.

Reading the verdict field by field

FieldWhat it tells you
verifiedThe verdict — the vendor signature chain, the REPORTDATA binding, the debug-guest check, and any TCB floor all passed. Read it together with measurement_pinned.
backendWho produced the verdict: attestation-go, the in-process Go port of the attestation-rs engine the cluster itself runs.
sourceWhere the evidence came from — an RA-TLS serving cert, an LB discovery document, or a --from-file.
platformThe attested TEE platform: snp, az-snp, gcp-snp, tdx, or az-tdx.
measurementThe SHA-384 launch digest the hardware actually reported. This is the value you pin — compare it to the component's published or recomputed digest.
current_tcbThe platform's current TCB (security version numbers). Enforce minimums with --min-tcb-bootloader / -tee / -snp / -microcode. SNP only.
fresh / bindingFreshness. REPORTDATA binds the certificate's public key, not a live nonce — so it proves "this key was minted in a TEE with this measurement," not "the enclave answered just now."
cert_sha256SHA-256 of the serving cert the evidence was bound to — worth recording in an audit log.
measurement_pinnedWhether you supplied --measurements. false means nothing was pinned.

verified: true with measurement_pinned: false is the deceptive combination.

It means "a genuine TEE," not "the image you audited." Any attested enclave running any code passes identically. In text mode the command prints an explicit WARNING: no --measurements pinned — any genuine TEE is accepted (UNSAFE for production).

Independently recompute the measurement

The "spicier" cross-check: rather than trusting the digest you were handed, recompute it yourself from the launch components and confirm that what a live node reports matches what the code should produce. The worked example below is AMD SEV-SNP, using sev-snp-measure; Intel TDX is covered at the end of the section.

Recompute the expected digest

From the launch components — the OVMF firmware, the guest vmlinuz, and the kernel cmdline (which carries the dm-verity root_hash) — at the same vCPU count used at launch (c8s measures at 1 vCPU for a stable digest):

# illustrative — see the sev-snp-measure docs for the exact flags for your image
sev-snp-measure --mode snp --vcpus 1 \
  --ovmf OVMF.fd \
  --kernel vmlinuz \
  --append "<KERNEL_CMDLINE_INCLUDING_ROOT_HASH>" \
  --output-format hex

A reproducible build yields the same digest published in the image manifest. A mismatch means the image isn't what you think it is.

Pin it and verify the live component

Feed the recomputed digest straight into c8s verify. If the live node's report carries a different launch digest, verification fails with exit code 2:

c8s cds verify "https://$CDS_IP:8443" --measurements <RECOMPUTED_SHA384>

This closes the loop end-to-end: the code you audited → the digest you computed → the digest the hardware actually measured. Running once without --measurements to read what a node reports is fine for discovery — but always pin the recomputed or published value, or you've only achieved trust-on-first-use.

On Intel TDX the pinned value is the MRTD — the TD's build-time measurement, the same 48-byte SHA-384 shape as the SNP launch digest, and what --measurements compares for a TDX target. Predict it from the TD's virtual firmware with Intel's TDX measurement tooling, then pin the predicted value exactly as above. The runtime measurement registers (RTMRs), which record later boot components, are not replayed by the c8s RA-TLS verify path — the TDX event log is deliberately kept out of the serving certificate (it can be fetched out-of-band) — so on TDX this cross-check covers the MRTD. The --min-tcb-* floor is SNP-only and is not enforced on a TDX target.

Caveats

  • Freshness. Verifying an RA-TLS serving cert binds REPORTDATA to the certificate key, not a per-request nonce — so it proves "this key was born in a TEE with this measurement," not "freshly, right now" (fresh: false in the output). The discovery document is the same: its challenge is fixed at issuance time. The CDS's own challenge/attest flow uses a one-time nonce; c8s verify of a serving cert or discovery doc does not.
  • Revocation. The verifier checks the vendor signature chain and the TCB floor. It does not fetch Intel TCB-info or CRL collateral, so a TDX quote is not checked against Intel's revocation lists.
  • debug= and smt= in the text output are not populated — they always print false. The debug policy is still enforced inside the verifier (that is what leaving --allow-debug off does, and a debug guest fails the run); don't read that line as the proof.
  • Reachability under Kata. Reach each component on its public / host address, not the in-cluster ClusterIP. The ClusterIP path goes through the RA-TLS mesh and demands an attested client cert (tls: certificate required). The CDS RA-TLS endpoint and the tls-lb's nginx serving port both answer unattested clients on their public address (the tls-lb serves /v1/discovery there with no client cert), so c8s cds verify and c8s verify <lb> work with no mesh changes.

See also