Install c8s
Install c8s onto an existing Kubernetes cluster running on confidential hardware (AMD SEV-SNP or Intel TDX) — prerequisites, the single-node quickstart, the flags that select the deployment shape, 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. Launched a node CVM yourself? Its kubeconfig comes from Fetch a kubeconfig from a running CVM — run that first. 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.
- Node kernels new enough for the TEE: Linux ≥ 6.11 for SEV-SNP, ≥ 6.16 for TDX.
kubectlconfigured for the target cluster. On a node CVM you launched yourself, that kubeconfig is the output ofc8s get-kubeconfig.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.
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, both are
required, 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.
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. Multi-node clusters use the dedicated CDS node selector.
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.pubThe same keypair does double duty on a node CVM you launch yourself: operator.pub also goes
into the CVM's launch identity, and operator.key is what
fetches its kubeconfig.
Installing 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 Node-as-CVM shape
Here the nodes are confidential VMs and pods run as ordinary processes. Both --cvm-mode and
--hardware-platform are required — there is no CLI default for either, so state the shape
and the CPU TEE explicitly:
c8s install --single-node --cvm-mode=bare-metal --hardware-platform=sev-snp \
--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.
On an Intel TDX host pass --hardware-platform=tdx instead.
bare-metal is the self-managed 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 --hardware-platform=sev-snp \
--operator-keys operator.pub \
--measurements <M>On a stock Azure image <M> comes off a live report instead of a manifest.json — see
Node CVMs on stock Azure images.
On stock RKE2, router's host port 443 collides with rke2-ingress-nginx and the install fails
fast at preflight. Set router.hostPort.enabled: false in a values file and pass it with -f.
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
(the CNI may live outside c8s-system):
kubectl get pods -A -wThe wait is bounded: helm --wait runs with a 5-minute timeout. 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:
+ router 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://<router> --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. Required. See trust boundaries before you pick.--hardware-platform— the CPU TEE,sev-snportdx. Required, and 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 router 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 router host-port check, the --resolve-digests=false
operator-image existence check, the --operator-keys guard, and the other default-path preflights. Host distro autodetection is suppressed only if a values file
sets nriImagePolicy.distro.
One value the chart will not let you turn off: nriImagePolicy.enabled=false renders only under
--cvm-mode=bare-metal (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)
The TDX attestation path requires a node labeled intel-tdx.node.kubernetes.io/enabled=true.
Apply it only after the host exposes /dev/tdx_guest and qgsd is running with the
quote-generation bridge. The installer checks for a labeled node before proceeding.
kubectl label node <NODE> intel-tdx.node.kubernetes.io/enabled=true--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 and the operator 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.
It should carry the injected certificate containers:
# 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://<ROUTER_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.