Node CVM launch contract
What the c8s node image expects from a hypervisor — published artifacts per platform, supported vCPU counts, disk serials and labels, the opkeydata and joindata disk contracts, network ports, and the manifest fields c8s pins.
The c8s node image is a measured Confidential Kubernetes node: a dm-verity root carrying a single-node RKE2 server, the baked admission inventory, and the attestation-api. It is published as an OCI artifact of raw files and launched by a hypervisor outside its trust boundary.
This page is the contract between that hypervisor and the image. The procedure for a QEMU host is Boot a node CVM on bare metal.
Published artifacts
Repository: ghcr.io/confidential-dot-ai/node-guest-base. Tags are platform-qualified:
rke2-snp and rke2-tdx are the raw-file artifacts, rke2-snp-cdi and rke2-tdx-cdi the
KubeVirt disk-image form of the same build. Every release tag names both the platform and the
version, as in rke2-tdx-v0.1.0, and each one is exact.
| File | AMD SEV-SNP | Intel TDX | Contents |
|---|---|---|---|
disk.raw | yes | yes | dm-verity root filesystem, mounted read-only |
uki.efi | yes | yes | unified kernel image (kernel, initrd, cmdline) |
combined-initrd.img | yes | yes | initrd as a standalone file |
OVMF.fd | yes | — | firmware measured into the IGVM |
OVMF.tdx.fd | — | yes | TDVF firmware, covered by MRTD |
guest-smp<N>.igvm | yes | — | one measured launch image per vCPU count |
dsdt.aml | yes | yes | ACPI differentiated system description table |
roothash | yes | yes | dm-verity root hash of disk.raw, as bare hex |
manifest.json | yes | yes | build inputs and the image's measured identity |
The two platforms establish identity differently. SEV-SNP measures firmware and kernel together as one IGVM launch digest. Intel TDX measures the TDVF firmware as MRTD and records the guest kernel and root filesystem in RTMR[1] and RTMR[2].
Supported vCPU counts
An SEV-SNP launch measurement covers initial vCPU state, so one image yields one launch digest per vCPU count and the build ships one measured IGVM for each.
-smp | IGVM file | Launch digest |
|---|---|---|
| 2 | guest-smp2.igvm | snp_variants[smp=2].measurement.snp_launch_digest |
| 4 | guest-smp4.igvm | snp_variants[smp=4].measurement.snp_launch_digest |
| 8 | guest-smp8.igvm | snp_variants[smp=8].measurement.snp_launch_digest |
| 16 | guest-smp16.igvm | snp_variants[smp=16].measurement.snp_launch_digest |
Each launch uses the IGVM matching its -smp value. An IGVM launched at another count produces
a report whose measurement is not the pinned one: the node boots normally and is then refused
credentials.
On Intel TDX the identity does not vary with vCPU count: the pinned tuple is mrtd, rtmr1,
and rtmr2. Guest RAM is in neither platform's launch identity; the build default is 16 GB.
Disks
| Purpose | How the guest finds it | Required | Absent |
|---|---|---|---|
| root | first virtio-blk device, attached read-only | yes | no boot |
| scratch overlay | device serial confai-scratch, at least 64 GB | yes | falls back to a 2 GB tmpfs; RKE2 wedges when it fills |
| containerd image cache | device serial confai-containerd, or filesystem label containerd | no | falls back to a 32 GB tmpfs |
| model weights | device serial confai-models | no | the mount is skipped |
| operator key | ISO9660 volume label opkeydata | for credential release | the credential-release unit is skipped |
| cluster join | ISO9660 volume label joindata | for multi-node | the node is a single-node server |
| encrypted volume | device serial c8s-vol-<name> | per volume | the volume cannot be opened |
Notes on discovery:
- A disk serial is set on the
-device. QEMU rejectsserial=on a raw-drivewithBlock format 'raw' does not support the option 'serial'. - The scratch and cache disks are handed over blank. The guest formats and encrypts each per boot with a key that never leaves the CVM, so nothing on them survives a relaunch.
- The weights disk is a pre-populated ext4 filesystem, mounted read-only at
/var/lib/modelswithnodev,nosuid,noexec. Its contents stay readable by the host, and integrity rests on the workload checking model digests. - Volume names are DNS-1123 labels of at most 12 characters, because the serial holds
c8s-vol-plus the name. See Create an encrypted volume. - SCSI transports surface a serial only under
/dev/disk/by-id/, virtio-blk only in sysfs. The guest scans both.
The opkeydata disk
An ISO9660 filesystem whose volume label is opkeydata, carrying one file:
| File | Contents |
|---|---|
pubkey | the operator's ECDSA public key, PKIX PEM, as written by openssl ec -pubout |
The bytes are hashed verbatim, armor and trailing newline included; any re-encoding changes the binding and verification fails. Rock Ridge extensions are required for the lowercase filename.
| Platform | Binding |
|---|---|
| AMD SEV-SNP | the launcher commits SHA-256(pubkey) as the report's HOSTDATA at launch |
| Intel TDX | the measured initrd extends SHA-384(pubkey) into RTMR[3] before switch_root |
The guest's credential-release unit requires both /dev/disk/by-label/opkeydata and the server
role. An unmet condition makes systemd skip the unit rather than fail it, so a node without the
disk boots and attests while nothing listens on 8443.
The joindata disk
An ISO9660 filesystem whose volume label is joindata, mounted read-only. Each field is one
file holding a single ASCII line of at most 256 bytes. Leading and trailing whitespace is
trimmed; interior whitespace, NUL bytes, extra lines, symlinks, and files outside the role's
list are rejected, and a rejected disk fails the role unit and every unit gated on it.
| File | Value | Server | Agent |
|---|---|---|---|
role | server or agent | required | required |
node-name | RFC1123 label | optional | optional |
node-ip | IPv4 | optional | optional |
node-external-ip | routable IPv4 | optional | optional |
server | server's IPv4, no scheme or port | rejected | required |
server-token | 64 lowercase hex characters | required | rejected |
agent-token | 64 lowercase hex characters | required | required |
server-tokenandagent-tokenmust differ.- An absent
node-namekeeps the image's baked hostname,c8s-node. Every node past the first needs its own. - An absent
node-ip, or the value0.0.0.0, leaves address selection to RKE2.0.0.0.0innode-external-ipis an error. - An agent dials
https://<server>:9345. - With no disk the node is a single-node server with a boot-local agent token.
Network ports
| Port | Direction | Service |
|---|---|---|
6443 | inbound | kube-apiserver |
8400 | inbound | attestation-api: /health, /attest |
8443 | inbound | credential release, server role only |
9345 | inbound | RKE2 supervisor, dialed by agents |
443 | inbound | tls-lb front door, after c8s install |
1019 | inbound | admission-inventory digests endpoint, dialed by the CDS |
| — | outbound | NTP for systemd-timesyncd, and the default route the inventory advertises |
The guest needs a default route: the admission inventory takes the address it advertises to the
CDS from it, and refuses to start with a loopback address. Clock drift trips the token check on
c8s get-kubeconfig, which is why outbound NTP matters.
Baked cluster settings
Set in the image and covered by its launch measurement. Runtime overrides go in
/etc/rancher/rke2/config.yaml.d/, and RKE2 fixes cluster and service CIDRs at first
initialization.
| Setting | Value |
|---|---|
| hostname | c8s-node |
| CNI | Cilium, with disable-kube-proxy: true |
| cluster CIDR | 10.52.0.0/16 |
| service CIDR | 10.53.0.0/16 |
| cluster DNS | 10.53.0.10 |
| apiserver SAN | c8s-cvm — the default for c8s get-kubeconfig --tls-server-name |
| disabled components | rke2-ingress-nginx, which leaves host port 443 to tls-lb |
| hardening profile | cis, with pod security admission and apiserver audit logging |
| kubelet | max-pods=200 |
| node label | confidential.ai/sev-snp=true or confidential.ai/tdx=true, from the build platform |
The platform label is what c8s install --hardware-platform selects on, and the node applies it
itself at boot.
Manifest fields c8s pins
manifest.json ships in the artifact and is the input to c8s get-kubeconfig --image-manifest
and c8s measurements derive. Its shape selects the platform.
| Field | Platform | Meaning |
|---|---|---|
version | both | manifest schema version; c8s reads version 3 |
build.platform | both | snp or tdx |
build.memory | both | guest RAM the build was validated at |
snp_variants[].smp | SEV-SNP | vCPU count of one measured variant |
snp_variants[].igvm.path | SEV-SNP | the IGVM file for that count |
snp_variants[].measurement.snp_launch_digest | SEV-SNP | SHA-384 launch digest for that count |
snp_variants[].measurement.algorithm | SEV-SNP | must be sha384 |
tdx.mrtd | Intel TDX | launch measurement of the TDVF firmware |
tdx.rtmr1, tdx.rtmr2 | Intel TDX | guest kernel and root filesystem |
tdx.firmware.path | Intel TDX | the TDVF file the tuple describes |
An SEV-SNP image pin is the snp_variants set of one build manifest, and a file without that
field is rejected. Duplicate smp entries fail the whole load, so a policy pins an image whole.
RTMR[3] is a deployment property — the operator key, extended by any measured workload images —
so it comes from c8s get-kubeconfig flags rather than from the manifest.