Security hardening: bind kubeconfig server to localhost, mount hardening (noexec/nosuid/nodev on tmpfs), sysctl network hardening, kernel module loading lock after boot, SHA256 checksum verification for downloads, kernel AppArmor + Audit support, complain-mode AppArmor profiles for containerd and kubelet, and security integration test. ARM64 Raspberry Pi support: piCore64 base extraction, RPi kernel build from raspberrypi/linux fork, RPi firmware fetch, SD card image with 4- partition GPT and tryboot A/B mechanism, BootEnv Go interface abstracting GRUB vs RPi boot environments, architecture-aware build scripts, QEMU aarch64 dev VM and boot test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
204 lines
6.1 KiB
Bash
Executable File
204 lines
6.1 KiB
Bash
Executable File
#!/bin/bash
|
|
# create-rpi-image.sh — Create a raw disk image for Raspberry Pi SD card
|
|
#
|
|
# Partition layout (GPT):
|
|
# Part 1: Boot Control (16 MB, FAT32, label KSOLOCTL) — autoboot.txt only
|
|
# Part 2: Boot A (256 MB, FAT32, label KSOLOA) — firmware + kernel + DTBs + initramfs
|
|
# Part 3: Boot B (256 MB, FAT32, label KSOLOB) — same as Boot A (initially identical)
|
|
# Part 4: Data (remaining of 2GB, ext4, label KSOLODATA)
|
|
#
|
|
# The RPi uses autoboot.txt in the control partition to implement A/B boot
|
|
# via the tryboot mechanism (tryboot_a_b=1). Normal boot → partition 2 (Boot A),
|
|
# tryboot → partition 3 (Boot B).
|
|
#
|
|
# Usage: build/scripts/create-rpi-image.sh
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
# shellcheck source=../config/versions.env
|
|
. "$SCRIPT_DIR/../config/versions.env"
|
|
|
|
ROOTFS_DIR="${ROOTFS_DIR:-$PROJECT_ROOT/build/rootfs-work}"
|
|
OUTPUT_DIR="${OUTPUT_DIR:-$PROJECT_ROOT/output}"
|
|
CACHE_DIR="${CACHE_DIR:-$PROJECT_ROOT/build/cache}"
|
|
VERSION="$(cat "$PROJECT_ROOT/VERSION")"
|
|
|
|
IMG_OUTPUT="$OUTPUT_DIR/${OS_NAME}-${VERSION}.rpi.img"
|
|
IMG_SIZE_MB="${IMG_SIZE_MB:-2048}" # 2 GB default
|
|
|
|
# ARM64 kernel (Image format, not bzImage)
|
|
KERNEL="${CACHE_DIR}/custom-kernel-arm64/Image"
|
|
INITRAMFS="${ROOTFS_DIR}/kubesolo-os.gz"
|
|
RPI_FIRMWARE_DIR="${CACHE_DIR}/rpi-firmware"
|
|
|
|
echo "==> Creating ${IMG_SIZE_MB}MB Raspberry Pi disk image..."
|
|
|
|
# --- Verify required files ---
|
|
MISSING=0
|
|
for f in "$KERNEL" "$INITRAMFS"; do
|
|
if [ ! -f "$f" ]; then
|
|
echo "ERROR: Missing $f"
|
|
MISSING=1
|
|
fi
|
|
done
|
|
|
|
if [ ! -d "$RPI_FIRMWARE_DIR" ]; then
|
|
echo "ERROR: Missing RPi firmware directory: $RPI_FIRMWARE_DIR"
|
|
echo " Run 'make fetch' to download firmware blobs."
|
|
MISSING=1
|
|
fi
|
|
|
|
if [ "$MISSING" = "1" ]; then
|
|
echo ""
|
|
echo "Required files:"
|
|
echo " Kernel: $KERNEL (run 'make kernel-arm64')"
|
|
echo " Initramfs: $INITRAMFS (run 'make initramfs')"
|
|
echo " Firmware: $RPI_FIRMWARE_DIR/ (run 'make fetch')"
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$OUTPUT_DIR"
|
|
|
|
# --- Create sparse image ---
|
|
dd if=/dev/zero of="$IMG_OUTPUT" bs=1M count=0 seek="$IMG_SIZE_MB" 2>/dev/null
|
|
|
|
# --- Partition table (GPT) ---
|
|
# Part 1: Boot Control 16 MB FAT32
|
|
# Part 2: Boot A 256 MB FAT32
|
|
# Part 3: Boot B 256 MB FAT32
|
|
# Part 4: Data remaining ext4
|
|
sfdisk "$IMG_OUTPUT" << EOF
|
|
label: gpt
|
|
|
|
# Boot Control partition: 16 MB
|
|
start=2048, size=32768, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="BootCtl"
|
|
# Boot A partition: 256 MB
|
|
size=524288, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="BootA"
|
|
# Boot B partition: 256 MB
|
|
size=524288, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="BootB"
|
|
# Data partition: remaining
|
|
type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name="Data"
|
|
EOF
|
|
|
|
# --- Set up loop device ---
|
|
LOOP=$(losetup --show -fP "$IMG_OUTPUT")
|
|
echo "==> Loop device: $LOOP"
|
|
|
|
MNT_CTL=$(mktemp -d)
|
|
MNT_BOOTA=$(mktemp -d)
|
|
MNT_BOOTB=$(mktemp -d)
|
|
MNT_DATA=$(mktemp -d)
|
|
|
|
cleanup() {
|
|
umount "$MNT_CTL" 2>/dev/null || true
|
|
umount "$MNT_BOOTA" 2>/dev/null || true
|
|
umount "$MNT_BOOTB" 2>/dev/null || true
|
|
umount "$MNT_DATA" 2>/dev/null || true
|
|
losetup -d "$LOOP" 2>/dev/null || true
|
|
rm -rf "$MNT_CTL" "$MNT_BOOTA" "$MNT_BOOTB" "$MNT_DATA" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# --- Format partitions ---
|
|
mkfs.vfat -F 32 -n KSOLOCTL "${LOOP}p1"
|
|
mkfs.vfat -F 32 -n KSOLOA "${LOOP}p2"
|
|
mkfs.vfat -F 32 -n KSOLOB "${LOOP}p3"
|
|
mkfs.ext4 -q -L KSOLODATA "${LOOP}p4"
|
|
|
|
# --- Mount all partitions ---
|
|
mount "${LOOP}p1" "$MNT_CTL"
|
|
mount "${LOOP}p2" "$MNT_BOOTA"
|
|
mount "${LOOP}p3" "$MNT_BOOTB"
|
|
mount "${LOOP}p4" "$MNT_DATA"
|
|
|
|
# --- Boot Control Partition (KSOLOCTL) ---
|
|
echo " Writing autoboot.txt..."
|
|
cat > "$MNT_CTL/autoboot.txt" << 'AUTOBOOT'
|
|
[all]
|
|
tryboot_a_b=1
|
|
boot_partition=2
|
|
[tryboot]
|
|
boot_partition=3
|
|
AUTOBOOT
|
|
|
|
# --- Helper: populate a boot partition ---
|
|
populate_boot_partition() {
|
|
local MNT="$1"
|
|
local LABEL="$2"
|
|
|
|
echo " Populating $LABEL..."
|
|
|
|
# config.txt — Raspberry Pi boot configuration
|
|
cat > "$MNT/config.txt" << 'CFGTXT'
|
|
arm_64bit=1
|
|
kernel=kernel8.img
|
|
initramfs kubesolo-os.gz followkernel
|
|
enable_uart=1
|
|
gpu_mem=16
|
|
dtoverlay=disable-wifi
|
|
dtoverlay=disable-bt
|
|
CFGTXT
|
|
|
|
# cmdline.txt — kernel command line
|
|
# Note: must be a single line
|
|
echo "console=serial0,115200 console=tty1 kubesolo.data=LABEL=KSOLODATA quiet" > "$MNT/cmdline.txt"
|
|
|
|
# Copy kernel as kernel8.img (RPi 3/4/5 ARM64 convention)
|
|
cp "$KERNEL" "$MNT/kernel8.img"
|
|
|
|
# Copy initramfs
|
|
cp "$INITRAMFS" "$MNT/kubesolo-os.gz"
|
|
|
|
# Copy firmware blobs (start*.elf, fixup*.dat)
|
|
if ls "$RPI_FIRMWARE_DIR"/start*.elf 1>/dev/null 2>&1; then
|
|
cp "$RPI_FIRMWARE_DIR"/start*.elf "$MNT/"
|
|
fi
|
|
if ls "$RPI_FIRMWARE_DIR"/fixup*.dat 1>/dev/null 2>&1; then
|
|
cp "$RPI_FIRMWARE_DIR"/fixup*.dat "$MNT/"
|
|
fi
|
|
if [ -f "$RPI_FIRMWARE_DIR/bootcode.bin" ]; then
|
|
cp "$RPI_FIRMWARE_DIR/bootcode.bin" "$MNT/"
|
|
fi
|
|
|
|
# Copy DTB overlays
|
|
if [ -d "$RPI_FIRMWARE_DIR/overlays" ]; then
|
|
cp -r "$RPI_FIRMWARE_DIR/overlays" "$MNT/"
|
|
fi
|
|
|
|
# Copy base DTBs (bcm2710-*, bcm2711-*, bcm2712-*)
|
|
if ls "$RPI_FIRMWARE_DIR"/bcm27*.dtb 1>/dev/null 2>&1; then
|
|
cp "$RPI_FIRMWARE_DIR"/bcm27*.dtb "$MNT/"
|
|
fi
|
|
|
|
# Write version marker
|
|
echo "$VERSION" > "$MNT/version.txt"
|
|
}
|
|
|
|
# --- Boot A Partition (KSOLOA) ---
|
|
populate_boot_partition "$MNT_BOOTA" "Boot A (KSOLOA)"
|
|
|
|
# --- Boot B Partition (KSOLOB, initially identical) ---
|
|
populate_boot_partition "$MNT_BOOTB" "Boot B (KSOLOB)"
|
|
|
|
# --- Data Partition (KSOLODATA) ---
|
|
echo " Preparing data partition..."
|
|
for dir in kubesolo containerd etc-kubesolo log usr-local network images; do
|
|
mkdir -p "$MNT_DATA/$dir"
|
|
done
|
|
|
|
sync
|
|
|
|
echo ""
|
|
echo "==> Raspberry Pi disk image created: $IMG_OUTPUT"
|
|
echo " Size: $(du -h "$IMG_OUTPUT" | cut -f1)"
|
|
echo " Part 1 (KSOLOCTL): Boot control (autoboot.txt)"
|
|
echo " Part 2 (KSOLOA): Boot A — firmware + kernel + initramfs"
|
|
echo " Part 3 (KSOLOB): Boot B — firmware + kernel + initramfs"
|
|
echo " Part 4 (KSOLODATA): Persistent K8s state"
|
|
echo ""
|
|
echo "Write to SD card with:"
|
|
echo " sudo dd if=$IMG_OUTPUT of=/dev/sdX bs=4M status=progress"
|
|
echo ""
|