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 everyaz-snpenvelope (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
--kind | Default evidence mode | What you point it at |
|---|---|---|
cds | ratls-cert | the CDS RA-TLS endpoint (its serving cert carries the evidence) |
lb | discovery | the load balancer's /v1/discovery document (cert + evidence, VCEK inline) |
workload | ratls-cert | a workload's RA-TLS serving endpoint |
auto | auto (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.pubVerification 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 | sha256sumThis 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-catakes 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-idpins the CRI pod sandbox the leaf names (containerd emits 64 hex characters). It requires--mesh-caand 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 CAWithout --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.txtThese 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
0–255) 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:
| Code | Meaning |
|---|---|
0 | verified |
1 | usage error |
2 | evidence obtained, but verification / policy failed (e.g. wrong measurement) |
3 | evidence 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
| Field | What it tells you |
|---|---|
verified | The verdict — the vendor signature chain, the REPORTDATA binding, the debug-guest check, and any TCB floor all passed. Read it together with measurement_pinned. |
backend | Who produced the verdict: attestation-go, the in-process Go port of the attestation-rs engine the cluster itself runs. |
source | Where the evidence came from — an RA-TLS serving cert, an LB discovery document, or a --from-file. |
platform | The attested TEE platform: snp, az-snp, gcp-snp, tdx, or az-tdx. |
measurement | The 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_tcb | The platform's current TCB (security version numbers). Enforce minimums with --min-tcb-bootloader / -tee / -snp / -microcode. SNP only. |
fresh / binding | Freshness. 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_sha256 | SHA-256 of the serving cert the evidence was bound to — worth recording in an audit log. |
measurement_pinned | Whether 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 hexA 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: falsein 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 verifyof 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=andsmt=in the text output are not populated — they always printfalse. The debug policy is still enforced inside the verifier (that is what leaving--allow-debugoff 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 thetls-lb's nginx serving port both answer unattested clients on their public address (thetls-lbserves/v1/discoverythere with no client cert), soc8s cds verifyandc8s verify <lb>work with no mesh changes.
See also
- CLI reference →
c8s verify— every flag. - Obtain launch measurements — where the value you pin comes from.
- The verification model — who verifies what, and the browser path this page does not cover.
- The trust root — how the CDS verifies clients.
- The image allowlist — what the operator keys authorize.