agent: add systemd template unit for addon image mount/unmount

Add a systemd template unit kata-addon-mount@.service and companion
helper scripts (kata-addon-mount.sh, kata-addon-umount.sh) that handle
the guest-side setup of addon block device images.

The mount script:
- Discovers the block device via /dev/disk/by-id/virtio-addon-<name>
- Reads verity parameters from kata.addon.<name>.verity_params on the
  kernel command line
- Creates a dm-verity device and mounts the EROFS filesystem
- Bind-mounts addon directories (usr/local/bin, etc, pause_bundle)
  into the rootfs

The unit uses ConditionPathExists to only activate when the
corresponding virtio-blk device is present, and is ordered before
kata-agent.service so addon binaries are available when the agent
starts.

The agent Makefile is updated to install the unit and scripts.

Signed-off-by: Fabiano Fidêncio <ffidencio@nvidia.com>
Assisted-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Fabiano Fidêncio
2026-05-10 19:03:37 +02:00
parent 75acd74912
commit c44f8336c9
6 changed files with 239 additions and 25 deletions

View File

@@ -91,6 +91,10 @@ ifeq ($(INIT),no)
GENERATED_FILES += $(UNIT_FILES)
# Target to be reached in systemd services
UNIT_FILES += kata-containers.target
# Template unit for mounting addon block devices (CoCo, GPU, etc.)
UNIT_FILES += kata-addon-mount@.service
# Helper scripts for addon mount/umount
ADDON_SCRIPTS = kata-addon-mount.sh kata-addon-umount.sh
endif
# Display name of command and it's version (or a message if not available).
@@ -154,6 +158,8 @@ install-services: $(GENERATED_FILES)
ifeq ($(INIT),no)
@echo "Installing systemd unit files..."
$(foreach f,$(UNIT_FILES),$(call INSTALL_FILE,$f,$(UNIT_DIR)))
@echo "Installing addon helper scripts..."
$(foreach f,$(ADDON_SCRIPTS),install -D -m 755 $f $(DESTDIR)/usr/libexec/$f || exit 1;)
endif
show-header:

104
src/agent/kata-addon-mount.sh Executable file
View File

