From c44f8336c9bc3d8d83360435496ec764c7ac5abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabiano=20Fid=C3=AAncio?= Date: Sun, 10 May 2026 19:03:37 +0200 Subject: [PATCH] agent: add systemd template unit for addon image mount/unmount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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- - Reads verity parameters from kata.addon..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 Assisted-by: Cursor --- src/agent/Makefile | 6 ++ src/agent/kata-addon-mount.sh | 104 +++++++++++++++++++ src/agent/kata-addon-mount@.service | 22 ++++ src/agent/kata-addon-umount.sh | 49 +++++++++ src/agent/src/confidential_data_hub/image.rs | 13 ++- src/agent/src/main.rs | 70 +++++++++---- 6 files changed, 239 insertions(+), 25 deletions(-) create mode 100755 src/agent/kata-addon-mount.sh create mode 100644 src/agent/kata-addon-mount@.service create mode 100755 src/agent/kata-addon-umount.sh diff --git a/src/agent/Makefile b/src/agent/Makefile index 79e59907a8..307af58063 100644 --- a/src/agent/Makefile +++ b/src/agent/Makefile @@ -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: diff --git a/src/agent/kata-addon-mount.sh b/src/agent/kata-addon-mount.sh new file mode 100755 index 0000000000..cd5eba2891 --- /dev/null +++ b/src/agent/kata-addon-mount.sh @@ -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 +# +# The block device is discovered by scanning /sys/block/*/serial for +# a device whose serial matches "addon-". +# Verity params are read from kernel cmdline: kata.addon..verity_params=... +# The addon is mounted read-only (erofs) at /run/kata-addons//. + +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}" diff --git a/src/agent/kata-addon-mount@.service b/src/agent/kata-addon-mount@.service new file mode 100644 index 0000000000..b95f9f4141 --- /dev/null +++ b/src/agent/kata-addon-mount@.service @@ -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 diff --git a/src/agent/kata-addon-umount.sh b/src/agent/kata-addon-umount.sh new file mode 100755 index 0000000000..5302d2f755 --- /dev/null +++ b/src/agent/kata-addon-umount.sh @@ -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 + +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" diff --git a/src/agent/src/confidential_data_hub/image.rs b/src/agent/src/confidential_data_hub/image.rs index 2087b7a378..680c77a742 100644 --- a/src/agent/src/confidential_data_hub/image.rs +++ b/src/agent/src/confidential_data_hub/image.rs @@ -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 { - 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 { pub fn unpack_pause_image(cid: &str) -> Result { 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"); } diff --git a/src/agent/src/main.rs b/src/agent/src/main.rs index 504644a16e..30e0af4da4 100644 --- a/src/agent/src/main.rs +++ b/src/agent/src/main.rs @@ -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(()) }