docs: adopt guest-extension-image naming in composable-vm proposal

Align the composable VM images proposal with the naming convention used
throughout the implementation: the generic mechanism and its data model
are now "guest extension image" (config key `guest_extension_images`,
struct `GuestExtensionImage`), and the runtime artifacts use the matching
"extension" vocabulary (`/run/kata-extensions/<name>`,
`kata.extension.<name>.verity_params`, `kata-extension-mount@.service`,
the `extension-<name>` virtio-blk serial / dm-verity target, and the
`kata-containers-coco-extension.img` image).

It also documents how template-unit instances are enabled: a systemd
generator instantiates kata-extension-mount@<name>.service for each
extension on the kernel command line, so the rootfs build stays
extension-agnostic.

This replaces the earlier mixed "extra_images"/"addon" terminology and
addresses the naming-consistency review feedback on the series.

Assisted-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Fabiano Fidêncio <ffidencio@nvidia.com>
This commit is contained in:
Fabiano Fidêncio
2026-06-27 13:55:21 +02:00
committed by Fabiano Fidêncio
parent e01a0b8975
commit f4d0ae1c2f

View File

@@ -10,18 +10,18 @@
This proposal introduces a **composable image architecture** for Kata Containers
guest VMs. Instead of building a single monolithic rootfs image that contains
every component a workload might need, the runtime assembles a VM from a lean
**base image** plus zero or more purpose-specific **addon images** that are
cold-plugged as additional virtio-blk devices. Each addon is an EROFS image
**base image** plus zero or more purpose-specific **guest extension images** that are
cold-plugged as additional virtio-blk devices. Each extension is an EROFS image
protected by dm-verity, mounted read-only inside the guest before the kata-agent
starts.
The first application of this architecture splits **Confidential Containers
(CoCo) guest components** out of the monolithic `kata-containers-confidential.img`
into a separate `kata-containers-coco-addon.img`.
into a separate `kata-containers-coco-extension.img`.
The current proposal targets **QEMU** as the hypervisor backend. The
design is intentionally hypervisor-agnostic at the configuration and guest
layers — the `extra_images` configuration, guest-side systemd units, and
layers — the `guest_extension_images` configuration, guest-side systemd units, and
agent path resolution work identically regardless of the hypervisor. Only the
host-side device attachment is hypervisor-specific (QEMU virtio-blk
cold-plug today), and extending support to other hypervisor backends
@@ -50,7 +50,7 @@ pause bundle). This creates several problems:
must maintain their own image build pipeline.
A composable approach addresses all four issues: the base image stays small and
generic, addon images are independently built and versioned, and the runtime
generic, guest extension images are independently built and versioned, and the runtime
composes them at VM creation time.
## Design
@@ -58,28 +58,30 @@ composes them at VM creation time.
### Architecture overview
```
Host Guest VM
┌──────────────────────┐ ┌─────────────────────────────────┐
│ Runtime config │ │ / (base rootfs, erofs)
│ ┌────────────────┐ │ │ kata-agent, systemd, ...
│ │ image = base boot
│ │ extra_images: │──────────>│ /run/kata-addons/coco/ (erofs)
│ │ - name: coco │ │ cold │ attestation-agent
│ │ path: ... plug confidential-data-hub
│ │ verity: ... │ │ api-server-rest
│ └────────────────┘ │ ocicrypt_config.json
pause_bundle/
│ QEMU
│ -drive base.img dm-verity protects both the
│ -drive coco.img base rootfs and the addon.
serial=addon-…└─────────────────────────────────┘
└──────────────────────
Host Guest VM
┌───────────────────────────────┐
│ Runtime config
│ ┌─────────────────────────┐
│ │ image = base ┌─────────────────────────────────────┐
│ │ guest_extension_images: │───────>│ / (base rootfs, erofs)
│ │ - name: coco │ │ bootkata-agent, systemd, ...
│ │ path: ... cold │
│ │ verity: ... │ plug │ /run/kata-extensions/coco/ (erofs)
│ └─────────────────────────┘ │ │ attestation-agent
confidential-data-hub
│ QEMU api-server-rest
│ -drive base.img │ ocicrypt_config.json
│ -drive coco.img │ pause_bundle/
│ serial=extension-coco │
└───────────────────────────────┘ │ dm-verity protects both the │
│ base rootfs and the extension. │
└─────────────────────────────────────┘
```
### Runtime configuration
A new `extra_images` field on the hypervisor configuration accepts an ordered
list of addon images:
A new `guest_extension_images` field on the hypervisor configuration accepts an ordered
list of guest extension images:
```toml
[hypervisor.qemu]
@@ -87,64 +89,78 @@ image = "/opt/kata/share/kata-containers/kata-containers.img"
# ...existing keys...
[[hypervisor.qemu.extra_images]]
[[hypervisor.qemu.guest_extension_images]]
name = "coco"
path = "/opt/kata/share/kata-containers/kata-containers-coco-addon.img"
path = "/opt/kata/share/kata-containers/kata-containers-coco-extension.img"
verity_params = "root_hash=abc...,salt=def...,data_blocks=1234,hash_block_size=4096,data_block_size=4096"
```
Each entry maps to a Rust struct:
```rust
pub struct ExtraImage {
pub struct GuestExtensionImage {
pub name: String,
pub path: String,
pub verity_params: String,
}
```
The `name` field is the primary identifier for an addon. It must be unique
The `name` field is the primary identifier for an extension. It must be unique
within the configuration and is used to:
- Set the **virtio-blk serial** to `addon-<name>`, enabling deterministic
- Set the **virtio-blk serial** to `extension-<name>`, enabling deterministic
device discovery in the guest via `/sys/block/*/serial`.
- Namespace the **kernel command-line** verity parameters as
`kata.addon.<name>.verity_params=...`.
- Name the guest **mount point** at `/run/kata-addons/<name>/`.
- Name the **dm-verity device mapper target** as `addon-<name>`.
`kata.extension.<name>.verity_params=...`.
- Name the guest **mount point** at `/run/kata-extensions/<name>/`.
- Name the **dm-verity device mapper target** as `extension-<name>`.
### Host-side: cold-plugging addon devices
### Host-side: cold-plugging extension devices
At VM creation time, for each entry in `extra_images`, the runtime:
At VM creation time, for each entry in `guest_extension_images`, the runtime:
1. Creates a virtio-blk device backed by the addon image file.
2. Sets the device serial to `addon-<name>`.
3. If `verity_params` is non-empty, appends
`kata.addon.<name>.verity_params=<value>` to the guest kernel command line.
1. Creates a virtio-blk device backed by the guest extension image file.
Extensions are **always** attached as virtio-blk, using the architecture's
virtio-blk transport (`virtio-blk-ccw` on s390x, `virtio-blk-pci`
elsewhere). Neither the VM rootfs driver (`vm_rootfs_driver`) nor the
generic block device driver (`block_device_driver`) is reused here: those
may resolve to a non-virtio-blk transport such as `virtio-pmem` (NVDIMM) or
`virtio-scsi`, and only virtio-blk devices carry the serial the guest relies
on for discovery (step 2), so a non-virtio-blk transport would leave the
extension undiscoverable and its mount unit would fail closed.
2. Sets the device serial to `extension-<name>`.
3. Appends `kata.extension.<name>.verity_params=<value>` to the guest kernel
command line for *every* configured extension. The value is empty for an
unmeasured extension (`verity_params = ""`, e.g. on s390x — see "Mount
script" below); the entry is emitted unconditionally because it doubles as
the guest-side activation signal for the mount unit. With an empty value the
entry renders as a bare `kata.extension.<name>.verity_params` (no `=`), which
the unit condition and generator both handle.
Both the Go runtime and the Rust runtime-rs implement this logic in their
respective QEMU hypervisor backends. The addon devices are **cold-plugged**
respective QEMU hypervisor backends. The extension devices are **cold-plugged**
they are present on the QEMU command line at VM start, not hot-plugged later.
Other hypervisor backends (Cloud Hypervisor, Firecracker) do not implement
addon cold-plugging yet, but the mechanism is straightforward to add: each
backend only needs to translate the `ExtraImage` entries into its native
extension cold-plugging yet, but the mechanism is straightforward to add: each
backend only needs to translate the `GuestExtensionImage` entries into its native
block device attachment API. The runtime configuration, guest-side mounting,
and agent path resolution are completely hypervisor-independent.
### Guest-side: mounting addons
### Guest-side: mounting extensions
A systemd template unit `kata-addon-mount@.service` handles addon discovery and
mounting. The unit is instantiated per addon name (e.g.
`kata-addon-mount@coco.service`).
A systemd template unit `kata-extension-mount@.service` handles extension discovery and
mounting. The unit is instantiated per extension name (e.g.
`kata-extension-mount@coco.service`); a systemd generator (described below)
creates those instances automatically from the kernel command line.
The mounting is performed by a systemd unit (before the agent starts) rather
than by the kata-agent itself for several reasons:
- **Chicken-and-egg problem** — in the CoCo addon case, the addon carries
- **Chicken-and-egg problem** — in the CoCo extension case, the extension carries
the very guest components (attestation-agent, confidential-data-hub) that
the kata-agent needs to launch. The agent cannot mount the addon because
it needs the addon's contents to be available before it starts.
the kata-agent needs to launch. The agent cannot mount the extension because
it needs the extension's contents to be available before it starts.
- **Init-system ordering guarantees** — systemd provides declarative
ordering (`Before=`, `After=`), conditional activation
@@ -160,7 +176,7 @@ than by the kata-agent itself for several reasons:
- **Non-systemd environments** — the design does not mandate systemd. In
environments using a different init system (or a dedicated orchestrator
like NVRC), the equivalent mounting logic can be implemented by whatever
entity manages early boot. The key requirement is that addon images are
entity manages early boot. The key requirement is that guest extension images are
mounted before the kata-agent starts — how that is achieved is an
init-system concern.
@@ -168,18 +184,18 @@ than by the kata-agent itself for several reasons:
```ini
[Unit]
Description=Mount Kata addon image %i
Description=Mount Kata guest extension image %i
DefaultDependencies=no
Before=kata-agent.service
After=local-fs-pre.target
ConditionKernelCommandLine=kata.addon.%i.verity_params
ConditionKernelCommandLine=kata.extension.%i.verity_params
OnFailure=poweroff.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/libexec/kata-addon-mount.sh %i
ExecStop=/usr/libexec/kata-addon-umount.sh %i
ExecStart=/usr/libexec/kata-extension-mount.sh %i
ExecStop=/usr/libexec/kata-extension-umount.sh %i
[Install]
WantedBy=kata-containers.target
@@ -188,28 +204,50 @@ WantedBy=kata-containers.target
Key design decisions:
- **`ConditionKernelCommandLine`** — the service only activates when the
runtime has actually configured the addon. This prevents the unit from
running (and failing) in non-addon VM configurations.
runtime has actually configured the extension. This prevents the unit from
running (and failing) in non-extension VM configurations.
- **`Before=kata-agent.service`** — guarantees the addon filesystem is mounted
- **`Before=kata-agent.service`** — guarantees the extension filesystem is mounted
before the agent attempts to use any component from it.
- **`OnFailure=poweroff.target`** — if the addon cannot be mounted (e.g.
- **`OnFailure=poweroff.target`** — if the extension cannot be mounted (e.g.
verity verification failure, missing device), the VM is shut down
immediately. A confidential VM must not continue with an unverified or
missing addon.
missing extension.
#### Enabling instances
> **Implementation note.** An earlier revision enabled a fixed
> `kata-extension-mount@coco.service` symlink at rootfs build time, which would
> have required editing the image build for every new extension. Review feedback
> pushed us to the generator below so the build stays extension-agnostic.
systemd template units must be enabled per instance, and the set of extensions
is only known at runtime (from the kernel command line). A systemd generator,
`kata-extension-mount-generator`, bridges that gap: it runs in early boot, scans
`/proc/cmdline` for every `kata.extension.<name>.verity_params` entry (whether it
carries a value or, for an unmeasured extension, is a bare key), and symlinks
`kata-extension-mount@<name>.service` into `kata-containers.target.wants`.
Because the runtime emits exactly one such cmdline entry per configured
`guest_extension_images`, the generator enables precisely the extensions the VM
was configured with. Adding a new extension therefore requires no change to the
rootfs build — it is wired up entirely from runtime configuration. The generator
ships in the base image (installed alongside the template unit by the agent), so
it is part of the dm-verity-measured rootfs, and the cmdline it reads is itself
covered by the guest launch measurement.
#### Mount script
The mount script (`kata-addon-mount.sh`) performs the following steps:
The mount script (`kata-extension-mount.sh`) performs the following steps:
1. **Device discovery** — scans `/sys/block/*/serial` for a device whose
serial matches `addon-<name>`. This is more reliable than waiting for udev
serial matches `extension-<name>`. This is more reliable than waiting for udev
to create `/dev/disk/by-id/` symlinks, since the minimal guest environment
may not run a full udev daemon.
2. **Verity parameter extraction** — reads
`kata.addon.<name>.verity_params=...` from `/proc/cmdline` and parses the
`kata.extension.<name>.verity_params=...` from `/proc/cmdline` and parses the
comma-separated key=value pairs (root_hash, salt, data_blocks,
hash_block_size, data_block_size).
@@ -219,24 +257,58 @@ The mount script (`kata-addon-mount.sh`) performs the following steps:
creation — all verity parameters are passed explicitly rather than stored
in an on-disk superblock.
4. **EROFS mount** — mounts the verified device (or the raw partition if
verity is not configured) read-only at `/run/kata-addons/<name>/`.
4. **EROFS mount** — mounts the resulting device read-only at
`/run/kata-extensions/<name>/` (the dm-verity target when verified, or the
data partition directly for an unmeasured extension).
##### Integrity policy: measured vs. unmeasured, and failing closed
An extension can legitimately ship *without* dm-verity — for example on s390x,
where IBM Secure Execution protects the guest through a different mechanism and
images are built with `MEASURED_ROOTFS=no`. The mount script must therefore
support a raw (unverified) mount, but it must **not** let that path become a
silent downgrade: an attacker who can edit the (host-supplied) kernel command
line could otherwise strip `verity_params` from a *measured* extension and have
it mounted unverified.
The script separates these two cases explicitly, using the **on-disk layout as
the source of truth** rather than trusting the cmdline alone. The image build
encodes its integrity policy in the partition table (see "Image build" below):
a measured extension carries a dm-verity **hash partition** (`p2`) next to the
data partition (`p1`), while an unmeasured extension has only `p1`. The script
detects the presence of the hash device and cross-checks it against the
`verity_params` carried on the kernel command line (which, in a confidential
guest, is itself part of the measured, attested boot):
| Hash device (`p2`) | `verity_params` on cmdline | Action |
|--------------------|----------------------------|----------------------------------------------------|
| present | present | **verify** with dm-verity, then mount (normal case)|
| present | absent / empty | **refuse** — verity was stripped/disabled (tamper) |
| absent | present | **refuse** — params but nothing to verify (mismatch)|
| absent | absent / empty | **raw mount** — genuinely unmeasured extension |
Any "refuse" path exits non-zero; combined with `OnFailure=poweroff.target` on
the mount unit, that powers the VM off rather than continuing with an unverified
or inconsistent extension. The defence has two layers: the cmdline (and thus the
root hash, or its deliberate absence) is covered by the guest launch
measurement and remote attestation, and the in-guest layout cross-check fails
closed so a measured extension can never be silently downgraded to a raw mount.
### Agent-side: data-driven component manifest
> **Implementation note.** An earlier revision of this proposal described the
> agent as resolving a small, hardcoded list of component paths. While
> implementing and testing the addon we found this too rigid: the addon
> implementing and testing the extension we found this too rigid: the extension
> evolved to ship multiple attestation-agent flavours, per-process environment
> requirements, and ordering constraints that the agent should not have to know
> about. The design below — a **data-driven manifest** owned by the addon — is
> about. The design below — a **data-driven manifest** owned by the extension — is
> what we converged on so that adding or reconfiguring a bundled component
> requires **no** kata-agent code change.
Each addon ships a manifest at `etc/kata-addons/components.toml`. When the addon
Each extension ships a manifest at `etc/kata-extensions/components.toml`. When the extension
is mounted, the kata-agent reads it and builds its launch plan from it. The
manifest declares the processes to launch and the resources they expose; all
paths are **relative to the addon mount root** (`/run/kata-addons/<name>`).
paths are **relative to the extension mount root** (`/run/kata-extensions/<name>`).
A process entry carries:
@@ -252,7 +324,7 @@ A process entry carries:
`${...}` tokens in `args`, `config`, `env` values, and variant fields are
substituted by the agent from a runtime context it assembles (socket and config
paths, the addon mount root `${addon_root}`, the selected `${attester_variant}`,
paths, the extension mount root `${extension_root}`, the selected `${attester_variant}`,
etc.). Introducing a brand-new variable is the only change that ever needs to
touch the agent.
@@ -280,14 +352,14 @@ select = "${attester_variant}"
[process.variants.nvidia]
path = "usr/local/bin/attestation-agent-nv"
env = { LD_LIBRARY_PATH = "${addon_root}/usr/local/lib:/run/kata-addons/gpu/usr/lib" }
env = { LD_LIBRARY_PATH = "${extension_root}/usr/local/lib:/run/kata-extensions/gpu/usr/lib" }
[[process]]
id = "confidential-data-hub"
level = 2
path = "usr/local/bin/confidential-data-hub"
config = "${cdh_config_path}"
env = { OCICRYPT_KEYPROVIDER_CONFIG = "${ocicrypt_config_path}", PATH = "${addon_root}/usr/sbin:/bin:/sbin:/usr/bin:/usr/sbin" }
env = { OCICRYPT_KEYPROVIDER_CONFIG = "${ocicrypt_config_path}", PATH = "${extension_root}/usr/sbin:/bin:/sbin:/usr/bin:/usr/sbin" }
wait_socket = "${cdh_socket}"
[[process]]
@@ -297,34 +369,34 @@ path = "usr/local/bin/api-server-rest"
args = ["--features", "${rest_api_features}"]
```
When **no** addon is mounted, the agent falls back to a built-in launch plan
When **no** extension is mounted, the agent falls back to a built-in launch plan
that reproduces the legacy behaviour (components launched from
`/usr/local/bin/...` in the rootfs). The same dual-path principle applies to
the non-process resources declared under `[paths]`: the agent resolves them
inside the addon first and falls back to the legacy location otherwise:
inside the extension first and falls back to the legacy location otherwise:
| Resource | Addon path | Legacy path |
| Resource | Extension path | Legacy path |
|------------------------|---------------------------------------------------------|---------------------------------|
| attestation-agent(-nv) | `/run/kata-addons/coco/usr/local/bin/attestation-agent[-nv]` | `/usr/local/bin/attestation-agent` |
| confidential-data-hub | `/run/kata-addons/coco/usr/local/bin/confidential-data-hub` | `/usr/local/bin/confidential-data-hub` |
| api-server-rest | `/run/kata-addons/coco/usr/local/bin/api-server-rest` | `/usr/local/bin/api-server-rest` |
| ocicrypt_config.json | `/run/kata-addons/coco/etc/ocicrypt_config.json` | `/etc/ocicrypt_config.json` |
| pause_bundle | `/run/kata-addons/coco/pause_bundle` | `/pause_bundle` |
| attestation-agent(-nv) | `/run/kata-extensions/coco/usr/local/bin/attestation-agent[-nv]` | `/usr/local/bin/attestation-agent` |
| confidential-data-hub | `/run/kata-extensions/coco/usr/local/bin/confidential-data-hub` | `/usr/local/bin/confidential-data-hub` |
| api-server-rest | `/run/kata-extensions/coco/usr/local/bin/api-server-rest` | `/usr/local/bin/api-server-rest` |
| ocicrypt_config.json | `/run/kata-extensions/coco/etc/ocicrypt_config.json` | `/etc/ocicrypt_config.json` |
| pause_bundle | `/run/kata-extensions/coco/pause_bundle` | `/pause_bundle` |
This approach:
- Preserves **backward compatibility** with existing monolithic rootfs images
where CoCo components are baked into the base image.
- Requires **no special rootfs modifications** — the base image does not need
stub files or directories for the addon components.
stub files or directories for the extension components.
- Works transparently on a **read-only rootfs** — no bind-mounting, no
remounting, no writes to the root filesystem.
- Keeps the agent **generic**addon-specific names, env, and ordering live
- Keeps the agent **generic**extension-specific names, env, and ordering live
in the manifest, not in agent code.
### Attester variant selection and the NVRC contract
The CoCo addon ships **two** attestation-agent builds: the stock
The CoCo extension ships **two** attestation-agent builds: the stock
`attestation-agent` and an NVIDIA-attester build, `attestation-agent-nv`, that
collects GPU evidence in addition to the TEE evidence. Which one runs is chosen
by the manifest's `select = "${attester_variant}"` selector, and the value of
@@ -333,7 +405,7 @@ by the manifest's `select = "${attester_variant}"` selector, and the value of
- On a plain confidential guest the kata-agent runs init itself and the variable
defaults to `default` → the stock attester launches.
- On a GPU guest, **NVRC** (the NVIDIA runtime config that owns early boot)
detects the GPU addon and exports `KATA_ATTESTER_VARIANT=nvidia` before
detects the GPU extension and exports `KATA_ATTESTER_VARIANT=nvidia` before
exec'ing the kata-agent. The agent forwards that into the substitution context
as `${attester_variant}`, so the `nvidia` variant launches.
@@ -343,28 +415,28 @@ consumed by the kata-agent, which keeps the agent free of any GPU- or
NVIDIA-specific knowledge — it only knows how to forward a selector into the
manifest. We arrived at this split after first trying to special-case the
attester inside the agent; pushing the decision out to NVRC + the manifest kept
both the agent and the addon generic.
both the agent and the extension generic.
#### Why one addon with two builds (and not two addons)
#### Why one extension with two builds (and not two extensions)
Shipping **two** attestation-agent builds inside a **single** CoCo addon is a
deliberate choice, and it is worth being explicit about it because addons are
Shipping **two** attestation-agent builds inside a **single** CoCo extension is a
deliberate choice, and it is worth being explicit about it because extensions are
otherwise meant to *eliminate* duplication.
- A single CoCo addon serves **both** plain confidential guests (TEE evidence
- A single CoCo extension serves **both** plain confidential guests (TEE evidence
only) and confidential GPU guests (TEE + GPU evidence). The only thing that
differs between them is which attestation-agent binary runs; everything else
in the addon (confidential-data-hub, api-server-rest, ocicrypt config, pause
in the extension (confidential-data-hub, api-server-rest, ocicrypt config, pause
bundle, `cryptsetup`) is shared verbatim. Splitting the NVIDIA attester into
its own addon would duplicate that shared payload and force every confidential
GPU guest to compose two CoCo-flavoured addons instead of one.
its own extension would duplicate that shared payload and force every confidential
GPU guest to compose two CoCo-flavoured extensions instead of one.
- The **cost** of keeping both builds together is precisely the manifest's
`select`/`variants` machinery: the manifest has to be aware that the
attestation-agent comes in two flavours and pick one at runtime. We consider
that a fair trade — the complexity is confined to data (the manifest), the
agent stays generic, and the addon stays a single, coherent "CoCo" unit.
- A separate addon only pays off when its contents are **substantially**
different (e.g. the GPU addon, which carries the driver userspace), not for
agent stays generic, and the extension stays a single, coherent "CoCo" unit.
- A separate extension only pays off when its contents are **substantially**
different (e.g. the GPU extension, which carries the driver userspace), not for
two near-identical builds of the same component. No further attester variants
are planned today, but if one appeared it would be another `[process.variants.<name>]`
entry — not a new image.
@@ -382,25 +454,25 @@ physically live.
turn:
- `dlopen`s `libnvidia-ml.so.1` (NVML) at runtime to gather GPU evidence. NVML
is part of the **GPU addon**, mounted at `/run/kata-addons/gpu` with its driver
is part of the **GPU extension**, mounted at `/run/kata-extensions/gpu` with its driver
libraries under `usr/lib`.
- pulls in a closure of non-glibc libraries (libxml2, zlib, lzma, the C++
runtime, ...) that the guest rootfs does not otherwise ship.
Neither set is present in a stock guest, so:
- The CoCo addon build bundles `libnvat.so` **and every non-glibc transitive
- The CoCo extension build bundles `libnvat.so` **and every non-glibc transitive
dependency** next to it under `usr/local/lib`.
- The `nvidia` **manifest variant** (the `[process.variants.nvidia]` entry, not
a separate image) sets an `LD_LIBRARY_PATH` that lists **both** the CoCo
addon's `usr/local/lib` (for `libnvat` and its closure) **and** the GPU
addon's `usr/lib` (for NVML). Setting only the first was the cause of an
extension's `usr/local/lib` (for `libnvat` and its closure) **and** the GPU
extension's `usr/lib` (for NVML). Setting only the first was the cause of an
`NVAT Error 500: NVML Initialization Failed` we hit during bring-up; the
RCAR handshake then never produced GPU evidence and attestation failed.
Because the agent applies manifest `env` on top of the inherited environment
(without clearing it), and because the `nvidia` variant only ever runs when the
GPU addon is present, referencing the GPU addon's well-known mount path here is
GPU extension is present, referencing the GPU extension's well-known mount path here is
safe.
#### CDH secure-mount tooling (encrypted vs plain storage)
@@ -415,17 +487,17 @@ in two different places:
unconditionally.
- **Encrypted storage** — `cryptsetup` LUKS-formats and opens the volume. This is
a CoCo-only capability, so it belongs with the CoCo guest components in the
**coco addon**.
**coco extension**.
This is the same `veritysetup`-vs-`cryptsetup` reasoning already applied to addon
mounting: the base must always carry `veritysetup` (it opens *every* addon as a
This is the same `veritysetup`-vs-`cryptsetup` reasoning already applied to extension
mounting: the base must always carry `veritysetup` (it opens *every* extension as a
dm-verity device before mounting), and `cryptsetup` shares an identical
shared-library closure, so wherever `veritysetup` lives the libraries for
`cryptsetup` are already present.
Because CDH runs in the **base rootfs namespace** but `cryptsetup` lives in the
addon (which is not on the default search path), the manifest sets CDH's `PATH`
to `${addon_root}/usr/sbin:/bin:/sbin:/usr/bin:/usr/sbin` — the addon's
extension (which is not on the default search path), the manifest sets CDH's `PATH`
to `${extension_root}/usr/sbin:/bin:/sbin:/usr/bin:/usr/sbin` — the extension's
`cryptsetup` first, then the base directories that carry `mke2fs`/`mkfs.ext4`/`dd`.
(The kata-agent launches components with `PATH=/bin:/sbin:/usr/bin:/usr/sbin`;
since setting any `env` value replaces that variable wholesale, the base
@@ -437,15 +509,15 @@ How each tool is provisioned depends on the **base flavour** (see
| Tool | Bucket | Full-distro base (Ubuntu) | Distroless base (chiseled NVIDIA + NVRC) |
|------------------------------|--------------------|----------------------------------------------------------|---------------------------------------------------------|
| `veritysetup` | base, always | `cryptsetup-bin` (unconditional in `ubuntu/config.sh`) | copied into the base layout unconditionally |
| `cryptsetup` | coco addon | also present in the full-distro base via `cryptsetup-bin`| binary bundled in the coco addon; libs come from base |
| `cryptsetup` | coco extension | also present in the full-distro base via `cryptsetup-bin`| binary bundled in the coco extension; libs come from base |
| `mke2fs`/`mkfs.ext4`/`dd` | base, for CoCo | `e2fsprogs` (on `CONFIDENTIAL_GUEST=yes`) + `coreutils` | copied into the base layout |
The distroless path needs explicit copying because nothing lands in a chiseled
image unless placed there deliberately, and the NVIDIA base is never built with
`CONFIDENTIAL_GUEST=yes`. The full-distro base is built with
`CONFIDENTIAL_GUEST=yes`, so the same tools arrive as ordinary packages. In both
cases the addon's `cryptsetup` resolves its libraries against the base, which
requires the base and the coco-addon builder to stay on the **same distro/ABI**
cases the extension's `cryptsetup` resolves its libraries against the base, which
requires the base and the coco-extension builder to stay on the **same distro/ABI**
(Ubuntu 24.04 "noble" today).
### Image build pipeline
@@ -454,16 +526,16 @@ requires the base and the coco-addon builder to stay on the **same distro/ABI**
Kata base images come in two flavours, distinguished by **who owns early boot**.
This distinction — not "Ubuntu vs NVIDIA" — is what drives the differences in
how tooling is provisioned and who mounts the addons:
how tooling is provisioned and who mounts the extensions:
- **Full-distro base** — ships a complete distribution with **systemd** as init.
systemd discovers and mounts the addons (via `kata-addon-mount@.service`), and
systemd discovers and mounts the extensions (via `kata-extension-mount@.service`), and
the binaries/libraries the guest needs arrive as ordinary distribution
packages. The standard confidential `kata-containers.img` (Ubuntu) is today's
instance.
- **Distroless base** — a minimal, chiseled image with no full init system. A
dedicated early-boot component takes over the responsibilities systemd would
otherwise have (addon discovery and mounting, attester selection,
otherwise have (extension discovery and mounting, attester selection,
orchestration), and any tooling must be placed into the image deliberately
rather than pulled in as packages. The chiseled `base-nvidia` driven by
**NVRC** is today's instance.
@@ -481,9 +553,9 @@ guest components (attestation-agent, confidential-data-hub, api-server-rest,
ocicrypt config, pause bundle). It includes:
- The kata-agent
- systemd and the `kata-addon-mount@.service` template unit
- systemd and the `kata-extension-mount@.service` template unit
- `cryptsetup-bin` (provides `veritysetup`) — required unconditionally so
that the base image can mount verity-protected addons regardless of whether
that the base image can mount verity-protected extensions regardless of whether
the base itself was built with `CONFIDENTIAL_GUEST=yes`. On Ubuntu this same
package also provides `cryptsetup`, so the full-distro base happens to carry
the encrypted-storage binary too.
@@ -491,7 +563,7 @@ ocicrypt config, pause bundle). It includes:
`dd`, and `/etc/mke2fs.conf`. On the standard base these come from
`e2fsprogs` (added when `CONFIDENTIAL_GUEST=yes`) and `coreutils`. See
"Runtime dependencies" for why the encrypted-storage `cryptsetup` lives in
the addon instead.
the extension instead.
The **distroless base** (`base-nvidia`, driven by NVRC) is a chiseled,
driver-agnostic image rather than a full distro, so the items above do not
@@ -499,9 +571,9 @@ arrive as packages — they are copied into the base layout explicitly:
`veritysetup` and its library closure unconditionally, and the
`mke2fs`/`mkfs.ext4`/`dd`/`mke2fs.conf` plain-storage tooling alongside it.
#### CoCo addon image
#### CoCo guest extension image
The `kata-containers-coco-addon.img` is built by:
The `kata-containers-coco-extension.img` is built by:
1. Unpacking the CoCo guest components tarball into a temporary rootfs. Besides
the agent-launched binaries (attestation-agent, attestation-agent-nv,
@@ -512,14 +584,14 @@ The `kata-containers-coco-addon.img` is built by:
- the NVIDIA attester libraries under `usr/local/lib``libnvat.so` plus its
non-glibc transitive closure (libxml2, zlib, lzma, the C++ runtime).
2. Unpacking the pause image tarball into the same rootfs.
3. Writing the component manifest to `etc/kata-addons/components.toml`.
3. Writing the component manifest to `etc/kata-extensions/components.toml`.
4. Running the image builder with:
- `FS_TYPE=erofs` — EROFS filesystem for compact, read-only storage.
- `MEASURED_ROOTFS=yes` — creates a dm-verity hash partition.
- `SKIP_DAX_HEADER=yes` — no DAX header (virtio-blk, not NVDIMM).
- `SKIP_ROOTFS_CHECK=yes` — the addon has no `/sbin/init`.
- `BUILD_VARIANT=coco-addon` — produces a correctly named root hash file
(`root_hash_coco-addon.txt`).
- `SKIP_ROOTFS_CHECK=yes` — the extension has no `/sbin/init`.
- `BUILD_VARIANT=coco-extension` — produces a correctly named root hash file
(`root_hash_coco-extension.txt`).
The resulting image is a two-partition disk:
@@ -531,11 +603,11 @@ into the runtime configuration templates.
### Security model
The addon architecture preserves the existing security guarantees of
The extension architecture preserves the existing security guarantees of
Confidential Containers:
- **dm-verity** provides integrity protection for the addon image, identical
to the existing protection on the base rootfs. Any tampering with the addon
- **dm-verity** provides integrity protection for the guest extension image, identical
to the existing protection on the base rootfs. Any tampering with the extension
contents is detected at mount time.
- **Verity parameters on the kernel command line** are measured by the TEE
@@ -547,7 +619,7 @@ Confidential Containers:
- **Mount failure = VM shutdown** — `OnFailure=poweroff.target` ensures the
VM does not proceed with missing or tampered components.
- **Read-only mounts** — both the base rootfs and addon images are mounted
- **Read-only mounts** — both the base rootfs and guest extension images are mounted
read-only (EROFS), preventing runtime modification.
## Alternatives considered
@@ -573,7 +645,7 @@ custom mounting approach described here:
- **Block device bridging** — sysext discovers images from well-known
directories (`/var/lib/extensions/`, `/run/extensions/`), not from raw
block devices. A bridging step would still be needed to discover the
virtio-blk addon device, verify it with dm-verity, and place or symlink it
virtio-blk extension device, verify it with dm-verity, and place or symlink it
where sysext expects to find it — eliminating much of the simplification
sysext would offer.
@@ -585,7 +657,7 @@ custom mounting approach described here:
- **Version matching** — sysext enforces `extension-release.d` metadata
matching against the host `os-release`. While this is useful for general
purpose systems, it adds friction in the Kata context where the base image
and addon images are built and versioned together in a controlled pipeline.
and guest extension images are built and versioned together in a controlled pipeline.
- **Minimal guest environment** — the Kata guest rootfs is a minimal
environment that may not ship a full systemd with sysext/confext support
@@ -616,7 +688,7 @@ proposal:
at VM creation time, without modifying the base image itself.
The two approaches are orthogonal. A Kata base image could be built and
managed using bootc/ostree, and addon images would still be cold-plugged
managed using bootc/ostree, and guest extension images would still be cold-plugged
and mounted at VM boot using the mechanism described here. bootc does not
provide a mechanism for dynamically composing additional block devices into
a running or booting system — it operates at the image build and deployment
@@ -624,28 +696,28 @@ layer, not at the VM assembly layer.
## Future work
### Additional addon types
### Additional extension types
The architecture is designed to support multiple addon images:
The architecture is designed to support multiple guest extension images:
- **GPU addon** — the NVIDIA GPU userspace (driver libraries, NVML, the
container-toolkit binaries, kernel modules) lives in a `gpu-addon` image
mounted at `/run/kata-addons/gpu`, carved out of the same build as the
- **GPU extension** — the NVIDIA GPU userspace (driver libraries, NVML, the
container-toolkit binaries, kernel modules) lives in a `gpu-extension` image
mounted at `/run/kata-extensions/gpu`, carved out of the same build as the
driver-agnostic `base-nvidia` image. NVRC orchestrates early boot, loads the
modules from the addon, and composes the GPU addon with the CoCo addon on
confidential GPU guests. This addon is implemented; its interplay with the
CoCo addon (NVML resolution, attester selection) is covered in
modules from the extension, and composes the GPU extension with the CoCo extension on
confidential GPU guests. This extension is implemented; its interplay with the
CoCo extension (NVML resolution, attester selection) is covered in
"Runtime dependencies" and "Attester variant selection" above.
- **Custom addons** — users can build their own addon images for
- **Custom extensions** — users can build their own guest extension images for
workload-specific libraries, models, or configurations.
### Addon ordering
### Extension ordering
When multiple addons are configured, they are cold-plugged in the order they
appear in the `extra_images` list. The current design does not enforce
explicit ordering dependencies between addons. If future use cases require
addon-to-addon dependencies, the systemd units can be extended with
When multiple extensions are configured, they are cold-plugged in the order they
appear in the `guest_extension_images` list. The current design does not enforce
explicit ordering dependencies between extensions. If future use cases require
extension-to-extension dependencies, the systemd units can be extended with
appropriate `After=`/`Requires=` relationships.
### Other hypervisor backends
@@ -653,58 +725,58 @@ appropriate `After=`/`Requires=` relationships.
The current proposal covers QEMU only. Extending to other backends
requires implementing block device cold-plug for each:
- **Cloud Hypervisor** — add `--disk` entries with the addon image path and
- **Cloud Hypervisor** — add `--disk` entries with the guest extension image path and
serial. Cloud Hypervisor natively supports virtio-blk serial numbers.
- **Dragonball** — attach additional virtio-blk devices through the
Dragonball VMM's block device configuration, mapping each `ExtraImage`
Dragonball VMM's block device configuration, mapping each `GuestExtensionImage`
to a drive with the corresponding serial.
- **Firecracker** — add block device entries via the Firecracker API with
the appropriate drive ID. Serial-based discovery may need adaptation since
Firecracker exposes drive IDs differently.
No changes are needed in the guest-side mounting logic or the agent — the
addon device discovery via `/sys/block/*/serial` and the systemd units work
extension device discovery via `/sys/block/*/serial` and the systemd units work
the same way regardless of which hypervisor attached the block device.
### Manifest-driven addon discovery via init-data
### Manifest-driven extension discovery via init-data
The current design passes verity parameters for each addon on the kernel
command line. This works well for a small number of addons but does not
scale: each additional addon adds a long `kata.addon.<name>.verity_params=...`
The current design passes verity parameters for each extension on the kernel
command line. This works well for a small number of extensions but does not
scale: each additional extension adds a long `kata.extension.<name>.verity_params=...`
entry, and the kernel command line has practical size limits.
A future evolution could introduce a **two-phase bootstrap**:
1. **Phase 1 (kernel-params driven)** — the kernel command line carries
verity parameters for a single, small **manifest addon** image. This
verity parameters for a single, small **manifest extension** image. This
image is mounted first using the existing mechanism.
2. **Phase 2 (manifest driven)** — the manifest addon contains a
configuration file (e.g. `addons.conf`) listing all other addons with
2. **Phase 2 (manifest driven)** — the manifest extension contains a
configuration file (e.g. `extensions.conf`) listing all other extensions with
their names, verity parameters, and any additional metadata. The mount
script reads this file and mounts the remaining addons accordingly.
script reads this file and mounts the remaining extensions accordingly.
This approach has several advantages:
- **Kernel command line stays fixed-size** regardless of how many addons are
- **Kernel command line stays fixed-size** regardless of how many extensions are
composed.
- **Attestation is simplified** — the TEE firmware measures one manifest
hash on the command line; the verifier only needs to validate that single
hash. The chain of trust extends from the measured kernel command line to
the verified manifest to the verified addons.
the verified manifest to the verified extensions.
- **Richer metadata** — the manifest can carry structured information beyond
verity parameters: addon ordering constraints, version requirements,
verity parameters: extension ordering constraints, version requirements,
policies, or init-data payloads.
The design of the systemd units and mount script can support both modes:
if per-addon verity parameters are present on the kernel command line, they
are used directly (current behavior); if a manifest addon is present, it
is consulted for the remaining addons. This makes the manifest mode an
if per-extension verity parameters are present on the kernel command line, they
are used directly (current behavior); if a manifest extension is present, it
is consulted for the remaining extensions. This makes the manifest mode an
additive, backward-compatible evolution.
### Addon versioning and attestation
### Extension versioning and attestation
Future work may add version metadata to addon images, enabling the
Future work may add version metadata to guest extension images, enabling the
attestation flow to verify not just integrity (via dm-verity) but also that
the specific expected version of each component is present.