@@ -0,0 +1,104 @@
#!/bin/bash
#
# Copyright (c) 2026 NVIDIA Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
# Mount a kata addon block device with optional dm-verity verification.
# Usage: kata-addon-mount.sh <addon-name>
#
# The block device is discovered by scanning /sys/block/*/serial for
# a device whose serial matches "addon-<name>".
# Verity params are read from kernel cmdline: kata.addon.<name>.verity_params=...
# The addon is mounted read-only (erofs) at /run/kata-addons/<name>/.
set -euo pipefail
ADDON_NAME="${1:?addon name required}"
SERIAL="addon-${ADDON_NAME}"
MOUNT_DIR="/run/kata-addons/${ADDON_NAME}"
find_block_dev_by_serial() {
local wanted="$1"
for s in /sys/block/*/serial; do
[[ -f "${s}" ]] || continue
local cur
cur="$(cat "${s}" 2>/dev/null)" || continue
if [[ "${cur}" == "${wanted}" ]]; then
echo "/dev/$(basename "$(dirname "${s}")")"
return 0
fi
done
return 1
}
REAL_DEV="$(find_block_dev_by_serial "${SERIAL}")" || {
echo "ERROR: no block device with serial ${SERIAL} found" >&2
exit 1
}
get_verity_param() {
local key="kata.addon.${ADDON_NAME}.verity_params"
local cmdline
cmdline="$(cat /proc/cmdline)"
local value=""
for param in ${cmdline}; do
case "${param}" in
"${key}="*)
value="${param#"${key}="}"
;;
esac
done
echo "${value}"
}
parse_verity_field() {
local params="$1"
local field="$2"
echo "${params}" | tr ',' '\n' | while IFS='=' read -r k v; do
if [[ "${k}" == "${field}" ]]; then
echo "${v}"
return
fi
done
}
VERITY_PARAMS="$(get_verity_param)"
if [[ -n "${VERITY_PARAMS}" ]]; then
ROOT_HASH="$(parse_verity_field "${VERITY_PARAMS}" "root_hash")"
SALT="$(parse_verity_field "${VERITY_PARAMS}" "salt")"
DATA_BLOCKS="$(parse_verity_field "${VERITY_PARAMS}" "data_blocks")"
HASH_BLOCK_SIZE="$(parse_verity_field "${VERITY_PARAMS}" "hash_block_size")"
DATA_BLOCK_SIZE="$(parse_verity_field "${VERITY_PARAMS}" "data_block_size")"
PART_SEP=""
[[ "${REAL_DEV}" =~ [0-9]$ ]] && PART_SEP="p"
DATA_DEV="${REAL_DEV}${PART_SEP}1"
HASH_DEV="${REAL_DEV}${PART_SEP}2"
DM_NAME="addon-${ADDON_NAME}"
veritysetup open "${DATA_DEV}" "${DM_NAME}" "${HASH_DEV}" "${ROOT_HASH}" \
--no-superblock \
--hash-block-size="${HASH_BLOCK_SIZE:-4096}" \
--data-block-size="${DATA_BLOCK_SIZE:-4096}" \
--data-blocks="${DATA_BLOCKS}" \
--salt="${SALT}"
MOUNT_SRC="/dev/mapper/${DM_NAME}"
else
PART_SEP=""
[[ "${REAL_DEV}" =~ [0-9]$ ]] && PART_SEP="p"
if [[ -e "${REAL_DEV}${PART_SEP}1" ]]; then
MOUNT_SRC="${REAL_DEV}${PART_SEP}1"
else
MOUNT_SRC="${REAL_DEV}"
fi
fi
mkdir -p "${MOUNT_DIR}"
mount -t erofs -o ro "${MOUNT_SRC}" "${MOUNT_DIR}"
echo "Addon ${ADDON_NAME} mounted at ${MOUNT_DIR}"

View File

@@ -0,0 +1,22 @@
#
# Copyright (c) 2026 NVIDIA Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
[Unit]
Description=Mount Kata addon image %i
DefaultDependencies=no
Before=kata-agent.service
After=local-fs-pre.target
ConditionKernelCommandLine=kata.addon.%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
[Install]
WantedBy=kata-containers.target

49
src/agent/kata-addon-umount.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
#
# Copyright (c) 2026 NVIDIA Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
# Unmount a kata addon block device.
# Usage: kata-addon-umount.sh <addon-name>
set -euo pipefail
ADDON_NAME="${1:?addon name required}"
MOUNT_DIR="/run/kata-addons/${ADDON_NAME}"
DM_NAME="addon-${ADDON_NAME}"
# Undo bind mounts from /usr/local/bin/
if [[ -d "${MOUNT_DIR}/usr/local/bin" ]]; then
for bin in "${MOUNT_DIR}"/usr/local/bin/*; do
[[ -f "${bin}" ]] || continue
target="/usr/local/bin/$(basename "${bin}")"
umount "${target}" 2>/dev/null || true
done
fi
# Undo bind mounts from /etc/
if [[ -d "${MOUNT_DIR}/etc" ]]; then
for cfg in "${MOUNT_DIR}"/etc/*; do
[[ -e "${cfg}" ]] || continue
target="/etc/$(basename "${cfg}")"
umount "${target}" 2>/dev/null || true
done
fi
# Undo pause_bundle bind mount
if mountpoint -q /pause_bundle 2>/dev/null; then
umount /pause_bundle 2>/dev/null || true
fi
# Unmount the addon filesystem
if mountpoint -q "${MOUNT_DIR}" 2>/dev/null; then
umount "${MOUNT_DIR}"
fi
# Close dm-verity device if present
if [[ -e "/dev/mapper/${DM_NAME}" ]]; then
veritysetup close "${DM_NAME}" 2>/dev/null || true
fi
echo "Addon ${ADDON_NAME} unmounted"

View File

@@ -22,6 +22,15 @@ use protocols::agent::Storage;
pub const KATA_IMAGE_WORK_DIR: &str = "/run/kata-containers/image/";
const CONFIG_JSON: &str = "config.json";
const KATA_PAUSE_BUNDLE: &str = "/pause_bundle";
const KATA_PAUSE_BUNDLE_ADDON: &str = "/run/kata-addons/coco/pause_bundle";
fn resolve_pause_bundle() -> &'static str {
if Path::new(KATA_PAUSE_BUNDLE_ADDON).exists() {
KATA_PAUSE_BUNDLE_ADDON
} else {
KATA_PAUSE_BUNDLE
}
}
const K8S_CONTAINER_TYPE_KEYS: [&str; 2] = [
"io.kubernetes.cri.container-type",
@@ -46,7 +55,7 @@ fn copy_if_not_exists(src: &Path, dst: &Path) -> Result<()> {
/// get guest pause image process specification
fn get_pause_image_process() -> Result<oci::Process> {
let guest_pause_bundle = Path::new(KATA_PAUSE_BUNDLE);
let guest_pause_bundle = Path::new(resolve_pause_bundle());
if !guest_pause_bundle.exists() {
bail!("Pause image not present in rootfs");
}
@@ -70,7 +79,7 @@ fn get_pause_image_process() -> Result<oci::Process> {
pub fn unpack_pause_image(cid: &str) -> Result<String> {
verify_id(cid).context("The guest pause image cid contains invalid characters.")?;
let guest_pause_bundle = Path::new(KATA_PAUSE_BUNDLE);
let guest_pause_bundle = Path::new(resolve_pause_bundle());
if !guest_pause_bundle.exists() {
bail!("Pause image not present in rootfs");
}

View File

@@ -96,20 +96,34 @@ const NAME: &str = "kata-agent";
const UNIX_SOCKET_PREFIX: &str = "unix://";
const COCO_ADDON_DIR: &str = "/run/kata-addons/coco";
const AA_PATH: &str = "/usr/local/bin/attestation-agent";
const AA_ADDON_PATH: &str = concatcp!(COCO_ADDON_DIR, "/usr/local/bin/attestation-agent");
const AA_ATTESTATION_SOCKET: &str =
"/run/confidential-containers/attestation-agent/attestation-agent.sock";
const AA_ATTESTATION_URI: &str = concatcp!(UNIX_SOCKET_PREFIX, AA_ATTESTATION_SOCKET);
const CDH_PATH: &str = "/usr/local/bin/confidential-data-hub";
const CDH_ADDON_PATH: &str = concatcp!(COCO_ADDON_DIR, "/usr/local/bin/confidential-data-hub");
const CDH_SOCKET: &str = "/run/confidential-containers/cdh.sock";
const CDH_SOCKET_URI: &str = concatcp!(UNIX_SOCKET_PREFIX, CDH_SOCKET);
const API_SERVER_PATH: &str = "/usr/local/bin/api-server-rest";
const API_SERVER_ADDON_PATH: &str = concatcp!(COCO_ADDON_DIR, "/usr/local/bin/api-server-rest");
/// Path of ocicrypt config file. This is used by CDH when decrypting image.
/// TODO: remove this when we move the launch of CDH out of the kata-agent.
const OCICRYPT_CONFIG_PATH: &str = "/etc/ocicrypt_config.json";
const OCICRYPT_CONFIG_ADDON_PATH: &str = concatcp!(COCO_ADDON_DIR, "/etc/ocicrypt_config.json");
/// Resolve a binary path: prefer the CoCo addon location, fall back to the
/// legacy rootfs path.
fn resolve_path<'a>(addon_path: &'a str, legacy_path: &'a str) -> &'a str {
if Path::new(addon_path).exists() {
addon_path
} else {
legacy_path
}
}
lazy_static! {
static ref AGENT_CONFIG: AgentConfig =
@@ -457,23 +471,31 @@ async fn start_sandbox(
Ok(())
}
// Check if required attestation binaries are available on the rootfs.
// Check if required attestation binaries are available, looking in the CoCo
// addon mount first, then in the legacy rootfs paths.
fn attestation_binaries_available(logger: &Logger, procs: &GuestComponentsProcs) -> bool {
let binaries = match procs {
GuestComponentsProcs::AttestationAgent => vec![AA_PATH],
GuestComponentsProcs::ConfidentialDataHub => vec![AA_PATH, CDH_PATH],
GuestComponentsProcs::ApiServerRest => vec![AA_PATH, CDH_PATH, API_SERVER_PATH],
let binaries: Vec<(&str, &str)> = match procs {
GuestComponentsProcs::AttestationAgent => vec![(AA_ADDON_PATH, AA_PATH)],
GuestComponentsProcs::ConfidentialDataHub => {
vec![(AA_ADDON_PATH, AA_PATH), (CDH_ADDON_PATH, CDH_PATH)]
}
GuestComponentsProcs::ApiServerRest => vec![
(AA_ADDON_PATH, AA_PATH),
(CDH_ADDON_PATH, CDH_PATH),
(API_SERVER_ADDON_PATH, API_SERVER_PATH),
],
_ => vec![],
};
for binary in binaries.iter() {
let exists = Path::new(binary)
for (addon, legacy) in binaries.iter() {
let resolved = resolve_path(addon, legacy);
let exists = Path::new(resolved)
.try_exists()
.unwrap_or_else(|error| match error.kind() {
ErrorKind::NotFound => {
warn!(logger, "{} not found", binary);
warn!(logger, "{} not found", resolved);
false
}
_ => panic!("Path existence check failed for '{}': {}", binary, error),
_ => panic!("Path existence check failed for '{}': {}", resolved, error),
});
if !exists {
@@ -492,7 +514,8 @@ async fn launch_guest_component_procs(
return Ok(());
}
debug!(logger, "spawning attestation-agent process {}", AA_PATH);
let aa_path = resolve_path(AA_ADDON_PATH, AA_PATH);
debug!(logger, "spawning attestation-agent process {}", aa_path);
let mut aa_args = vec!["--attestation_sock", AA_ATTESTATION_URI];
if initdata_return_value.is_some() {
aa_args.push("--initdata-toml");
@@ -501,7 +524,7 @@ async fn launch_guest_component_procs(
launch_process(
logger,
AA_PATH,
aa_path,
aa_args,
Some(AA_CONFIG_PATH),
AA_ATTESTATION_SOCKET,
@@ -509,43 +532,44 @@ async fn launch_guest_component_procs(
&[],
)
.await
.map_err(|e| anyhow!("launch_process {} failed: {:?}", AA_PATH, e))?;
.map_err(|e| anyhow!("launch_process {} failed: {:?}", aa_path, e))?;
// skip launch of confidential-data-hub and api-server-rest
if config.guest_components_procs == GuestComponentsProcs::AttestationAgent {
return Ok(());
}
let cdh_path = resolve_path(CDH_ADDON_PATH, CDH_PATH);
let ocicrypt_path = resolve_path(OCICRYPT_CONFIG_ADDON_PATH, OCICRYPT_CONFIG_PATH);
debug!(
logger,
"spawning confidential-data-hub process {}", CDH_PATH
"spawning confidential-data-hub process {}", cdh_path
);
launch_process(
logger,
CDH_PATH,
cdh_path,
vec![],
Some(CDH_CONFIG_PATH),
CDH_SOCKET,
config.launch_process_timeout.as_secs(),
&[("OCICRYPT_KEYPROVIDER_CONFIG", OCICRYPT_CONFIG_PATH)],
&[("OCICRYPT_KEYPROVIDER_CONFIG", ocicrypt_path)],
)
.await
.map_err(|e| anyhow!("launch_process {} failed: {:?}", CDH_PATH, e))?;
.map_err(|e| anyhow!("launch_process {} failed: {:?}", cdh_path, e))?;
// skip launch of api-server-rest
if config.guest_components_procs == GuestComponentsProcs::ConfidentialDataHub {
return Ok(());
}
let api_path = resolve_path(API_SERVER_ADDON_PATH, API_SERVER_PATH);
let features = config.guest_components_rest_api;
debug!(
logger,
"spawning api-server-rest process {} --features {}", API_SERVER_PATH, features
"spawning api-server-rest process {} --features {}", api_path, features
);
launch_process(
logger,
API_SERVER_PATH,
api_path,
vec!["--features", &features.to_string()],
None,
"",
@@ -553,7 +577,7 @@ async fn launch_guest_component_procs(
&[],
)
.await
.map_err(|e| anyhow!("launch_process {} failed: {:?}", API_SERVER_PATH, e))?;
.map_err(|e| anyhow!("launch_process {} failed: {:?}", api_path, e))?;
Ok(())
}