Initial commit
This commit is contained in:
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-oci.sh <artifact-dir> <image-ref> <version> [sb-key] [sb-cert] [push]
|
||||
#
|
||||
# Turns the extracted build artifacts into the fleet release unit:
|
||||
# 1. ukify: vmlinuz + initramfs + cmdline(roothash) -> fleet-<version>.efi
|
||||
# 2. sbsign via ukify if a Secure Boot key/cert pair is supplied
|
||||
# (key should live in CI/HSM -- it is deliberately NEVER inside the
|
||||
# Packer build VM, which is why this runs on the build host)
|
||||
# 3. consistency gate: roothash inside the UKI cmdline must match
|
||||
# verity.json -- a mismatched pair is an unbootable fleet
|
||||
# 4. builds a FROM-scratch OCI image containing ONLY:
|
||||
# /artifacts/fleet-<version>.efi (the signed UKI)
|
||||
# /artifacts/rootfs.erofs (erofs + appended verity tree)
|
||||
# /artifacts/verity.json
|
||||
# /artifacts/SHA256SUMS
|
||||
# Deploy as an initContainer (or k8s image volume) feeding the
|
||||
# SPDK + nginx serving containers. Pin deployments by digest.
|
||||
#
|
||||
# Requires on the build host: systemd-ukify, sbsigntools (if signing),
|
||||
# jq, and buildah (preferred) or docker.
|
||||
set -euo pipefail
|
||||
|
||||
ART=${1:?usage: build-oci.sh <artifact-dir> <image-ref> <version> [sb-key] [sb-cert] [push]}
|
||||
IMAGE=${2:?image ref required, e.g. registry.example.org/fleet-boot}
|
||||
VERSION=${3:?version required}
|
||||
SB_KEY=${4:-}
|
||||
SB_CERT=${5:-}
|
||||
PUSH=${6:-false}
|
||||
|
||||
for bin in ukify jq; do
|
||||
command -v "$bin" >/dev/null || { echo "ERROR: $bin not found on build host" >&2; exit 1; }
|
||||
done
|
||||
|
||||
UKI="$ART/fleet-${VERSION}.efi"
|
||||
CMDLINE=$(cat "$ART/cmdline.txt")
|
||||
|
||||
if printf '%s' "$CMDLINE" | grep -q NVMET_TRADDR; then
|
||||
echo "WARNING: cmdline still contains the NVMET_TRADDR placeholder." >&2
|
||||
echo " Substitute the real target address before a production build;" >&2
|
||||
echo " the cmdline is immutable once signed into the UKI." >&2
|
||||
fi
|
||||
|
||||
# --- 1+2. Build (and sign) the UKI -------------------------------------------
|
||||
UKIFY_ARGS=(build
|
||||
--linux "$ART/vmlinuz"
|
||||
--initrd "$ART/initramfs.img"
|
||||
--cmdline "$CMDLINE"
|
||||
--os-release "@NAME=FleetDiskless@VERSION=${VERSION}@"
|
||||
--output "$UKI")
|
||||
|
||||
if [ -n "$SB_KEY" ] && [ -n "$SB_CERT" ]; then
|
||||
command -v sbsign >/dev/null || { echo "ERROR: sbsigntools required for signing" >&2; exit 1; }
|
||||
UKIFY_ARGS+=(--signtool sbsign
|
||||
--secureboot-private-key "$SB_KEY"
|
||||
--secureboot-certificate "$SB_CERT")
|
||||
else
|
||||
echo "WARNING: building UNSIGNED UKI (no sb-key/sb-cert supplied)." >&2
|
||||
echo " Fine for the lab; a production fleet must sign in CI/HSM." >&2
|
||||
fi
|
||||
|
||||
ukify "${UKIFY_ARGS[@]}"
|
||||
|
||||
# --- 3. Roothash coupling gate ------------------------------------------------
|
||||
WANT=$(jq -r .root_hash "$ART/verity.json")
|
||||
GOT=$(printf '%s' "$CMDLINE" | grep -o 'roothash=[0-9a-f]*' | cut -d= -f2)
|
||||
if [ "$WANT" != "$GOT" ]; then
|
||||
echo "FATAL: roothash in UKI cmdline ($GOT) != verity.json ($WANT)." >&2
|
||||
echo " Refusing to build a mismatched (unbootable) release image." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> roothash coupling verified: $WANT"
|
||||
|
||||
# --- 4. Build the OCI release-artifact image ----------------------------------
|
||||
STAGE=$(mktemp -d)
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
mkdir "$STAGE/artifacts"
|
||||
cp "$UKI" "$ART/rootfs.erofs" "$ART/verity.json" "$STAGE/artifacts/"
|
||||
( cd "$STAGE/artifacts" && sha256sum -- * > SHA256SUMS )
|
||||
|
||||
REF="${IMAGE}:${VERSION}"
|
||||
LABELS=(
|
||||
"org.opencontainers.image.title=fleet-boot-artifacts"
|
||||
"org.opencontainers.image.version=${VERSION}"
|
||||
"org.fleet.roothash=${WANT}"
|
||||
"org.fleet.uki=fleet-${VERSION}.efi"
|
||||
)
|
||||
|
||||
if command -v buildah >/dev/null; then
|
||||
ctr=$(buildah from scratch)
|
||||
buildah copy "$ctr" "$STAGE/artifacts" /artifacts
|
||||
for l in "${LABELS[@]}"; do buildah config --label "$l" "$ctr"; done
|
||||
buildah commit --format oci --rm "$ctr" "$REF"
|
||||
DIGEST=$(buildah inspect --type image --format '{{.FromImageDigest}}' "$REF" || true)
|
||||
[ "$PUSH" = "true" ] && buildah push "$REF"
|
||||
elif command -v docker >/dev/null; then
|
||||
{ printf 'FROM scratch\nCOPY artifacts /artifacts\n'
|
||||
for l in "${LABELS[@]}"; do printf 'LABEL "%s"\n' "$l"; done
|
||||
} > "$STAGE/Dockerfile"
|
||||
docker build -t "$REF" "$STAGE"
|
||||
DIGEST=$(docker inspect --format '{{index .RepoDigests 0}}' "$REF" 2>/dev/null || true)
|
||||
[ "$PUSH" = "true" ] && docker push "$REF"
|
||||
else
|
||||
echo "ERROR: need buildah or docker to build the OCI image" >&2; exit 1
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
==> Release image built: $REF
|
||||
roothash: $WANT
|
||||
digest: ${DIGEST:-"(available after push)"}
|
||||
|
||||
Next steps:
|
||||
- cosign sign the image for supply-chain provenance (the UKI's sbsign
|
||||
signature is for firmware; cosign is for your deployment pipeline)
|
||||
- update the fleet-boot Deployment/compose manifest in git to this
|
||||
version's DIGEST (one commit per fleet release; that manifest is the
|
||||
single place coupling UKI <-> root namespace)
|
||||
EOF
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
# extract-artifacts.sh <qcow2> <output-dir>
|
||||
#
|
||||
# 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 <qcow2> <output-dir> [disable_dot1x]}
|
||||
OUT=${2:?usage: extract-artifacts.sh <qcow2> <output-dir> [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}
|
||||
|
||||
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"
|
||||
# -T0 + fixed UUID => deterministic output => stable dm-verity root hash
|
||||
# for identical input trees. lz4hc: good ratio, cheap decompress; saves
|
||||
# fabric bandwidth (and MACsec cycles) on every read.
|
||||
FIXED_UUID="6c10a7f0-1d2e-4f5a-9b3c-000000000001"
|
||||
mkfs.erofs -z lz4hc -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
|
||||
DOT1X_FRAGMENT="rd.dot1x=0 "
|
||||
fi
|
||||
printf '%s' \
|
||||
"$DOT1X_FRAGMENT" \
|
||||
"rd.neednet=1 rootovl " \
|
||||
"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=/dev/nvme0n1 " \
|
||||
"systemd.verity_root_hash=/dev/nvme0n1 " \
|
||||
"systemd.verity_root_options=hash-offset=${HASH_OFFSET}" \
|
||||
> "$OUT/cmdline.txt"
|
||||
|
||||
echo "==> Checksums"
|
||||
( cd "$OUT" && sha256sum vmlinuz initramfs.img rootfs.erofs | tee SHA256SUMS )
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Artifacts in $OUT:
|
||||
vmlinuz, initramfs.img -> 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
|
||||
Reference in New Issue
Block a user