CDS Bootstrapping & Verification
How the Certificate Distribution Service comes up — the in-memory mesh CA, token signing and JWKS rotation, the challenge/attest flow, leaf signing, CA handoff, and how clients verify the CDS itself.
The Certificate Distribution Service (CDS) is the trust root of a c8s cluster. In one process it verifies TEE attestation evidence, issues short-lived attestation-bound tokens, manages an in-memory mesh CA, signs leaf certificates, and — optionally — hands its CA off to peer replicas. There is no internal RA-TLS hop to an external signer, so the CA private key never leaves CDS memory.
Mesh CA bootstrap
At startup the CDS generates a fresh ECDSA P-384 mesh CA entirely in memory:
- The private key is in-process only and is never written to disk.
- Validity defaults to 8760h (one year), configurable with
--ca-cert-validity. - The public CA bundle is published so peers and clients can chain to it; the CA fingerprint is logged at startup.
Because the key lives only in memory, any CDS restart generates a new CA and invalidates previously issued leaves. That is acceptable for the chart-managed default; the production direction keeps the CA in attested CVM memory and replicates it via handoff.
Token signing and JWKS rotation
The attestation-bound token is a JWT, signed with ES256 (ECDSA P-256), that the
CDS mints (via /attest-key) after a successful attestation. It is the cluster's
attestation-bound bearer credential: a caller presents the token to authorize a
CA handoff. (Certificate issuance does not use this token — /attest
verifies evidence and returns the signed leaf in one step. Allowlist writes use an
operator token, not an EAR.) Key claims:
| Claim | Meaning |
|---|---|
iss | issuer (--ear-issuer, default cds) |
iat / exp | issued-at and expiry. the lifetime follows --cert-ttl (default 24h) |
submods.attester | the attestation result, including ear_status, the trustworthiness vector, raw evidence, and the normalized launch_digest |
tee_public_key | base64url PKIX DER of the ECDSA key the TEE attested — used for key binding |
pbh (optional) | payload-body hash, binding the token to a specific request body |
The signing key rotates with overlap so verification never breaks mid-rotation:
--token-signer-rotation-interval(default720h) — how often a new P-256 key becomes active.--token-signer-overlap(default25h) — how long a retired key stays published in the JWKS.--token-signer-rotation-jitter(default0.1) — jitter on the first tick.
Each key's kid is its RFC 7638 JWK thumbprint. The current JWKS (active + non-expired
retiring keys) is served at GET /.well-known/jwks.json.
The attestation flow
The CDS uses a nonce-challenge model so attestation evidence is cryptographically bound to a CSR's public key, preventing evidence replay or substitution.
Workload CDS attestation-api
[get-cert] [host] [host]
│ │ │
│ POST /authenticate │ │
│──────────────────────►│ │
│ │ │
│ nonce │ │
│ one-time challenge │ │
│◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
│ │ │
┌───┴────────────────────────────────────┐ │
│ report_data = │ │
│ SHA-384(CSR pubkey ‖ nonce) │ │
└───┬────────────────────────────────────┘ │
│ │ │
│ POST /attest │ │
│ challenge + evidence + CSR │
│──────────────────────►│ │
│ │ verify(evidence, │
│ │ report_data) │
│ │──────────────────────►│
│ │ │
│ │ valid · launch_digest│
│ │◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│
│ │ │
│ ┌────────┴─────────────────────┐ │
│ │ check measurement · │ │
│ │ validate CSR · sign (mesh CA)│ │
│ └────────┬─────────────────────┘ │
│ │ │
│ leaf certificate + CA chain │
│◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
│ │ │Step by step, on POST /attest the CDS:
- consumes the one-time challenge (decoded from base64);
- parses the CSR and verifies its self-signature;
- computes the expected
report_data = SHA-384(CSR_public_key ‖ challenge)(first 48 bytes); - calls the attestation-api
/verifywith the evidence and that expected report data; - requires
signature_valid == trueandreport_data_match == true; - if
--measurementsis set, requires the evidencelaunch_digestto be in that allowlist; - validates the CSR's DNS/IP SANs and CN against policy
(
--dns-san-pattern,--san-validation,--allowed-cn-pattern); - signs the CSR with the mesh CA and returns the PEM chain.
--measurements defaults to empty, which means no pinning — any TEE that produces valid
evidence is accepted. The CDS logs a warning at startup in this mode. Pin it in production —
see Obtaining launch measurements for how to get the values
to put here.
The image allowlist
The CDS also owns and serves the image allowlist — the digests of the container images
permitted to run (a SQLite store at --allowlist-db, served at /allowlist). Updating it always
goes through the CDS; there is no side channel.
Who may update it is decided by the operator at CDS bootstrap: the install-time value
cds.operatorKeys (--operator-keys, set by c8s install --operator-keys) pins the operator
EC public key(s) authorized to write; empty rejects every write while reads keep serving.
For each write, the c8s allowlist CLI
signs a short-lived token with the operator's private key, bound to the exact method, path, and
request body; the CDS verifies the signature against its pinned keys and re-checks each binding
before committing. The pinned bundle is public material (served back at GET /operator-keys so
c8s cds verify can report it), but it is host-supplied config — not part of the CDS's launch
measurement.
See The Allowlist for the data model, the full write flow, seeding, and enforcement.
CA handoff
The in-memory-only CA is exactly what makes high availability tricky. Because the private key never touches disk (see Mesh CA bootstrap), the usual HA tactics don't apply: a cold-standby replica or a simple pod restart each comes up generating a fresh CA, which signs a different chain and invalidates every leaf already issued. There is no shared key on a PersistentVolume to fail over to — that's the whole point of keeping it in memory.
Handoff resolves this by moving the live CA from one attested CDS to another, in memory, over an attested channel — never through disk or a third party:
- A successor replica starts, generates an ephemeral X25519 keypair, attests itself, and
calls
POST /handoffon the active CDS, presenting its attestation-bound token and that ephemeral public key. - The active CDS verifies the token's signature and checks the recipient's
launch_digestagainst--handoff-measurements— so the CA only ever leaves for another CDS whose exact launch measurement you have pinned. - It returns the CA payload (cert and private key) AES-GCM-wrapped to the recipient's X25519 key. The key crosses the wire only as ciphertext that one attested enclave can open, and it is written to disk on neither side.
The successor now holds the same CA, so every certificate the cluster has already issued stays valid across the changeover — a rolling upgrade or a failover happens with no trust-on-first-use gap and no mass re-issuance. The CA key spends its entire life inside attested CVM memory, replicated enclave-to-enclave.
Handoff is opt-in (cds.handoff.enabled=true) and gated on --handoff-measurements; with no
measurements listed, POST /handoff refuses every request. Pair it with the CA's validity
window (--ca-cert-validity) and the /readyz gate — which starts failing once remaining
validity drops below --min-ca-validity — so a successor is brought up and handed the CA well
before the active one's certificate expires.
Verifying the CDS itself (RA-TLS)
Clients verify the CDS before trusting anything it returns. The CDS serves its API over
RA-TLS: its serving certificate carries hardware evidence (SEV-SNP or TDX) in an X.509
extension. A client
(for example get-cert --cds-measurements <sha384-csv>) verifies that evidence through its
local attestation-api and checks the CDS launch digest against the supplied allowlist before
completing the handshake. With no measurements supplied the client falls back to
trust-on-first-use and logs a warning — pin --cds-measurements in production (the CDS's own
launch digest; see Obtaining launch measurements).
To check the CDS on demand — after install, or from CI — rather than as part of a client
handshake, use c8s cds verify. It pulls the CDS's RA-TLS evidence and re-verifies it with the
cluster's own attestation engine. Start by inspecting what the CDS reports (-o json gives the
structured form):
# c8s-cds is a headless Service; dial the pod IP directly (the kata guest policy blocks
# kubectl port-forward/exec), from somewhere with cluster-network reach (a node, or 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" -o json{
"verified": true,
"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": false
}Reading that report field by field:
| Field | This output | What it tells you |
|---|---|---|
verified | true | The verdict — the AMD signature chain, the REPORTDATA binding, the debug flag, and any TCB floor all passed. Read it together with measurement_pinned (see the warning below). |
backend | attestation-go | Who produced the verdict: attestation-go, the in-process Go port of the attestation-rs engine the CDS itself runs (no container needed). |
source | RA-TLS serving certificate … | Where the evidence came from: the bare SEV-SNP report carried in the CDS's serving cert (vs. an LB discovery doc or a --from-file). |
platform | snp | The attested TEE platform: AMD SEV-SNP. |
measurement | f894…34a3 | The SHA-384 launch digest the hardware actually reported. This is the value you pin — compare it to the CDS's published or recomputed digest. |
current_tcb | bootloader=12 tee=0 snp=28 microcode=28 | The platform's current TCB (security version numbers). Enforce minimums with --min-tcb-bootloader / -tee / -snp / -microcode; an out-of-date host then fails. |
fresh / binding | false / "…no per-request nonce…" | 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." (The CDS's own /attest flow binds a one-time challenge and is a freshness proof.) |
cert_sha256 | 0109…92f9 | SHA-256 of the serving cert the evidence was bound to — useful to record in an audit log, or to confirm you verified the exact cert presented. |
measurement_pinned | false | Whether you supplied --measurements. false means nothing was pinned. |
This run shows verified: true with measurement_pinned: false — the deceptive
combination. It only means "a genuine TEE," not "the CDS image you audited." Any
attested enclave running any code would pass identically. In text mode the command prints an
explicit WARNING: no --measurements pinned — any genuine TEE is accepted (UNSAFE for production).
Once you've confirmed the reported measurement is the CDS's expected launch digest, pin it so a
wrong image is rejected (exit code 2) instead of silently accepted:
c8s cds verify "https://$CDS_IP:8443" \
--measurements f894e79d20ab98ba8fc878425739d1b5900999835fe3f94c90fa8603e0636325c131192eb0005f5d3b8134e363d434a3measurement_pinned now reports true, and verification fails closed if the CDS ever presents a
digest you didn't pin. See operator & service verification for the
load-balancer and workload variants, the CI exit-code/JSON contract, and how to independently
recompute the digest with sev-snp-measure.
Obtaining launch measurements
A measurement is the SHA-384 launch digest of a confidential VM — a hash the TEE hardware
(AMD SEV-SNP or Intel TDX) computes over the exact launch state (firmware, guest kernel, and the
kernel command line, which embeds the dm-verity root hash of the rootfs). Every *measurements
flag in c8s takes one or more of these hex strings. Because a reproducible image always yields the same
digest, pinning it means "accept only this exact, audited image — nothing else."
There are three ways to get the value, best first:
Use the digest published with the image (recommended)
c8s guest images are reproducible and ship their predicted launch digest. The Kata guest
image's digest is published in its manifest.json and pinned via kata.guestImage.tag;
component CVM images publish theirs the same way. Pin the published digest for the exact image
version (tag) you deploy — since the build is reproducible, that value is "exactly this
audited code." This is the production path.
Recompute it with sev-snp-measure
To independently verify the published value (or for a custom image), recompute the expected
digest yourself with AMD's sev-snp-measure 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 that's in the manifest; a mismatch means the image
isn't what you think it is. (sev-snp-measure is AMD-specific; on Intel TDX the equivalent
launch values — the MRTD and RTMRs — are predicted with TDX's own tooling, but the pin-the-
published-digest workflow above is the same.) To use this as a post-deploy cross-check —
recompute, pin, then verify a live node reports the same digest — see
Operator & service verification → Independently recompute the measurement.
Read it from a live attestation report (inspect, then pin)
To see what a running component actually measures, read the launch_digest from its evidence:
get-cert --discovery-out discovery.json … writes the attestation evidence and measurement for
the issued certificate, and the attestation-api's verify result returns claims.launch_digest.
Use this to inspect what's running — but in production pin the published or recomputed
value above, not merely whatever a node reports, or you've only achieved trust-on-first-use.
Each flag wants the digest of a different party:
| Flag | Pin the launch digest of… |
|---|---|
cds.measurements (--measurements) | the workloads/peers allowed to attest to the CDS |
ratlsMesh.measurements | the mesh peers allowed to connect |
--cds-measurements (get-cert, ratls-mesh cds mode) | the CDS itself |
--measurements (c8s allowlist) | the CDS itself, before reading or writing the allowlist |
--handoff-measurements (the chart reuses cds.measurements when cds.handoff.enabled) | peer CDS replicas allowed to receive the CA |
measurements in c8s-verify | the user-facing LB enclave |
Measurements identify confidential VMs — per-pod CVMs (pod-as-CVM) or a confidential node (node-as-CVM). On a non-confidential host, components run as ordinary containers with no launch digest, so pinning only becomes meaningful once the node or pods are actually CVMs.
Endpoint summary
| Method | Path | Purpose |
|---|---|---|
POST | /authenticate | issue a one-time challenge nonce |
POST | /attest | verify evidence + sign a CSR, returning the leaf PEM chain |
POST | /attest-key | verify evidence + issue an attestation-bound token bound to the caller's key (the handoff credential) |
POST | /handoff | hand the mesh CA to an attested peer replica |
GET POST PUT DELETE | /allowlist | read / add / replace / remove image digests (writes require an operator token) |
GET | /operator-keys | the pinned operator public-key PEM bundle (404 when allowlist writes are disabled) |
GET | /.well-known/jwks.json | the token-signing JWKS (ES256) |
GET | /ca | the mesh CA bundle |
GET | /readyz | readiness (fails if CA validity < --min-ca-validity) |
See the CLI Reference for every CDS flag.