The trust root
How the Certificate Distribution Service comes up and what it vouches for — the in-memory mesh CA, EAR signing and JWKS rotation, the challenge/attest flow, sandbox identity, leaf signing, and why clients verify the CDS before trusting it.
The Certificate Distribution Service (CDS) is the trust root of a c8s cluster. In one process it verifies TEE attestation evidence, issues short-lived EARs (Entity Attestation Results), manages an in-memory mesh CA, signs leaf certificates, and gates issuance on what the requesting pod is actually running. 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 obtains its ECDSA P-384 mesh CA entirely in memory:
- The private key is in-process only and is never written to disk or to a Kubernetes Secret.
- Validity defaults to 8760h (one year), configurable with
--ca-cert-validity. The CN defaults toc8s Mesh CA(--ca-common-name). - The CDS generates a fresh self-signed CA at every startup.
- The public CA bundle is published so peers and clients can chain to it; the CA fingerprint and its expiry are logged at startup.
Because the key lives only in memory, a CDS restart generates a new CA and invalidates previously issued leaves. The CDS is a singleton: treat every restart as a re-bootstrap event and re-provision workloads after one — see Availability and operations.
EAR signing and JWKS rotation
The EAR is a JWT, signed with ES256 (ECDSA P-256), that the CDS mints via /attest-key
after a successful attestation. It is a TEE-bound credential for flows that attest a key
rather than obtain a certificate: a caller already holding one can have a CSR signed via
POST /sign-csr. Workload certificate issuance does not use it — /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 valid and published.--token-signer-rotation-jitter(default0.1) — jitter on the first tick.
Each key's kid is its RFC 7638 JWK thumbprint. The current JWKS is served at
GET /.well-known/jwks.json. A retiring key is rejected the moment it passes its overlap
deadline, and drops out of the JWKS at the same point — not at the next rotation, which with the
defaults is weeks later.
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. One challenge covers the whole issuance: it binds the evidence and the sandbox token, so neither can be replayed into a different request.
get-cert CDS attestation-api
[in the pod] [trust root] [same TCB as CDS]
│ │ │
│ POST /authenticate │ │
│──────────────────────►│ │
│ │ │
│ challenge │ │
│ (single-use, 32 B) │ │
│◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
│ │ │
┌───┴─────────────────────────────────────┐ │
│ redeem a sandbox token from the local │ │
│ inventory (see Sandbox identity) │ │
│ report_data = │ │
│ SHA-384(CSR pubkey ‖ challenge) │ │
└───┬─────────────────────────────────────┘ │
│ │ │
│ POST /attest │ │
│ challenge + evidence + CSR + sandbox_token │
│──────────────────────►│ │
│ │ verify(evidence, │
│ │ report_data) │
│ │──────────────────────►│
│ │ │
│ │ valid · launch_digest│
│ │◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│
│ │ │
│ ┌────────┴─────────────────────┐ │
│ │ measurement pin · CSR policy │ │
│ │ sandbox images allowlisted │ │
│ │ sign (mesh CA) │ │
│ └────────┬─────────────────────┘ │
│ │ │
│ leaf certificate + CA chain │
│◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
│ │ │The challenge is the freshness proof; the evidence proves the key lives in a measured TEE; the sandbox token proves which pod is asking.
Step by step, on POST /attest the CDS:
- consumes the one-time challenge (decoded from base64;
--challenge-ttl, default60s); - parses the CSR and verifies its self-signature;
- verifies the sandbox token, if one was sent — see Sandbox identity;
- 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; - enforces the measurement policy: under
--measurements-configthe evidence must match one pinned image whole — itslaunch_digest, then every register that image pins on TDX; under--measurementsthelaunch_digestmust be in the allowlist, and on TDX any--rtmrspins must match as well; - validates the CSR's DNS/IP SANs and CN against policy
(
--dns-san-pattern,--san-validation,--allowed-cn-pattern); - asks the sandbox's inventory what it is running and requires every image to be allowlisted;
- signs the CSR with the mesh CA and returns the PEM chain.
With neither --measurements nor --measurements-config set, the CDS pins nothing — any
TEE that produces valid evidence is accepted. Pin it in production: see
Obtain launch measurements for where a digest
comes from, and Pin multiple measurements for the
whole-image set a cluster that runs more than one VM image needs.
What an issued leaf carries
Leaves are short-lived: /attest issues at --cert-ttl, hard-capped at 24h, and /sign-csr
honors the caller's requested TTL up to --max-ttl (default 24h). Every leaf carries, in its
signed area:
| OID | Extension | Contents |
|---|---|---|
1.3.6.1.4.1.66378.1.1 | RA-TLS attestation | the requester's own hardware evidence, copied verbatim from the CSR |
1.3.6.1.4.1.66378.1.2 | audit digest | SHA-256 of the evidence the CDS accepted at issuance |
1.3.6.1.4.1.66378.1.4 | pod sandbox ID | present only when the requester presented a sandbox token |
The .1.1 extension is what makes an issued cert re-verifiable: get-cert binds evidence to
its bare CSR key with no nonce, and the CDS copies that extension onto the leaf, so a relying
party can re-check the hardware evidence itself rather than trusting the CA chain alone. The
challenge-bound evidence verified at issuance is deliberately not embedded — its REPORTDATA
includes the consumed challenge, so it could never re-verify against the bare key.
Sandbox identity
Hardware attestation binds a key to a launch measurement. Under Node-as-CVM every pod on a node shares that one measurement, so it says nothing about which pod stands behind a mesh key. Sandbox identity narrows that: a leaf names the CRI pod sandbox the CDS issued it to, and the CDS issues only after asking that sandbox's own admission record what it is running.
The inventory is the component that admitted the pod's containers — nri-image-policy on the node. It is the arbiter of both which sandbox a
process belongs to and what runs in that sandbox, and it serves two deliberately disjoint
surfaces: a local token route that only the pod's own get-cert reaches, and a network
identity/digests route on port 1019 that only the CDS reaches. The token route cannot
enumerate other sandboxes; the network route cannot mint identity.
get-cert -> node NRI inventory: sandbox token
get-cert -> CDS: challenge, CSR, sandbox token
CDS -> node inventory over RA-TLS: signing key and admitted image digests
CDS -> get-cert: signed workload certificateThe requester never names its pod and never reports its images; both answers come from the component that made the admission decision.
- get-cert asks anonymously. It
POSTs/sandboxcarrying only its CSR public key and the CDS challenge — no PID, pod name, or container ID. On Node-as-CVM this is a node-local Unix socket, where the kernel stamps the caller's credentials (SO_PEERCREDplusSO_PEERPIDFD) and the inventory resolves them PID → cgroup → container → sandbox from its own admission record. The webhook injects--workload-claimsintoc8s-cert; the local inventory socket is compiled into get-cert and fails closed if unavailable. - The inventory signs a token over the sandbox ID, SHA-256 of the requester's public key, the CDS challenge, and the IP of the node or guest serving its own identity endpoint. The envelope carries no credential for the signing key.
- get-cert forwards it opaquely in the
/attestbody assandbox_token. - The CDS resolves the key itself. It reads the inventory host out of the unverified
token — the only thing that value ever does is pick a dial target — requires it to be a
routable unicast IP literal (never a name, never loopback, link-local, or multicast) inside a
CIDR the operator configured with
--sandbox-inventory-cidr, then fetches the signing key fromGET /identityat<host>:1019over mutually-attested RA-TLS. Only then does it verify the signature, require the token's nonce to be the challenge it is consuming, and require the key digest to name the CSR key. - The CDS asks what the sandbox runs.
GET /digests/<sandboxID>at the same endpoint returns the sorted, deduplicated image digests the inventory currently tracks in that sandbox. Every one must be in the allowlist. An empty answer is refused, not treated as "nothing to check" — a sandbox always runs at least the sidecar that is asking.
Port 1019 is the whole identity argument. It is a compiled constant, not a deployment value,
and it is privileged: binding it requires the node's own network namespace, which the chart's
deny-host-namespaces ValidatingAdmissionPolicy (hostNamespacePolicy.enabled, on by default)
withholds from tenant pods. Measurement cannot make this distinction on Node-as-CVM — every pod
there shares the node's launch digest — but "answers on :1019 in the node's netns, at an
address inside the operator's node range" can.
--sandbox-inventory-cidr is required for any of this. Unset, the CDS refuses every request
carrying a sandbox token and logs a warning at startup; pods still get mesh certificates, but
with no sandbox ID and no issuance-time image gate. c8s install --node-cidr sets the chart
value cds.sandboxInventoryCIDRs, and with the flag omitted c8s install fills it in from the
cluster as one host route per node — a point-in-time snapshot, so a node added later is not
covered until the value is refreshed. A CDS with no --ratls-platform has no RA-TLS identity to
present to an inventory, so it also refuses every token.
What vouches for a sandbox ID — and what does not
The sandbox ID rides the leaf's signed area; it is not folded into REPORTDATA. The mesh CA signature, not the hardware evidence, is what authenticates it. Everything downstream encodes that:
- A relying party pins it with
c8s verify --sandbox-id <id> --mesh-ca <bundle>.--mesh-cais mandatory with--sandbox-id. Without it the verdict still reports the ID, but alongside asandbox_id_notesaying it is not verified — an unqualified ID never reads as attested. - In-mesh, a
SandboxIDpin is enforced only on the CA-verified branch of the dual verifier. Pure RA-TLS verification of a self-signed peer fails closed on a pin, because a self-signed leaf's extension is whatever its holder chose.
The gate is membership, not composition: every running image must be allowlisted, but the
running set is not required to match a whole workload entry. Issuance lands at arbitrary points
in a pod's lifecycle — an init container running, main containers coming up one at a time, one
restarting, completed init containers reaped — and in each of those the running set is a strict
subset of what the pod declares. Requiring the whole set would deny certificates for ordinary
states, permanently so once init containers are reaped. So a leaf's sandbox ID means this key
belongs to pod X, not pod X runs exactly workload Y. Per-container digest and argv policy is
enforced continuously at admission by nri-image-policy instead — see
The Allowlist.
The residual trust is the inventory itself: the key's provenance narrows to a node, not to a
process, so anything able to bind :1019 on a node — the inventory, or a privileged node
DaemonSet — can sign for any sandbox that node admitted. See
Limitations.
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. It is the same store the sandbox gate above
checks at issuance.
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), but
it is host-supplied config. The serving certificate commits only CDS's own key and measurement,
so cross-check the pinned set with c8s cds verify --operator-keys <bundle>, which fails closed
on a swapped key set.
See The Allowlist for the data model, the full write flow, seeding, and enforcement.
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, minted for the platform named by --ratls-platform, which the CDS requires. 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 accepts any
RA-TLS-attested CDS and logs a warning — pin --cds-measurements in production (the CDS's own
launch digest; see Obtain launch measurements).
To check the CDS on demand rather than as part of a client handshake — after install, or on a
schedule from CI — run c8s cds verify.
The CDS also publishes the reference values it is enforcing at GET /measurements, so the same
run can compare that set against the operator's own file — see
Pin multiple measurements.
Browsers cannot do RA-TLS at all — they cannot inspect a certificate mid-handshake — so external
clients get a different proof, and it terminates in the same place: the cds-attest sidecar
beside router signs its post-quantum session transcript with the CDS-issued mesh leaf and
commits the mesh CA alongside it, so the identity a browser ends up trusting is the mesh identity
this CA issued. See consumer & browser verification.
See also
- CDS HTTP API — every endpoint, with auth and response codes.
- CLI reference →
c8s cds— every CDS flag. - Obtain launch measurements — where the values the CDS pins come from.
- Verify a deployment — checking the CDS, the router, and workloads by hand or in CI.