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, CA handoff, 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, gates issuance on what the requesting pod is actually running, 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 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). - With no
--handoff-peer-urlthe CDS generates a fresh self-signed CA. With one set it adopts a surviving peer's CA over the attested handoff flow instead, and fails startup if that peer is unreachable, refuses, or attests a different operator-key policy — so a partition can never mint a divergent trust root. - The public CA bundle is published so peers and clients can chain to it; the CA fingerprint and
its source (
self-generatedoradopted-from-peer) are logged at startup.
Because the key lives only in memory, a CDS restart without a handoff peer 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.
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: CDS's own handoff machinery uses it to authorize
CA handoff, and 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 |
operator_keys_hash (optional) | the canonical hash of the CDS operator public-key set, folded into REPORTDATA so two handoff replicas can require the same write policy |
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; - 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); - 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.
--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 Obtain launch measurements for how to get
the values to put here.
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.59888.1.1 | RA-TLS attestation | the requester's own hardware evidence, copied verbatim from the CSR |
1.3.6.1.4.1.59888.1.2 | audit digest | SHA-256 of the evidence the CDS accepted at issuance |
1.3.6.1.4.1.59888.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
Node-as-CVM, policy-monitor inside the Kata guest. 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.
╔═ one TEE: the node under Node-as-CVM, the guest under Pod-as-CVM ═╗
║ ║
║ ┌────────────┐ 1 CSR pubkey + ┌────────────────────────┐ ║
║ │ get-cert │──── CDS challenge ─►│ inventory: │ ║
║ │ [c8s-cert] │◄─── signed sandbox ─│ nri-image-policy on │ ║
║ └─────┬──────┘ 2 token │ Node-as-CVM, or │ ║
║ │ │ policy-monitor in the │ ║
║ │ │ Kata guest │ ║
║ │ └───────────▲────────────┘ ║
╚════════║════════════════════════════════════════║═════════════════╝
│ │ 4 GET /identity
│ 3 sandbox_token, forwarded │ GET /digests/<id>
│ verbatim on POST /attest │ on :1019, mutually
▼ │ attested RA-TLS
┌──────────────────────────────────────────┐ │
│ CDS — verifies the token under the key ├──────┘
│ it fetched, requires every reported │
│ image to be allowlisted, then stamps │
│ the sandbox ID onto the leaf │
└──────────────────────────────────────────┘The 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. Inside a Kata guest it is the guest's loopback127.0.0.1:8401, where the one-pod- per-guest boundary leaves nothing to disambiguate. The webhook injects--workload-claimsinto thec8s-certsidecar (plus--workload-claims-guestunder Kata); both addresses are compiled into get-cert, so that flag selects the shape, never an address, and a wrong setting fails closed against a port nothing serves. - 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 / policy-monitor 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. Under Kata each guest holds one pod and
the token's host selects which guest the CDS asks, which is tighter. 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 — it is committed to REPORTDATA only on the /attest-key and
/handoff paths, not by the serving certificate. Cross-check it 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.
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 EAR and that ephemeral public key. - The active CDS verifies the EAR against its own JWKS, checks the recipient's
launch_digestagainst--handoff-measurements, and requires the recipient's REPORTDATA-boundoperator_keys_hashto equal its own — so the CA only ever leaves for another CDS whose exact launch measurement you have pinned and whose allowlist-write policy matches. - It returns the payload — the CA cert and private key, plus the current allowlist snapshot (digest floor and workload entries) — AES-256-GCM-encrypted to the recipient's X25519 key (X25519-ECDH → HKDF-SHA256 → AES-256-GCM). 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 and the same runtime policy state, 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.
Handoff is opt-in (cds.handoff.enabled=true, which passes cds.measurements through as
--handoff-measurements) and requires --operator-keys, since the operator policy is what
both sides attest. With no measurements listed, POST /handoff is not mounted at all. 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 (default 1h) — 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, minted for the platform named by --ratls-platform (default sev-snp). 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.
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 tls-lb 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 load balancer, and workloads by hand or in CI.