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 to c8s 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:

ClaimMeaning
ississuer (--ear-issuer, default cds)
iat / expissued-at and expiry. the lifetime follows --cert-ttl (default 24h)
submods.attesterthe attestation result, including ear_status, the trustworthiness vector, raw evidence, and the normalized launch_digest
tee_public_keybase64url 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 (default 720h) — how often a new P-256 key becomes active.
  • --token-signer-overlap (default 25h) — how long a retired key stays valid and published.
  • --token-signer-rotation-jitter (default 0.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:

  1. consumes the one-time challenge (decoded from base64; --challenge-ttl, default 60s);
  2. parses the CSR and verifies its self-signature;
  3. verifies the sandbox token, if one was sent — see Sandbox identity;
  4. computes the expected report_data = SHA-384(CSR_public_key ‖ challenge) (first 48 bytes);
  5. calls the attestation-api /verify with the evidence and that expected report data;
  6. requires signature_valid == true and report_data_match == true;
  7. enforces the measurement policy: under --measurements-config the evidence must match one pinned image whole — its launch_digest, then every register that image pins on TDX; under --measurements the launch_digest must be in the allowlist, and on TDX any --rtmrs pins must match as well;
  8. validates the CSR's DNS/IP SANs and CN against policy (--dns-san-pattern, --san-validation, --allowed-cn-pattern);
  9. asks the sandbox's inventory what it is running and requires every image to be allowlisted;
  10. 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:

OIDExtensionContents
1.3.6.1.4.1.66378.1.1RA-TLS attestationthe requester's own hardware evidence, copied verbatim from the CSR
1.3.6.1.4.1.66378.1.2audit digestSHA-256 of the evidence the CDS accepted at issuance
1.3.6.1.4.1.66378.1.4pod sandbox IDpresent 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 certificate

The requester never names its pod and never reports its images; both answers come from the component that made the admission decision.

  1. get-cert asks anonymously. It POSTs /sandbox carrying 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_PEERCRED plus SO_PEERPIDFD) and the inventory resolves them PID → cgroup → container → sandbox from its own admission record. The webhook injects --workload-claims into c8s-cert; the local inventory socket is compiled into get-cert and fails closed if unavailable.
  2. 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.
  3. get-cert forwards it opaquely in the /attest body as sandbox_token.
  4. 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 from GET /identity at <host>:1019 over 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.
  5. 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-ca is mandatory with --sandbox-id. Without it the verdict still reports the ID, but alongside a sandbox_id_note saying it is not verified — an unqualified ID never reads as attested.
  • In-mesh, a SandboxID pin 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