#!/usr/bin/env bash # extract-artifacts.sh # # Pulls the diskless deliverables out of the Packer-built disk image: # vmlinuz -> HTTPS boot server # initramfs.img -> HTTPS boot server (the fleet-generic image) # rootfs.erofs -> nvmet target: shared READ-ONLY NVMe/TCP namespace # (erofs data + dm-verity hash tree appended in-file) # verity.json -> root hash + hash offset (feeds the kernel cmdline) # cmdline.txt -> ready-made kernel cmdline fragment for this build # # Requires on the build host: libguestfs-tools, erofs-utils (mkfs.erofs), # cryptsetup (veritysetup), jq. set -euo pipefail QCOW2=${1:?usage: extract-artifacts.sh [disable_dot1x]} OUT=${2:?usage: extract-artifacts.sh [disable_dot1x]} # TEMPORARY test toggle: when "true", bake rd.dot1x=0 into the cmdline so the # UKI boots with 802.1X + MACsec disabled (open-port NVMe-oF boot test). DISABLE_DOT1X=${3:-false} # NVMe/TCP discovery address baked into the (immutable, signed) UKI cmdline. # Prefer a per-environment DNS name resolved at boot so one UKI works across # environments. Defaults to the literal placeholder so build-oci.sh's warning # fires if it was never set. NVMET_TRADDR=${NVMET_TRADDR:-NVMET_TRADDR} # Stable root-namespace UUID. The root device is referenced by-id # (/dev/disk/by-id/nvme-uuid.$ROOT_NS_UUID) rather than /dev/nvmeNn1 because # NVMe controller indices are NOT stable: a transient discovery controller can # take nvme0, pushing the I/O controller to nvme1, so a hardcoded node is a # latent brick in an immutable signed cmdline. MUST match ROOT_NS_UUID in # serving/spdk-setup.sh (which assigns it on the target via add_ns -u). ROOT_NS_UUID=${ROOT_NS_UUID:-f1ee7000-0000-4000-8000-000000000001} ROOT_DEV="/dev/disk/by-id/nvme-uuid.${ROOT_NS_UUID}" for bin in virt-copy-out virt-tar-out virt-ls mkfs.erofs veritysetup jq; do command -v "$bin" >/dev/null || { echo "ERROR: $bin not found on build host" >&2; exit 1; } done mkdir -p "$OUT" export LIBGUESTFS_BACKEND=direct WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT echo "==> Locating kernel and generic initramfs in $QCOW2" KERNEL=$(virt-ls -a "$QCOW2" /boot | grep '^vmlinuz-' | grep -v rescue | sort -V | tail -1) [ -n "$KERNEL" ] || { echo "ERROR: no kernel found in image" >&2; exit 1; } virt-copy-out -a "$QCOW2" "/boot/$KERNEL" "/boot/initramfs-generic.img" "$OUT/" mv "$OUT/$KERNEL" "$OUT/vmlinuz" mv "$OUT/initramfs-generic.img" "$OUT/initramfs.img" echo "==> Exporting root tree" virt-tar-out -a "$QCOW2" / "$WORK/rootfs.tar" mkdir "$WORK/root" tar -C "$WORK/root" --numeric-owner -xpf "$WORK/rootfs.tar" rm -rf "$WORK/root/boot/"* "$WORK/rootfs.tar" # diskless hosts don't need /boot echo "==> Building reproducible erofs image" # UNCOMPRESSED on purpose: the target Rocky 9 kernel's erofs is built without # CONFIG_EROFS_FS_ZIP, so mounting a compressed image fails at boot with # "erofs: lz4 algorithm isn't enabled" (verified on the bench — the root mount # never completes). An uncompressed image mounts with base CONFIG_EROFS_FS # alone. Tradeoff: larger namespace + more NVMe/TCP bandwidth per read than the # old `-z lz4hc`. Re-add compression (`-z lz4hc`, or zstd/deflate) ONLY against # a kernel you've confirmed has EROFS_FS_ZIP (grep CONFIG_EROFS /boot/config-*). # -T0 + fixed UUID keep output deterministic => stable dm-verity root hash for # identical input trees. FIXED_UUID="6c10a7f0-1d2e-4f5a-9b3c-000000000001" mkfs.erofs -T 0 -U "$FIXED_UUID" --all-root \ "$WORK/rootfs.erofs" "$WORK/root" echo "==> Appending dm-verity hash tree (single-file / single-namespace layout)" DATA_BYTES=$(stat -c %s "$WORK/rootfs.erofs") # Align hash area to 4 KiB after the data HASH_OFFSET=$(( (DATA_BYTES + 4095) / 4096 * 4096 )) VERITY_OUT=$(veritysetup format "$WORK/rootfs.erofs" "$WORK/rootfs.erofs" \ --hash-offset="$HASH_OFFSET" \ --data-block-size=4096 --hash-block-size=4096) ROOT_HASH=$(printf '%s\n' "$VERITY_OUT" | awk '/Root hash:/ {print $3}') [ -n "$ROOT_HASH" ] || { echo "ERROR: veritysetup produced no root hash" >&2; exit 1; } mv "$WORK/rootfs.erofs" "$OUT/rootfs.erofs" jq -n --arg hash "$ROOT_HASH" --argjson offset "$HASH_OFFSET" \ --arg uuid "$FIXED_UUID" \ '{root_hash: $hash, hash_offset: $offset, erofs_uuid: $uuid, data_block_size: 4096, hash_block_size: 4096}' \ > "$OUT/verity.json" # Kernel cmdline fragment for the boot server. Single line: this file is # consumed verbatim by ukify in build-oci.sh and baked into the signed UKI. # The device node assumes the root namespace is the host's first/only # NVMe/TCP namespace; adjust if not. NVMET_TRADDR: set per environment # *before* the UKI build, since the cmdline is immutable once signed. DOT1X_FRAGMENT="" if [ "$DISABLE_DOT1X" = "true" ]; then echo "==> WARNING: building TEST cmdline with rd.dot1x=0 (802.1X + MACsec DISABLED)" >&2 echo " open-port NVMe-oF boot test only — do NOT ship this UKI." >&2 # ip=dhcp: with auth disabled, dracut brings the NIC up by DHCP (writes # resolv.conf, satisfies rd.neednet) so the nvmf module can connect. The # cmdline otherwise has no ip=, so nothing configures the NIC. Test-only — # in the secure build the hook owns the link (macsec0); ip=dhcp must NOT be # set there. DOT1X_FRAGMENT="rd.dot1x=0 ip=dhcp " fi printf '%s' \ "$DOT1X_FRAGMENT" \ "rd.neednet=1 systemd.volatile=overlay " \ "rd.dot1x.nvindex=0x01800001 rd.dot1x.timeout=60 " \ "rd.nvmf.discover=tcp,${NVMET_TRADDR},4420 " \ "root=/dev/mapper/root rootfstype=erofs rootflags=ro " \ "systemd.verity=yes roothash=${ROOT_HASH} " \ "systemd.verity_root_data=${ROOT_DEV} " \ "systemd.verity_root_hash=${ROOT_DEV} " \ "systemd.verity_root_options=hash-offset=${HASH_OFFSET}" \ > "$OUT/cmdline.txt" echo "==> Checksums" ( cd "$OUT" && sha256sum vmlinuz initramfs.img rootfs.erofs | tee SHA256SUMS ) cat < publish to the HTTPS boot server (versioned dir, flip a symlink for atomic rollout/rollback) rootfs.erofs -> expose as a READ-ONLY nvmet namespace; version rollout = publish new namespace, repoint hosts cmdline.txt -> kernel cmdline for this build. The roothash binds the root fs to the boot artifacts: serve via a signed UKI or signed GRUB config so Secure Boot covers it, or the verity guarantee is advisory. Remember: identity is NOT in these artifacts. Hosts authenticate with the TPM key at the fleet persistent handle and the cert in the fleet NV index; the hostnqn is derived from that cert at boot. EOF