Start a worker
Connect a measured worker to CDS and your external Kubernetes control plane, then check node readiness with your normal kubeconfig.
Boot a worker with the worker.yaml admitted during Start policy enforcement.
The worker runs kubelet and containerd inside its confidential VM. Your existing
Kubernetes API, scheduler, and controllers run outside it.
Before you start
Continue in the deployment directory and shell from Prepare launch inputs. You need:
- A healthy CDS quorum.
worker.yaml,worker-image.json, andartifacts/worker/.- The component digest variables,
admin.kubeconfig, and the private bootstrap token.
Keep the approved worker.yaml bytes unchanged.
The commands below use a Linux systemd control-plane host, sudo, Docker for
extracting the Cilium operator binary, and kubectl compatible with your API
server.
Provision each API-server host with:
- The matching
bin/c8sand the two controller kubeconfigs generated below. - A deployment directory that persists across reboot, with no spaces in its absolute path.
- The recorded
OPERATOR_DIGESTandMESH_DIGESTvalues.
Keep operator.key on the trusted operator machine. Protect controller
kubeconfigs as credentials.
Use your ordinary administrator kubeconfig on the operator machine:
export KUBECONFIG="$PWD/admin.kubeconfig"
kubectl get --raw=/readyzExpect ok. Check the external API's bootstrap-token authenticator,
kubelet CSR signer/approver, certificate rotation, and pod-CIDR allocation
before continuing. Workers supply Cilium, kube-proxy, and CoreDNS; the control
plane must have the bundled agents disabled as described in preparation.
Install the cluster objects
Render with the same component digests and platform as the worker image:
c8s worker control-plane render \
--document worker.yaml --webhook-host 127.0.0.1 \
--hardware-platform tdx --kube-version v1.36.4 \
--image-digest "$OPERATOR_DIGEST" \
--armtls-mesh-image-digest "$MESH_DIGEST" \
--output-dir control-plane
kubectl apply -f control-plane/cluster.yaml
c8s worker control-plane credentials \
--kubeconfig admin.kubeconfig --output-dir controllersUse sev-snp for an AMD worker. The render installs cluster RBAC, the workload
CRD, webhook configurations, router, and DNS Service. The credential command
creates and approves its own certificate requests using your admin credentials.
It writes controllers/c8s-operator.kubeconfig and
controllers/c8s-cilium-operator.kubeconfig with mode 0600.
Supervise the external controllers
Run both controllers beside each API-server replica. With the loopback
webhook, the c8s operator must share that API server's network namespace:
127.0.0.1:9443 must reach its webhook from the API-server process. For a
containerized API server, arrange that namespace explicitly in your substrate.
A host-loopback listener alone does not establish this connectivity.
On each control-plane host, obtain the matching Cilium operator without running its image. Resolve and retain the digest before extracting the binary:
CILIUM_DIGEST=$(oras resolve quay.io/cilium/operator-generic:v1.19.6)
printf '%s\n' "$CILIUM_DIGEST" > cilium-operator-digest.txt
docker create --name c8s-cilium-extract \
"quay.io/cilium/operator-generic@$CILIUM_DIGEST"
docker cp c8s-cilium-extract:/usr/bin/cilium-operator-generic bin/cilium-operator-generic
docker rm c8s-cilium-extract
chmod 0755 bin/cilium-operator-genericApprove that image under your image acquisition process. The version matches Cilium baked into this worker.
Save the following two unit files in the deployment directory on each host. Before installing them, replace every placeholder with that host's recorded value:
<DEPLOYMENT_DIRECTORY>: the persistent, absolute, space-free deployment path, equal to that host's deployment-directorypwdoutput.<OPERATOR_DIGEST>: the completeOPERATOR_DIGESTvalue from preparation, includingsha256:.<MESH_DIGEST>: the completeMESH_DIGESTvalue from preparation, includingsha256:.
Keep the matching binaries and protected controller kubeconfigs at those paths. The units contain the substituted values literally; systemd does not expand shell variables in these files.
Save c8s-operator.service:
[Unit]
Description=c8s external operator
After=network-online.target
Wants=network-online.target
[Service]
WorkingDirectory=<DEPLOYMENT_DIRECTORY>
ExecStart=<DEPLOYMENT_DIRECTORY>/bin/c8s operator --kubeconfig <DEPLOYMENT_DIRECTORY>/controllers/c8s-operator.kubeconfig --webhook-host 127.0.0.1 --get-cert-image ghcr.io/confidential-dot-ai/c8s-operator@<OPERATOR_DIGEST> --mesh-image ghcr.io/confidential-dot-ai/armtls-mesh@<MESH_DIGEST>
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetSave c8s-cilium-operator.service:
[Unit]
Description=Cilium external operator
After=network-online.target
Wants=network-online.target
[Service]
WorkingDirectory=<DEPLOYMENT_DIRECTORY>
ExecStart=<DEPLOYMENT_DIRECTORY>/bin/cilium-operator-generic --k8s-kubeconfig-path <DEPLOYMENT_DIRECTORY>/controllers/c8s-cilium-operator.kubeconfig --k8s-namespace kube-system --ipam kubernetes --identity-allocation-mode crd --enable-l7-proxy=false
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetAfter making the substitutions in both files, install and start the services:
sudo install -m 0644 c8s-operator.service c8s-cilium-operator.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now c8s-operator c8s-cilium-operator
sudo systemctl is-active c8s-operator c8s-cilium-operatorExpect both services active.
- The Cilium operator creates its CRDs; its leader lease lives in
kube-system. - The c8s operator serves and configures the admission webhook.
Inspect controller startup failures with journalctl -u on the control-plane
host.
Inspect each controller certificate's actual expiry:
for config in controllers/*.kubeconfig; do
printf '%s: ' "$config"
kubectl config view --raw --kubeconfig "$config" \
-o jsonpath='{.users[0].user.client-certificate-data}' | \
base64 -d | openssl x509 -noout -enddate
doneController credentials request 30 days of validity, capped by the cluster signer's lifetime. Record each issued certificate's actual expiry.
Renew controller credentials before that expiry:
- Rerun
worker control-plane credentialswith the administrator kubeconfig. - Securely replace each host's files.
- Restart both services.
These controller kubeconfigs require operator-managed renewal. The worker's kubelet handles its own client-certificate rotation.
Return to the original trusted operator machine, deployment directory and preparation shell before continuing.
Prepare the worker launch disk
Create an ISO containing the approved document and raw bootstrap token. The
prompt accepts only the token after any k3s/RKE2 K10<hash>:: prefix:
umask 077
mkdir worker-opkeydata
cp worker.yaml worker-opkeydata/pubkey
read -r -s -p 'Kubernetes bootstrap token: ' WORKER_BOOTSTRAP_TOKEN
printf '\n'
[[ "$WORKER_BOOTSTRAP_TOKEN" =~ ^[a-z0-9]{6}\.[a-z0-9]{16}$ ]]
printf '%s' "$WORKER_BOOTSTRAP_TOKEN" > worker-opkeydata/bootstrap-token
unset WORKER_BOOTSTRAP_TOKEN
cmp worker.yaml worker-opkeydata/pubkey
genisoimage -quiet -V opkeydata -J -r \
-o worker-opkeydata.iso worker-opkeydata
sha256sum worker-opkeydata/pubkey > worker-launch-sha256.txtProtect the ISO and staging directory: the bootstrap token is plaintext and outside the measured document. The worker verifies the launch document, then uses the token to request a kubelet certificate from the authenticated API. Apply your control plane's bootstrap-token expiry and rotation procedure.
Launch and check the worker
Use the TEE launcher prepared for your substrate with these inputs:
- The approved boot disk and matching firmware/IGVM files under
artifacts/worker/; choose the manifest's matching TDX or SEV-SNP variant and vCPU count. worker-opkeydata.isoattached read-only, with ISO labelopkeydataand both files at its root.- A dedicated scratch disk of at least 64 GiB, attached as virtio-blk with serial
confai-scratch. The measured guest initializes its encryption; runtime state disappears on reboot. - For SEV-SNP,
HOST_DATAequal to SHA-256 of the exactworker-opkeydata/pubkeybytes, recorded inworker-launch-sha256.txt. TDX binds those bytes through the measured initrd's RTMR[3]. - An exclusive primary IPv4 inside
NODE_CIDR. Stop or fence any old VM before reusing its address.
Allow these network paths:
- Workers to the external API, CDS TCP
8443, registries, DNS, and CPU endorsement-certificate services. - UDP
8473between workers for Cilium VXLAN. - CDS to worker TCP
1019for admission inventory. - Control plane to authenticated kubelet TCP
10250. - Client TCP
443to workers hosting the router. - CDS peer TCP
8444between CDS replicas.
Keep the CDS client path as TLS passthrough.
The substrate launches the VM; c8s supplies measured guest artifacts and launch documents. Its boot gates verify the document, scratch encryption, runtime restrictions, and GPU CC readiness when GPUs are present before starting pods.
Set WORKER_IP to the primary IPv4 assigned by the launcher:
WORKER_IP='<WORKER_PRIMARY_IPV4>'
WORKER_NODE="n-${WORKER_IP//./-}"
kubectl wait --for=create "node/$WORKER_NODE" --timeout=5m
kubectl wait --for=condition=Ready "node/$WORKER_NODE" --timeout=5m
kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl -n c8s-router rollout status deployment/c8s-router --timeout=5mRequire:
- The node is
Ready. - Its InternalIP equals
WORKER_IP. - The router is ready.
If registration or readiness times out, check kubectl get csr and
control-plane events. Check DNS, routing, bootstrap credentials, signer
availability, and VM boot-gate failures before proceeding.
The measured kubelet disables exec, attach, port-forward, debug, and logs.
Use object status/events and application diagnostics. Node readiness is a
Kubernetes operating signal; client attestation and policy verification establish
what handles private data. Continue to Run a workload.