Components & Data Flow
The c8s components, how they fit together, and the difference between the two trust boundaries — node-as-CVM and pod-as-CVM (Kata) — including the certificate and attestation flows.
c8s adds five capabilities to a Kubernetes cluster: TEE attestation verification, certificate management (RA-TLS + a mesh CA), RA-TLS mesh networking, container-image policy enforcement, and an attested public front door. This page describes the components that provide them and how requests flow between them.
The components
| Component | Image | Node-as-CVM (--cvm-mode=node) | Pod-as-CVM (--cvm-mode=pod) |
|---|---|---|---|
| CDS — Certificate Distribution Service | cds | runc container | its own Kata CVM |
| c8s operator + admission webhook | c8s-operator | runc container (untrusted host) | runc container (untrusted host, exempt) |
| attestation-api | attestation-api | baked into the node image, host loopback :8400 | baked into the guest image, 127.0.0.1:8400 |
| ratls-mesh | ratls-mesh | host DaemonSet (L4 proxy) | in-guest, baked into the guest image |
| nri-image-policy | nri-image-policy | baked into the node image (NRI plugin) | in-guest policy-monitor, baked into the guest image |
get-cert — the c8s-cert sidecar | c8s-operator | webhook-injected native sidecar | webhook-injected native sidecar |
get-secret — the c8s-secret sidecar | c8s-operator | webhook-injected native sidecar, on confidential.ai/c8s-secrets | rejected at admission |
get-volume — the c8s-volume sidecar | c8s-operator | webhook-injected native sidecar, on confidential.ai/c8s-volumes | rejected at admission |
| tls-lb — the public front door | c8s-operator + upstream nginx | Deployment: nginx, cds-attest, allowlist proxy | same, pinned to a Kata CVM |
| volumed — encrypted-volume node agent | volumed | node DaemonSet, off by default | node DaemonSet, off by default; no pod can request a volume here |
Every c8s image ships the same multi-mode c8s binary and differs only in its entrypoint
subcommand and base layer, so a role is a command, not a separate program — the chart runs
c8s-operator for the controller, the injected c8s-cert,
c8s-secret, and
c8s-volume sidecars, and the tls-lb sidecars
alike. Most images are distroless; ratls-mesh is debian-slim because it needs iptables,
and volumed is debian-slim because it needs cryptsetup/veritysetup. All are published
under ghcr.io/confidential-dot-ai/, alongside a standalone get-cert image for use outside
the chart. Two images come from elsewhere: attestation-api is built from the Rust
attestation-rs tree, and tls-lb's web server is upstream nginxinc/nginx-unprivileged.
Where those services live depends on the shape. Under --cvm-mode=node the confidential node
image (c8s-base, built and published by c8s CI on every merge to main) bakes the
attestation-api and a fail-closed nri-image-policy, so the chart installs neither as a
workload; ratls-mesh is not baked and stays a DaemonSet. Under --cvm-mode=pod the same
services are baked into the measured Kata guest image instead. On the managed shapes
(--cvm-mode=gke, --cvm-mode=aks) the node image is the cloud's, not ours, so the chart
installs attestation-api and nri-image-policy as ordinary host workloads — and there the
chart requires nri-image-policy: nriImagePolicy.enabled=false fails the render, because
it is the only image-admission gate that would be left running.
The CDS is the trust root. It performs attestation verification, mesh-CA signing, and optional CA handoff all in one process, so there is no internal RA-TLS hop to an external signer and the CA private key never leaves CDS memory.
The c8s operator and its admission webhook are untrusted. Even under pod-as-CVM (Kata)
they stay in a plain runc container on the host — outside the per-pod TEE boundary — and
they are exempt
from the workload enforcement they configure. They are control-plane orchestration, not
part of the trust boundary: a compromised operator can disrupt scheduling (e.g. refuse to
inject get-cert, which fails closed under the webhook's Fail policy) but it cannot
forge attestation, mint certificates, or read TEE memory — those happen inside the CDS, in
the TEE. The cluster's confidentiality never depends on trusting the operator.
The admission inventory
The image-policy component does double duty, and the second job is easy to miss. Whichever
enforcer admitted a pod's containers — nri-image-policy on the node, policy-monitor
inside the Kata guest — is also the admission inventory: the authority on which pod
sandbox a process belongs to, and on what is actually running in that sandbox.
It serves two disjoint surfaces. A local token surface mints an inventory-signed sandbox token for the calling process, where the caller is identified by kernel peer credentials (node-as-CVM) or by the guest boundary itself (pod-as-CVM, one pod per guest) — never by anything the caller sends. A separate network surface, on a privileged port reachable only from the node's own network namespace and over mutually-attested RA-TLS, answers CDS's queries for the inventory's signing key and for a sandbox's current image digests.
CDS uses both when issuing a leaf: it verifies the token, gates issuance on every running image being allowlisted, and stamps the sandbox ID into the certificate's signed area. That is what lets a relying party ask "which workload is behind this key?" rather than only "is this a genuine TEE?". The mesh CA signature — not hardware evidence — is what vouches for that ID; see Sandbox identity for the full treatment.
Node-as-CVM and Pod-as-CVM
The trust boundary is a deployment-time choice — see
Pod-as-CVM vs Node-as-CVM for the full tradeoff.
c8s install requires you to state it with --cvm-mode (pod, node, gke, or aks;
there is no default), and Kata is the mechanism c8s uses to give each pod its own CVM —
not the name of a mode:
- Node-as-CVM (
--cvm-mode=node, and the managedgke/aksvariants) — the c8s services run as ordinary node containers and the mesh and attestation operate at node level. The node itself is a confidential VM (an Azure AKS or GKE confidential node, or a bare-metal AMD SEV-SNP / Intel TDX host), so the whole node is the trust boundary and everything on it inherits it. One launch digest covers the lot, which is whyc8s install --measurements <M>can pin the whole mesh in a single step here. - Pod-as-CVM (
--cvm-mode=pod) — each workload pod runs in its own confidential VM, launched via Kata Containers. The security services (attestation-api,ratls-mesh,policy-monitor) are baked into the measured guest image, so they are covered by the launch measurement and the host — adversarial here, since it sits outside every pod CVM — cannot MITM them. Both AMD SEV-SNP (kata-qemu-snp) and Intel TDX (kata-qemu-tdx) are wired, selected per install by--hardware-platform; a cluster runs one CPU TEE, not both.
Pod-as-CVM enforcement is two cooperating pieces — both, or nothing:
- A mutating webhook injects a
runtimeClassNameinto workload pods that lack one — the confidential class (kata-qemu-snporkata-qemu-tdx, and their-nvidiavariants for GPU pods) for pods annotatedconfidential.ai/cw, orkata-qemuotherwise. - A ValidatingAdmissionPolicy (
c8s-kata-enforcement) rejects workload pods that request a non-KataruntimeClassName.
The webhook's failure policy is Fail: if the webhook is unreachable, pod creation is
rejected, so an unmutated runc pod can never slip through. Two further policies back it
up in both shapes — c8s-cw-label-integrity denies a confidential.ai/cw pod that has shed
its injected c8s-cert sidecar, and c8s-deny-host-namespaces denies tenant pods
hostNetwork, hostPID, hostIPC, and hostPath volumes, which is what keeps the
admission inventory's privileged port out of a tenant's reach.
Trust boundaries
Under node-as-CVM, the boundary is the node. The whole node — c8s services and workload pods alike — sits inside one TEE the host cannot read:
╔═ TEE BOUNDARY · the whole node is one CVM, host cannot read ═══════════════╗
║ ║
║ ┌──────────────── Kubernetes node = one confidential VM ─────────────┐ ║
║ │ attestation-api · nri-image-policy │ ║
║ │ (baked into the node image on --cvm-mode=node; │ ║
║ │ chart DaemonSets on gke / aks) │ ║
║ │ chart-installed: │ ║
║ │ CDS · ratls-mesh · operator + webhook · tls-lb │ ║
║ │ workload pods (runc), each with a get-cert sidecar │ ║
║ └────────────────────────────────────────────────────────────────────┘ ║
╚════════════════════════════════════════════════════════════════════════════╝
host side (adversarial): hypervisor · cloud or bare-metal operatorWhere those two components live depends on whether you control the node image. On
--cvm-mode=node you build and measure it yourself, so they are baked in and covered by the
node's launch digest. On the managed shapes (gke, aks) the provider owns the image and c8s
cannot add to it, so the chart runs them as DaemonSets inside the same CVM boundary instead.
One launch digest covers everything on the node, including the kubelet.
The corollary is that pods inside the node are separated only by the kernel. That is fine — node-as-CVM is a single-tenant shape and the adversary is outside the node — but see the threat model for what c8s does enforce between workloads on one node.
Under pod-as-CVM, the boundary is each pod's own CVM; the host is outside every one of them:
╔═ TEE BOUNDARY · per-pod confidential CVM, host cannot read ════════════════╗
║ ║
║ ┌──── kata-qemu-snp/tdx CVM ────┐ ┌──── kata-qemu-snp/tdx CVM ────┐ ║
║ │ CDS │ │ workload │ ║
║ │ RA-TLS serving cert │ │ + c8s-cert sidecar │ ║
║ │ (SNP / TDX evidence) │ │ (leaf cert from CDS) │ ║
║ │ baked in: attestation-api · │ │ baked in: attestation-api · │ ║
║ │ ratls-mesh · policy-monitor │ │ ratls-mesh · policy-monitor │ ║
║ └───────────────────────────────┘ └───────────────────────────────┘ ║
╚════════════════════════════════════════════════════════════════════════════╝
host side (adversarial): containerd · kata-runtime · kata-shim · operatorEvery pod carries its own launch digest, so each attests independently.
Certificate flow — node-as-CVM
A workload obtains a TEE-bound certificate from the CDS in one issuance. The nonce CDS mints for the challenge is reused as the sandbox token's nonce, so one single-use value binds both the hardware evidence and the identity claim — there is no clock and no replay window:
Workload CDS attestation-api admission
[c8s-cert] [same TCB] inventory
│ │ │ │
│ POST /authenticate │ │ │
│───────────────────►│ │ │
│ single-use nonce │ │ │
│◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ │
│ │ │ │
│ POST /sandbox — CSR public key + that nonce │
│───────────────────────────────────────────────────────────►│
│ inventory-signed sandbox token │ │
│◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │ │ │
│ report_data = SHA-384(CSR public key ‖ nonce) │
│ │ │ │
│ POST /attest — evidence + CSR + token │ │
│───────────────────►│ │ │
│ │ verify(evidence, report_data) │
│ │────────────────────►│ │
│ │ valid · launch_digest │
│ │◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
│ │ GET /identity · GET /digests/{sandbox}│
│ │──────────────────────────────────────►│
│ │ │ │
│ check measurement · every running image allowlisted │
│ validate CSR · sign with the mesh CA │ │
│ │ │ │
│ leaf cert + CA chain, sandbox ID in the signed area │
│◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ │get-cert never reports its pod's images — CDS reads them live from the component that admitted them.
Certificate flow — pod-as-CVM (Kata)
Under pod-as-CVM the same flow happens inside the pod's TEE boundary. The CDS mints its
own RA-TLS serving cert from its hardware evidence, the workload reaches it over RA-TLS, and
the attestation-api is the in-guest service at 127.0.0.1:8400. The inventory is
policy-monitor in the same guest, reached on guest loopback rather than a mounted socket,
so there is no caller to disambiguate — one pod per guest. The trust anchor is the launch
measurement: CDS, attestation-api, ratls-mesh, and policy-monitor are all baked into the
guest image and therefore covered by it.
╔═ SEV-SNP / TDX BOUNDARY · HOST CANNOT READ ══════════════════════════════╗
║ ║
║ Workload CVM CDS CVM in-guest services ║
║ [c8s-cert] [mesh CA] [127.0.0.1] ║
║ │ │ │ ║
║ │ RA-TLS connect, verify CDS measurement │ ║
║ │────────────────────►│ │ ║
║ │ │ │ ║
║ │ sandbox token, over guest loopback │ ║
║ │◄────────────────────────────────────────────► ║
║ │ │ │ ║
║ │ POST /attest — evidence + CSR + token │ ║
║ │────────────────────►│ │ ║
║ │ │ verify in-process │ ║
║ │ │──────────────────────►│ ║
║ │ │ verdict + launch_digest ║
║ │ │◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ ║
║ │ │ │ ║
║ │ leaf cert from the mesh CA │ ║
║ │◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ ║
╚══════════════════════════════════════════════════════════════════════════╝Nothing in this exchange crosses the host — the whole issuance is inside measured memory.
The public front door
External traffic enters through tls-lb, a Deployment whose pod holds nginx alongside
three c8s containers: a c8s-cert sidecar that keeps nginx's serving certificate fresh, and
the two below. Under Kata the whole pod is pinned to a confidential RuntimeClass, because it
terminates client TLS with mesh-issued keys and that plaintext must stay inside the TEE.
cds-attest(tlsLb.attest.enabled, on by default) serves the browser-facingc8s-verify/v1protocol on loopback: the per-session attestation challenge and the post-quantum over-encryption handshake. nginx reverse-proxies/.well-known/c8s/to it. Exposing it does not weaken the server — it publishes a signed quote of its own TEE and nothing else. Trust is established on the client, which must pin expected measurements.- The allowlist proxy (
tlsLb.allowlist.enabled, on by default) publishes CDS's complete/allowlistAPI through the front door. It verifies CDS's RA-TLS evidence before forwarding, so the public hop and the in-cluster hop are both attested. Reads stay unauthenticated at CDS and are rate-limited per client; writes still require the short-lived, body-bound operator token, and the operator private key never enters the cluster. See Allowlist.
The protocol endpoints serve wide-open CORS by default.
tlsLb.cors.protocolEndpoints defaults to true, which sends
Access-Control-Allow-Origin: * (with no credentials) on /.well-known/c8s/, the
discovery document, the certificate endpoints, and the built-in /allowlist route. Those
responses are self-authenticating — hardware evidence, CDS-signed certificates, sealed
tunnel records — and no request on them relies on ambient browser credentials, so an origin
allowlist would block third-party verifiers without protecting anything. Your workload
routes and the catch-all upstream are never covered by this flag; set tlsLb.cors.enabled
if you want an origin policy of your own, and it then governs every location.
Install & admission ordering
The install sequence is load-bearing, and the operator — not a Helm hook — closes the loop:
- Helm creates the
MutatingWebhookConfigurationalong with the rest of the release. ItscaBundleis empty at this point, so the API server cannot yet reach the webhook. - The operator starts, mints a long-lived webhook CA and a short-lived serving leaf, and
patches the
caBundleonto the webhook configuration. The patch is idempotent, and an in-process rotator re-mints the leaf under the stable CA so the bundle never needs re-patching. - Once the operator's cache is up it runs a one-shot reinject sweep: any pod annotated
confidential.ai/cwthat was admitted during the gap — and so never got itsc8s-certsidecar — is deleted so its controller recreates it through the webhook. Pods with no controller owner are logged and left running, because deleting them would destroy the workload with no recreate path. - On uninstall, where the chart installed the host NRI plugin, a pre-delete hook runs a privileged DaemonSet that removes it from every node's containerd configuration before the release goes away.
Next: the threat model makes the trust assumptions explicit and lists what is — and isn't — enforced today.