Deploy a confidential AKS cluster and run c8s

One sitting, start to finish — create a single-node AKS cluster on a confidential node pool, build the c8s CLI, install c8s in node-as-CVM mode, and verify the result.

This tutorial goes from an empty Azure subscription to an installed, verified confidential c8s cluster in a single pass, on a managed AKS cluster with a confidential node pool. It repeats the steps from the provisioning guide and the install guide so you can follow one page top to bottom — reach for those reference pages when you want a single piece (every install flag, the architecture rationale) on its own.

The cluster you build runs the node-as-CVM shape: the node is one confidential VM and your pods are ordinary containers inside it. That's the only shape Azure supports — see why at the end. If the node-as-CVM vs pod-as-CVM distinction is new, read the runtime model first.

You run everything from your laptopkubectl and c8s install talk to the managed cluster over az aks get-credentials. There's nothing to SSH into.

Prerequisites

On your laptop:

  • An Azure subscription, with the Azure CLI installed and logged in (az login).
  • kubectl (az aks install-cli if you don't have it).
  • Access to the c8s source repo and a GitHub Personal Access Token. The source repo and the component images are both private but need different scopes, so give the token both: repo (classic) or contents: read (fine-grained) to git clone the source, and read:packages to pull the images. A read:packages-only token can pull the images but cannot clone the private repo. Create one at GitHub → Settings → Developer settings → Personal access tokens.

What you'll build

  • A single-node AKS cluster with a Standard_DC*as_v5 confidential node pool — AMD SEV-SNP, with a vTPM.
  • c8s installed — operator, CDS, attestation-api, RA-TLS mesh, and image policy — and verified.

AKS confidential node pools use the AMD SEV-SNP DCas_v5 family; Standard_DC4as_v5 is a comfortable single-node dev size. Confidential VM sizes exist only in some regions (for example northeurope) — check az vm list-skus --location <region> --size Standard_DC first. The provisioning guide has the full SKU table.

Step by step

1. Create the AKS cluster with a confidential node pool

A DCas_v5 node size makes the pool a SEV-SNP confidential VM node pool — each node boots as a CVM with a vTPM. --node-count 1 keeps it single-node.

az group create --name c8s-rg --location northeurope

az aks create \
  --resource-group c8s-rg \
  --name c8s-aks \
  --node-count 1 \
  --node-vm-size Standard_DC4as_v5 \
  --os-sku Ubuntu \
  --generate-ssh-keys

Verify the node-pool shape.

If your subscription/region only allows the confidential size on a user pool, create a regular system pool and add the confidential pool with az aks nodepool add --mode User --node-vm-size Standard_DC4as_v5 --os-sku Ubuntu … — then drop --single-node in step 5 and let the CDS schedule onto the confidential pool. See the provisioning guide.

2. Point kubectl at the cluster

az aks get-credentials --resource-group c8s-rg --name c8s-aks

# wait until the node reports Ready
kubectl get nodes -o wide

get-credentials merges the cluster into your kubeconfig, so kubectl (and c8s install) talk to it directly from your laptop.

3. Install the c8s CLI

There's no prebuilt binary yet, so build the CLI from source on your laptop. It shells out to helm, kubectl, and crane.

Install the build tools (make, Go, Helm, crane)

On Linux:

# make + Go 1.26+ (the c8s go.mod floor)
sudo apt-get update && sudo apt-get install -y make
curl -sL https://go.dev/dl/go1.26.3.linux-amd64.tar.gz | sudo tar -C /usr/local -xz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc && export PATH=$PATH:/usr/local/go/bin

# Helm
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# crane (only needed with the default --resolve-digests=true)
CRANE_VER=$(curl -s https://api.github.com/repos/google/go-containerregistry/releases/latest | grep -oP '"tag_name": "\K[^"]+')
curl -sL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VER}/go-containerregistry_Linux_x86_64.tar.gz" \
  | sudo tar -xz -C /usr/local/bin crane

On macOS, brew install make go helm crane covers all four.

Clone the c8s repo — authenticate over HTTPS with that token (it needs the repo / contents: read scope, not read:packages), or with gh — and build the CLI. make install runs go install, which drops the binary in $(go env GOPATH)/bin (default ~/go/bin) — add that directory to your PATH:

git clone https://github.com/confidential-dot-ai/c8s.git
cd c8s
make install     # builds and installs `c8s` to $(go env GOPATH)/bin (default ~/go/bin)

export PATH=$PATH:$(go env GOPATH)/bin
c8s --help

4. Provide registry credentials

The c8s component images are currently private, so authenticate in two separate places — they are independent credentials and the install needs both:

  • Locally, so crane can resolve each image tag to a digest while c8s install runs (the default --resolve-digests=true shells out to crane on your laptop, reading ~/.docker/config.json). Skipping it produces crane digest ...: UNAUTHORIZED: authentication required.
  • In the cluster, via a pull Secret in the release namespace, so the kubelet can pull the images at runtime. Create it before installing, in c8s-system.

Both use the same read:packages token from the prerequisites:

GITHUB_USER=<your-github-username>
GITHUB_PAT=<github-token-with-read:packages>

# 1. Local login so `crane` can resolve digests during `c8s install`
echo "$GITHUB_PAT" | crane auth login ghcr.io -u "$GITHUB_USER" --password-stdin

# 2. Cluster-side pull Secret so the kubelet can pull the private images at runtime
kubectl create namespace c8s-system
kubectl create secret docker-registry ghcr-secret \
  --namespace c8s-system \
  --docker-server=ghcr.io \
  --docker-username="$GITHUB_USER" \
  --docker-password="$GITHUB_PAT"

This step is only needed while the c8s images are private. After c8s's public launch you can skip the Secret and drop --image-pull-secret — you'd only keep it for a private or custom registry.

5. Install c8s

On Azure, SEV-SNP attestation comes from the vTPM at /dev/tpm0, not a native /dev/sev-guest device. Select that path with --cvm-mode aks — despite the name it means "Azure vTPM". (--cvm-mode picks the deployment shape; the separate CPU-TEE selector --hardware-platform is ignored under aks, which is always vTPM-backed SEV-SNP.) Note there's no --kata: per-pod CVMs need nesting Azure doesn't provide, so this is a node-as-CVM install.

First mint the operator keypair whose public half the install pins into the CDS — the private key is what will authorize allowlist writes later, and without --operator-keys the installer stops and asks for --force:

openssl ecparam -name prime256v1 -genkey -noout -out operator.key
openssl ec -in operator.key -pubout -out operator.pub

Then install:

c8s install \
  --single-node \
  --cvm-mode aks \
  --image-pull-secret ghcr-secret \
  --operator-keys operator.pub

--single-node makes every node CDS-eligible (no dedicated CDS node).

Validate on managed nodes.

This install runs privileged DaemonSets that touch the node host — nri-image-policy patches containerd and restarts it, ratls-mesh sets up iptables, and attestation-api mounts /dev/tpm0. Confirm these complete on your AKS node pool (and survive node image upgrades) before relying on it.

helm --wait makes c8s install block until every component reports Ready, so the command can sit for a few minutes — that's expected, not a hang. To watch progress, in another terminal:

kubectl get pods -A -w

A brief ImageInspectError or a single restart on ratls-mesh early in the rollout is normal: nri-image-policy restarts containerd as it installs, and the mesh pod retries through that and self-heals. The wait is not unbounded, though — helm --wait runs with a 5-minute timeout, so if a pod stays stuck in ImagePullBackOff or CrashLoopBackOff (a missing or wrong pull secret is the usual culprit), c8s install fails once the timeout elapses. The resources it already applied stay in place, so fix the cause and re-run c8s install — it's idempotent (helm upgrade --install) and completes once every pod is Ready.

For production, pin launch measurements — see Obtaining launch measurements — and read the install guide for the rest of the flags.

6. Verify the install

Confirm the control plane came up. A node-as-CVM install runs six components — c8s-cds, c8s-operator, c8s-attestation-api, c8s-ratls-mesh, c8s-nri-image-policy, and c8s-tls-lb — and all should be Running with every container ready:

kubectl get pods -n c8s-system

Check the admission webhook is wired up — the operator patches its caBundle once at startup, so it should be non-empty:

# the config is named <release>-pod-injector (release defaults to c8s):
kubectl get mutatingwebhookconfiguration c8s-pod-injector \
  -o jsonpath='{.webhooks[*].clientConfig.caBundle}' | wc -c
# any non-zero count means it's patched

Then prove confidentiality from outside the cluster — the real test — with c8s-verify, which checks the node's attestation evidence and launch measurement against what you expect.

Why pod-as-CVM is not available on Azure

Azure's hypervisor does not expose nested virtualization. A pod-as-CVM design launches a new confidential VM inside the node for every pod — that requires nesting, which Azure does not provide. So each AKS confidential node is a single node-level CVM, and all pods on it share that one confidential boundary. Don't add --kata on Azure.

You still get strong guarantees: pod identities are issued by the CDS, the image allowlist is enforced at the node, inter-node traffic (once you scale past one node) is RA-TLS-protected, and secrets are released only to attested nodes. The tradeoff is attestation granularity — you can't distinguish two pods on the same node, because they're inside the same TEE. If your threat model needs per-pod isolation from the operator, you need a bare-metal-class confidential host that allows nesting; GKE (native /dev/sev-guest, --cvm-mode gke) is the other platform where per-pod CVMs are an option. See pod-as-CVM vs node-as-CVM for the full comparison.

Where to go next

Continue the tutorial: Verified chat over confidential vLLM — deploy a vLLM model server onto this AKS cluster, attest that it's running in a TEE, and chat with it over an encrypted channel from your laptop.

Or bring your serving stack of choice onto this cluster:

  • NVIDIA Dynamo — a multi-component serving graph (frontend, router, worker, discovery) run confidentially.
  • KServeInferenceService model serving in Standard mode, every image digest-allowlisted.

Or branch off: