Install c8s
Install c8s onto an existing Kubernetes cluster running on confidential hardware (AMD SEV-SNP or Intel TDX) — prerequisites, the single-node quickstart for both deployment shapes, the four flags that decide the trust model, and how to confirm the install came up.
This guide installs c8s onto an existing Kubernetes cluster already running on confidential hardware. No cluster yet? Start with Provision a confidential cluster on Azure, then come back here. Never done any of this before? The first confidential cluster tutorial walks the whole path in one sitting.
Prerequisites
- A Kubernetes cluster (k8s or RKE2) whose nodes run on confidential hardware — AMD SEV-SNP (Genoa / Milan / Turin) or Intel TDX. For the confidential pod runtime you additionally need a bare-metal-class host that allows nested VMs — see pod-as-CVM vs node-as-CVM.
- Node kernels new enough for the TEE: Linux ≥ 6.11 for SEV-SNP, ≥ 6.16 for TDX.
kubectlconfigured for the target cluster.helmonPATH(the chart is embedded in the binary and installed via Helm).craneonPATH— needed for the default--resolve-digests=true, which pins each component image to its registry digest.- The
c8sCLI, built from source withmake install(see below).
Building the c8s CLI (make, Go, GOPATH)
The source repo is public and the build needs Go 1.26+. make install builds for your host
platform — Linux and macOS both work:
git clone https://github.com/confidential-dot-ai/c8s
cd c8s
make installmake install runs go install, which drops the binary in $(go env GOBIN), or
$(go env GOPATH)/bin (default ~/go/bin) when GOBIN is unset — make sure that directory is
on your PATH. You'll also need make itself; it's not preinstalled on stock Azure images
(sudo apt install make on Debian/Ubuntu).
Pulling from a private mirror (crane login, --image-pull-secret)
The stock c8s component images are public, so the default install needs no registry credentials. You need both of the following only when you re-host the images on a registry that requires authentication, or when your own workload images do:
-
craneruns locally duringc8s installand authenticates from your~/.docker/config.json, so log in on the box first. Missing it surfaces ascrane digest ...: UNAUTHORIZED: authentication required.crane auth login <REGISTRY> -u <USER> --password-stdinAlternatively skip digest resolution with
--resolve-digests=falseand supply digests via-f. -
--image-pull-secret <name>is the separate cluster-side credential: the name of an existingkubernetes.io/dockerconfigjsonSecret in the release namespace, which the chart appends to every component'simagePullSecrets. The install never creates it, and fails fast if it is missing or has the wrong type.
Neither credential reaches a Kata guest: under --cvm-mode=pod a workload image's layers are
fetched inside the guest, and that pull is anonymous. A pod-as-CVM workload image must be
pullable without credentials.
Which TEEs?
The platform — CDS, attestation verification, the RA-TLS mesh, the allowlist,
and the node-as-CVM shape — runs on both AMD SEV-SNP and Intel TDX. --cvm-mode
(deployment shape) and --hardware-platform (CPU TEE) are orthogonal, and every
combination is accepted — including --cvm-mode=aks --hardware-platform=tdx, the Azure vTPM
TDX shape. A cluster picks one CPU TEE at install time (sev-snp by default; under
--cvm-mode=pod, tdx renders the kata-qemu-tdx RuntimeClass instead of kata-qemu-snp)
— there are no mixed-TEE clusters.
Quickstart: a single-node cluster
For a single-node cluster, pass --single-node. This clears the dedicated-CDS node selector and
taint toleration so every node is CDS-eligible — there is no separate role=cds node to
label or schedule. (It's about node count, not the number of pod CVMs: a multi-node cluster
running a single Kata pod still wants a dedicated CDS node, so leave it off there.)
Create the operator credential
Writes to the image allowlist are authorized by an operator EC key whose public half the install pins into the CDS. Generate the pair once; the private key stays with you (vault/HSM):
openssl ecparam -name prime256v1 -genkey -noout -out operator.key
openssl ec -in operator.key -pubout -out operator.pubInstalling without --operator-keys disables allowlist writes — nobody can allowlist a workload
image — so the installer refuses the default path unless you acknowledge with --force.
(Supplying any -f values file also clears the guard: those files own cds.operatorKeys.)
Install the default shape (node-as-CVM)
Start with the node-as-CVM shape to confirm the control plane is healthy. Here the nodes are
themselves confidential VMs and pods run as ordinary processes; there is no per-pod
confidentiality until you select the pod runtime with --cvm-mode=pod. --cvm-mode is
required — there is no CLI default, so state the shape explicitly:
c8s install --single-node --cvm-mode=node \
--operator-keys operator.pub \
--measurements <M><M> is the hex launch measurement of the node image you booted, published in its
manifest.json. It pins both internal trust boundaries in this one install
(cds.measurements and ratlsMesh.measurements), so the mesh is pinned from first boot.
Leave it off only for a throwaway cluster — see the pinning callout below.
node is the cloud-agnostic node-as-CVM shape, for confidential VMs that expose the TEE device
directly. Two managed platforms need a different device shape instead: --cvm-mode=gke for
GKE's managed confidential VMs, and --cvm-mode=aks for any Azure confidential VM, where the
report comes from the vTPM at /dev/tpm0 rather than a guest device:
c8s install --single-node --cvm-mode=aks --operator-keys operator.pub --measurements <M>Or install pod-as-CVM (--cvm-mode=pod)
To make each workload pod its own confidential VM, use --cvm-mode=pod. This installs the Kata runtime
stack, injects kata RuntimeClasses into workload pods, rejects non-kata pods, and moves the
security services into the guest image.
c8s install --single-node --cvm-mode=pod --operator-keys operator.pubOn Intel TDX hosts, add --hardware-platform=tdx (the default is sev-snp):
c8s install --single-node --cvm-mode=pod --hardware-platform=tdx --operator-keys operator.pub--measurements is rejected under --cvm-mode=pod: the peers CDS talks to here are per-pod
Kata guests measured against the kata-guest-base image, not the node image, so a node
measurement would reject every real peer. Pin the guest measurement through -f instead
(cds.measurements).
Wait for readiness
The installer passes helm --wait by default, so the command returns only once the release
is ready. Confirm the components are running:
kubectl get pods -n c8s-systemhelm --wait makes c8s install block until every component reports Ready, so the
command can sit there for a few minutes — that's expected, not a hang. To watch progress while
it waits, open a second shell on the box and tail the pods across all namespaces
(kata-deploy and the CNI live outside c8s-system):
kubectl get pods -A -wThe wait is bounded: helm --wait runs with a 5-minute timeout (10 minutes for a --cvm-mode=pod install). If a pod stays wedged in
ImagePullBackOff or CrashLoopBackOff past it (a missing or wrong image pull secret is the
usual culprit), c8s install fails when the timeout elapses rather than waiting forever. The
resources it already applied remain, so fix the cause and re-run c8s install — it's
idempotent (helm upgrade --install) and completes once every pod is Ready.
The last thing the installer prints is the pinning state it left behind. With --measurements
it confirms mesh pinned to --measurements; without it, read this and act on it before
production:
+ tls-lb attestation sidecar enabled, but the mesh is UNPINNED (accepts any
attested TEE). Pin it with the node image's launch measurement M (its
manifest.json): reinstall with --measurements <M>. Clients verify with the
same M: c8s verify https://<tls-lb> --measurements <M>.Choosing the install shape
Four flags decide the trust model and topology; the rest tune webhook-injected certificate defaults. Every flag, with its type and default, is in the CLI reference.
--cvm-mode— the deployment shape, and the one decision you cannot defer. See trust boundaries before you pick.--hardware-platform— the CPU TEE,sev-snportdx. Orthogonal to--cvm-mode.--measurements— what pins the mesh. Read the callout below before leaving it off.--operator-keys— who may write the image allowlist. Omitting it disables writes entirely.
Pin the mesh, or it accepts any attested peer.
cds.measurements is what the mesh, nri-image-policy, the operator, and tls-lb pin when they
dial CDS, and what CDS admits to /attest and /sign-csr; ratlsMesh.measurements is what
mesh peers pin on each other. With neither set, every one of those checks confirms the peer is
a genuine TEE and stops there — any attested CVM is admitted. --measurements <M> sets both on
the install itself, so there is no separate pinning step.
Obtain launch measurements explains
how to get <M>; the threat model
explains why it matters.
How -f values files interact with the flags
c8s install collects everything its flags imply into one computed values file and passes it to
Helm as the last -f. Precedence is therefore: chart defaults, then your -f files in the
order you list them, then the installer's computed values. A flag always wins on the keys it
sets; your files supply everything else.
Passing any -f also hands you ownership of the default-path checks, which the installer then
skips: the role=cds node-label preflight, the tls-lb host-port check, the --resolve-digests=false
operator-image existence check, the --operator-keys guard, and — under --cvm-mode=pod — the
automatic TEE node labeling. Host distro autodetection is suppressed only if a values file
actually sets kata.distro or nriImagePolicy.distro.
One value the chart will not let you turn off: nriImagePolicy.enabled=false renders only under
--cvm-mode=pod (the in-guest policy-monitor admits instead) or --cvm-mode=node (the node
image bakes the plugin). Anywhere else the host NRI plugin is the only image-admission gate, and
the render fails rather than leave confidential workloads with no allowlist enforcement.
TDX host prerequisites (--hardware-platform=tdx)
TDX RuntimeClasses and the TDX attestation path schedule on nodes labeled
confidential.ai/tdx=true. On the default --cvm-mode=pod path the installer applies that label
itself to every kata-targeted node, trusting --hardware-platform — and aborts if any node still
carries the other platform's label, so a platform switch has to be your explicit act. On a -f
install or a non-pod shape the label is yours to apply, and the install fails fast without it:
kubectl label node <NODE> confidential.ai/tdx=trueThe label is a scheduling aid, not a hardware probe. Apply it only once the host is actually TDX-ready:
- the
tdx_guestmodule is available, qgsd(Intel DCAP Quote Generation Service) is running, with asocatunix→vsock bridge so kata's quote-generation socket reaches it,- an Intel PCS API key is configured in
/etc/sgx_default_qcnl.conf(DCAP fetches TCB collateral during verification).
--cvm-mode=aks is exempt from all of this: the TD quote comes from the Azure vTPM, so there is
no TDX node label and no guest device.
Verify the install
A few quick checks confirm everything came up.
Components are healthy — CDS, the operator, and (under pod-as-CVM) kata-deploy should be
Running:
kubectl get pods -n c8s-systemThe admission webhook is wired up — the operator patches its caBundle once at startup, so
it should be non-empty:
kubectl get mutatingwebhookconfiguration | grep c8s
# caBundle populated? (any non-zero count means it's patched)
# the config is named <release>-pod-injector (release defaults to c8s):
kubectl get mutatingwebhookconfiguration c8s-pod-injector \
-o jsonpath='{.webhooks[*].clientConfig.caBundle}' | wc -cA workload got its confidential identity — inspect a pod you annotated confidential.ai/cw.
Under pod-as-CVM it should run under the confidential RuntimeClass for your CPU TEE and carry
both injected init containers:
# confidential RuntimeClass (pod-as-CVM) — describe shows it directly:
kubectl describe pod <pod> | grep "Runtime Class"
# Runtime Class Name: kata-qemu-snp (kata-qemu-tdx on TDX)
# the webhook-injected containers are present, in this order:
kubectl get pod <pod> -o jsonpath='{range .spec.initContainers[*]}{.name}{"\n"}{end}'
# c8s-cert the get-cert sidecar
# c8s-cert-wait blocks the workload until the first cert is writtenEnd-to-end confidentiality — verify it from outside the cluster with
c8s-verify. An external client pins the same <M> you
installed with:
c8s verify https://<TLS_LB_HOST> --measurements <M>Run with no --measurements, it prints the measurement it saw plus
WARNING: no --measurements pinned — any genuine TEE is accepted (UNSAFE for production). That
is how you read <M> off a booted cluster; it is never the production check.
Next steps
- Run a confidential workload — mark a Deployment for confidential treatment.
- Manage the allowlist — nothing starts until its digest is allowlisted.
- Verify a deployment — prove the cluster is what you think it is.
- Troubleshoot an install — when a component will not come up.
- Uninstall c8s — including the host-side kata sweep.