Root cause of USB boot failure: our xorriso used -e boot/grub/efi.img to embed the EFI image inside the ISO. This works for CD-ROM and QEMU but NOT for USB on real UEFI hardware. Fix: use the Will Haley / Debian live-build approach: - -append_partition 2 (GPT type EFI) appends efi.img AFTER ISO data - -e --interval:appended_partition_2:all:: references the appended partition - --mbr-force-bootable forces MBR active flag - grub-mkstandalone with embedded bootstrap config (searches for grub.cfg) - grub.cfg placed in both /boot/grub/ AND /EFI/BOOT/ on ISO - grub.cfg uses search --label ARCHIPELAGO to find the ISO root This is the exact approach used by StartOS, TAILS, and every production custom Debian live ISO that boots from USB. Also: iso-debug, iso-branding skills + reference docs, dev-start.sh option 0 for branding dev, improved dev-branding.sh and test-iso-qemu.sh. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 KiB
Custom Debian ISO Boot Chain — Technical Reference
Expert reference for building and debugging custom bootable Debian-based ISOs. Covers hybrid MBR/GPT, live-boot, debootstrap, GRUB, ISOLINUX, Plymouth, and xorriso.
1. Hybrid MBR/GPT for USB Boot
What is isohdpfx.bin?
The first 432 bytes of a hybrid-bootable ISO. Contains the Master Boot Record code that BIOS firmware executes when booting from USB. Different sources produce different MBR code:
| Source | First bytes | Compatibility |
|---|---|---|
Debian Live ISO (dd if=debian-live.iso bs=1 count=432) |
45 52 |
Best — works on all tested hardware |
/usr/lib/ISOLINUX/isohdpfx.bin |
33 ed |
Generic — fails on some UEFI hardware |
Manually built with isohybrid |
Varies | Unpredictable |
Rule: Always extract MBR from a known-working ISO. Never rely on the generic ISOLINUX one.
xorriso flags for hybrid boot
xorriso -as mkisofs -o output.iso \
-isohybrid-mbr isohdpfx.bin \ # Embeds MBR for BIOS USB boot
-c isolinux/boot.cat \ # El Torito boot catalog
-b isolinux/isolinux.bin \ # BIOS bootloader
-no-emul-boot -boot-load-size 4 -boot-info-table \
-eltorito-alt-boot \ # Second boot entry (EFI)
-e boot/grub/efi.img \ # EFI boot image
-no-emul-boot \
-isohybrid-gpt-basdat \ # Adds GPT partition for EFI
-partition_offset 16 \ # Space for GPT table — REQUIRED for UEFI
/path/to/iso/contents
Critical flags:
-isohybrid-gpt-basdat: Without this, UEFI firmware won't see the EFI partition-partition_offset 16: Reserves 16 sectors for GPT. Without it, some UEFI firmware ignores the USB entirely-isohybrid-mbr: Without this, the ISO won't boot from USB at all (only CD-ROM)
Balena Etcher
Writes the ISO byte-for-byte to USB — no reformatting, no special partition creation. If the ISO works with dd, it works with Etcher. If BIOS doesn't see the USB, the MBR code is wrong, not Etcher.
Verifying hybrid structure
xxd -l 4 image.iso # MBR code (should be 45 52 for Debian Live)
xxd -s 510 -l 2 image.iso # Boot signature (must be 55 aa)
xxd -s 512 -l 8 image.iso # GPT signature at LBA 1 (should be "EFI PART")
file image.iso # Should say "DOS/MBR boot sector" and "bootable"
2. live-boot Package
What it does
Provides initramfs hooks that mount a squashfs file as the root filesystem using overlayfs. This is how every Debian/Ubuntu live ISO works.
Boot flow: kernel → initramfs → live-boot scripts → find squashfs → mount overlayfs → pivot_root → systemd
Package structure
live-boot(~29KB): Main package, boot scriptslive-boot-initramfs-tools(~6KB): Initramfs hooks that get baked into initrd.img
Critical: scripts/live is a FILE, not a directory. Verification must use [ -e ] not [ -d ].
Kernel parameters
| Parameter | Required | Effect |
|---|---|---|
boot=live |
YES | Activates live-boot's initramfs hooks |
components |
YES | Scans live/ for additional squashfs modules |
toram |
No | Copies squashfs to RAM (faster, allows USB removal) |
persistence |
No | Enables writable overlay on a partition labeled "persistence" |
quiet |
No | Suppresses boot messages |
splash |
No | Enables Plymouth splash screen |
console=ttyS0,115200 |
No | Serial console for QEMU debugging |
Where live-boot mounts things
/run/live/medium— The boot media (USB/CDROM) mount point/run/live/rootfs/filesystem.squashfs— The mounted squashfs/run/live/overlay— The tmpfs overlay for writes
Verifying live-boot in initramfs
TMPDIR=$(mktemp -d)
unmkinitramfs /path/to/initrd.img $TMPDIR
# Check for live-boot scripts
file $TMPDIR/scripts/live # Should be "ASCII text"
# OR (some initramfs have main/ prefix)
file $TMPDIR/main/scripts/live
Common failures
- live-boot not in initrd: Installed in rootfs but initramfs not regenerated after
- Missing kernel params:
boot=livenot in GRUB/ISOLINUX config - Broken initramfs: Built without /proc /sys /dev mounted in chroot
- Wrong verification:
[ -d scripts/live ]fails because it's a file
3. debootstrap for Installer Environments
Variants
--variant=minbase: Absolute minimum (~150MB). Only essential + apt. Good for installer squashfs.- Default (no variant): Full base system (~300MB). More packages, fewer missing deps.
--include limitations
debootstrap's minbase resolver is simplified and cannot resolve complex dependency chains. Packages like live-boot that depend on initramfs-tools which depends on many other packages will silently fail or be skipped.
Fix: Install complex packages via chroot apt-get after debootstrap completes:
debootstrap --variant=minbase --include=basic,packages bookworm /installer http://deb.debian.org/debian
# Then:
mount --bind /proc /installer/proc
mount --bind /sys /installer/sys
mount --bind /dev /installer/dev
chroot /installer apt-get update
chroot /installer apt-get install -y live-boot live-boot-initramfs-tools
umount /installer/dev /installer/sys /installer/proc
Initramfs generation inside containers
update-initramfs REQUIRES /proc, /sys, /dev to be mounted in the chroot. Without them:
- Module detection fails (can't read /proc/modules)
- Device nodes missing (can't detect hardware)
- The resulting initramfs boots but can't load kernel modules
Container-in-container considerations
When running debootstrap inside a Podman/Docker container on a CI runner:
--privilegedflag needed for chroot to work- The container runtime may kill the container after debootstrap exits if using
set -e - proc/sys/dev mounts inside the debootstrapped chroot work fine with
--privileged
4. GRUB Theming
theme.txt format
desktop-color: "#0a0a0a" # Fallback background color
desktop-image: "background.png" # Background image (any PNG, GRUB scales)
title-text: "" # Empty = hide title
+ boot_menu {
left = 25%
top = 40%
width = 50%
height = 30%
item_color = "#aaaaaa" # Normal menu item color
selected_item_color = "#fb923c" # Selected item color
item_height = 36
item_spacing = 8
scrollbar = false
}
+ label {
left = 25%
top = 20%
width = 50%
text = "Some Text"
color = "#f7931a"
align = "center"
}
IMPORTANT: Do NOT specify font = "Name Size" in theme elements unless you know the exact internal font name. If GRUB can't find the font, the ENTIRE theme fails to load and you get the ugly default.
Font handling
# Generate .pf2 font file
grub-mkfont -s 16 -o dejavu_16.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
# In grub.cfg, load fonts BEFORE setting theme:
loadfont /boot/grub/font.pf2
loadfont /boot/grub/themes/archipelago/dejavu_16.pf2
set theme=/boot/grub/themes/archipelago/theme.txt
Background images
- Any PNG works, GRUB scales to screen resolution
- Smaller images (1024x768) load faster
- Large images (3000x2000+) add seconds to boot and may fail on limited GRUB heap
grub-mkimage — essential modules for ISO boot
grub-mkimage -O x86_64-efi -o BOOTX64.EFI -p /boot/grub \
part_gpt part_msdos fat iso9660 udf \ # Filesystem access
normal boot linux search search_fs_uuid search_fs_file search_label \
configfile echo cat ls test true \ # Basic commands
loopback \ # Loop device support
gfxterm gfxmenu font png \ # Graphical display
all_video video video_bochs video_cirrus \ # Video drivers
efi_gop efi_uga # EFI display protocols
Missing all_video/efi_gop = black screen on real hardware (works in QEMU).
EFI boot image creation
dd if=/dev/zero of=efi.img bs=1M count=4
mkfs.vfat efi.img
mmd -i efi.img ::/EFI ::/EFI/BOOT
mcopy -i efi.img BOOTX64.EFI ::/EFI/BOOT/BOOTX64.EFI
5. Plymouth Boot Splash
Theme types
- script: Most flexible. Lua-like scripting with sprites, animations, callbacks.
- two-step: Simple logo + spinner. Less customizable but easier.
- fade-in: Logo fades in. Minimal.
Script theme structure
/usr/share/plymouth/themes/mytheme/
mytheme.plymouth # Theme metadata
mytheme.script # Animation script
logo.png # Logo image (PNG with alpha)
mytheme.plymouth
[Plymouth Theme]
Name=MyTheme
Description=Custom boot splash
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/mytheme
ScriptFile=/usr/share/plymouth/themes/mytheme/mytheme.script
Script language key functions
Window.SetBackgroundTopColor(r, g, b); // 0.0-1.0 floats
Window.SetBackgroundBottomColor(r, g, b);
image = Image("logo.png");
sprite = Sprite(image);
sprite.SetX(x); sprite.SetY(y); sprite.SetOpacity(0.0-1.0);
Plymouth.SetRefreshFunction(fn); // Called every frame
Plymouth.SetBootProgressFunction(fn); // fn(duration, progress)
Plymouth.SetDisplayPasswordFunction(fn); // fn(prompt, bullets)
Plymouth.SetQuitFunction(fn);
screen_w = Window.GetWidth();
screen_h = Window.GetHeight();
Setting default theme
plymouth-set-default-theme mytheme
# OR manually:
ln -sf /usr/share/plymouth/themes/mytheme/mytheme.plymouth /etc/alternatives/default.plymouth
Kernel params
splashin GRUB_CMDLINE_LINUX_DEFAULT enables Plymouthquietsuppresses text that would overlay Plymouth
6. ISOLINUX/SYSLINUX
Required files
| File | Source | Purpose |
|---|---|---|
isolinux.bin |
/usr/lib/ISOLINUX/isolinux.bin |
BIOS bootloader |
ldlinux.c32 |
/usr/lib/syslinux/modules/bios/ldlinux.c32 |
Core library (REQUIRED) |
menu.c32 |
/usr/lib/syslinux/modules/bios/menu.c32 |
Text menu UI |
libutil.c32 |
/usr/lib/syslinux/modules/bios/libutil.c32 |
Utility library |
boot.cat |
Auto-generated by xorriso | El Torito boot catalog |
isohdpfx.bin |
Extracted from working ISO | Hybrid MBR code |
Configuration (isolinux.cfg)
UI menu.c32
PROMPT 0
TIMEOUT 50 # 5 seconds (units of 1/10 second)
DEFAULT install
MENU TITLE MY INSTALLER
MENU COLOR border 30;44 #40ffffff #00000000 std
MENU COLOR title 1;36;44 #ff00b7ff #00000000 std
MENU COLOR sel 7;37;40 #ffffffff #ff333333 std
MENU COLOR unsel 37;44 #ffaaaaaa #00000000 std
LABEL install
MENU LABEL Install System
KERNEL /live/vmlinuz
APPEND initrd=/live/initrd.img boot=live components quiet
MENU DEFAULT
menu.c32 vs vesamenu.c32
menu.c32: Text-mode menu. More compatible, no background image.vesamenu.c32: VESA graphical menu. Supports background PNG, but some hardware/VMs don't support VESA.
7. Testing Without Real Hardware
QEMU UEFI boot
qemu-system-x86_64 \
-machine q35 \
-drive if=pflash,format=raw,readonly=on,file=/path/to/OVMF_CODE.fd \
-m 4G -smp 2 \
-boot d -cdrom image.iso \
-drive if=virtio,format=qcow2,file=test-disk.qcow2 \
-vga virtio -display default
QEMU BIOS boot (sees ISOLINUX)
qemu-system-x86_64 \
-machine pc \
-m 4G -smp 2 \
-boot d -cdrom image.iso \
-drive if=virtio,format=qcow2,file=test-disk.qcow2 \
-vga virtio -display default
Serial console capture
Add to QEMU: -serial file:/tmp/serial.log
Add to kernel params: console=ttyS0,115200 console=tty0
ISO structure verification (no boot required)
MNT=$(mktemp -d)
sudo mount -o loop,ro image.iso $MNT
# Check all critical files
for f in live/vmlinuz live/initrd.img live/filesystem.squashfs \
isolinux/isolinux.bin EFI/BOOT/BOOTX64.EFI boot/grub/grub.cfg; do
[ -e $MNT/$f ] && echo "OK: $f" || echo "MISSING: $f"
done
# Check initramfs for live-boot
INITRD=$(mktemp -d)
unmkinitramfs $MNT/live/initrd.img $INITRD
[ -e $INITRD/scripts/live ] && echo "live-boot: OK" || echo "live-boot: MISSING"
# Check kernel params
grep "boot=live" $MNT/boot/grub/grub.cfg && echo "params: OK"
sudo umount $MNT
8. Security Considerations for Custom ISOs
Supply chain
- Pin the Debian mirror URL (don't use redirectors in production)
- Verify package signatures (debootstrap does this by default)
- Pin kernel and GRUB package versions for reproducibility
Installer security
- Auto-install.sh runs as root — validate all inputs before path construction
- LUKS key generation must use CSPRNG (
/dev/urandom, never/dev/randomwhich blocks) - Drop the LUKS key file after writing to crypttab (or store in root-only location with 0400)
Boot security
- Secure Boot requires signed GRUB EFI binary (shim-signed package)
- Without Secure Boot, the unsigned BOOTX64.EFI works but users must disable Secure Boot in BIOS
- The MBR code (isohdpfx.bin) is not signed — Secure Boot only validates EFI path