In this guide, we’ll be going over the steps involved to build a LiveCD Debian distribution using debootstrap with both legacy BIOS and UEFI boot support using GRUB2. Why build your own distribution, you ask? There are scenarios and use cases that necessitate packages, certificates or other artifacts are already installed in the OS Image that’s distributed in the machines that are shipped, this guide will go over the steps involved to build that image.
Prerequisites
Let’s begin by installing the build tools.
sudo apt install \
debootstrap \
squashfs-tools \
xorriso \
isolinux \
syslinux-efi \
grub-pc-bin \
grub-efi-amd64-bin \
grub-efi-ia32-bin \
mtools \
dosfstools \
parted \
debian-archive-keyring
Create a directory where we will store all of the files we create throughout this guide.
mkdir -p $HOME/LIVE_BOOT
Bootstrap and Configure Debian
Set up the base Debian environment. I’m using bullseye
for my distribution and amd64
for the architecture. Consult the list of debian mirrors.
Please change the URL in this command if there is a mirror that is closer to you.
sudo debootstrap \
--arch=amd64 \
--variant=minbase \
bullseye \
$HOME/LIVE_BOOT/edit \
http://ftp.us.debian.org/debian/
Copy resolver settings
We’re copying the DNS resolver settings from the host to the root filesystem of the image we are building so that we can resolve DNS request while within the chroot that we’ll be pivoting into in the next step.
sudo mkdir -p $HOME/LIVE_BOOT/edit/run/systemd/resolve
sudo cp -f /etc/resolv.conf $HOME/LIVE_BOOT/edit/run/systemd/resolve/stub-resolv.conf
Prepare and chroot
Chroot to the Debian environment we just bootstrapped and mounts directories linux expects to handles processes and block devices
sudo mount -t proc /proc $HOME/LIVE_BOOT/edit/proc
sudo mount -t sysfs /sys $HOME/LIVE_BOOT/edit/sys
sudo mount -o bind /dev $HOME/LIVE_BOOT/edit/dev
sudo mount -o bind /dev/pts $HOME/LIVE_BOOT/edit/dev/pts
sudo chroot $HOME/LIVE_BOOT/edit /bin/bash
export HOME=/root
export LC_ALL=C
[chroot] Set a custom hostname for your Debian environment.
echo "custom-debian-machine" > /etc/hostname
[chroot] add hostname to hosts file and update the nameservers
cat > /etc/hosts << EOF
127.0.0.1 localhost
127.0.1.1 $(cat /etc/hostname)
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
EOF
cat << EOF > /run/systemd/resolve/stub-resolv.conf
nameserver 8.8.8.8
nameserver 1.1.1.1
EOF
[chroot] Install a Linux kernel of your choosing. I chose the image linux-image-amd64
. We also install live-boot
which provides the necessary hooks for LiveCD system. We install systemd-sysv
(or an equivalent) as it’s necessary for an init system. Let’s install some other packages we might need in our distribution. You can also add packages to this list to get installed as part of the image you are building, allowing you to further customize the image to include software you want to distribute with your distro.
apt update && \
apt install --no-install-recommends \
linux-image-amd64 \
live-boot \
systemd-sysv \
vim \
less \
dnsmasq \
network-manager \
net-tools \
wireless-tools \
curl \
openssh-client \
procps \
wget \
gnupg2 \
sudo \
ca-certificates \
iputils-ping
[chroot] Installing mosquitto This is a MQTT agent that facilitates a lot IoT based interactions with cloud-services or your local MQTT based IoT platform. This step can be ignores if you don’t have this requirement.
wget http://repo.mosquitto.org/debian/mosquitto-repo.gpg.key
apt-key add mosquitto-repo.gpg.key
apt-get update
apt-get install mosquitto
apt-get install mosquitto-clients
[chroot] Downloading the AmazonRootCA.pem Cert for various AWS IoT communications, can be ignored if not needed for your use case.
wget https://www.amazontrust.com/repository/AmazonRootCA1.pem -P /etc/pki
# check contents with.
openssl x509 -in /etc/pki/AmazonRootCA1.pem -text -noout
[chroot] Cleaning up
apt-get clean
apt-get autoclean
apt-get autoremove
# remove bash history
rm -rf /tmp/* ~/.bash_history
[chroot] Adding a user and setting password for root** Make sure to change this and not use the default. You can generated salted passwords using this command
openssl passwd -6 -salt xyz password
# password is password
useradd -G sudo -m -U -s /bin/bash -p \
$6$pass$75vTrf3kWE4ncL.vF1TixnLx7qs34pt3tlgm/6uXOhccJXsb9LrY.d3izKxAYnzlRvRwkolgdRlipd.eNrE8g. \
user
[chroot] Exit chroot and un-mount all special directories
exit
sudo umount $HOME/LIVE_BOOT/edit/proc
sudo umount $HOME/LIVE_BOOT/edit/sys
sudo umount $HOME/LIVE_BOOT/edit/dev/pts
sudo umount $HOME/LIVE_BOOT/edit/dev
Building the image with GRUB2
Create directories that will contain files for our live environment and scratch files.
mkdir -p $HOME/LIVE_BOOT/{scratch,image/live}
Compress the chroot environment into a Squash filesystem.
sudo mksquashfs \
$HOME/LIVE_BOOT/edit \
$HOME/LIVE_BOOT/image/live/filesystem.squashfs \
-e boot
Copy the kernel and initramfs from inside the chroot to the live directory.
cp $HOME/LIVE_BOOT/chroot/boot/vmlinuz-* \
$HOME/LIVE_BOOT/image/vmlinuz && \
cp $HOME/LIVE_BOOT/chroot/boot/initrd.img-* \
$HOME/LIVE_BOOT/image/initrd
Create a menu configuration file for grub.
This config instructs Grub to use the search command to infer which device contains our live environment. This seems like the most portable solution considering the various ways we may write our live environment to bootable media.
cat <<'EOF' >$HOME/LIVE_BOOT/scratch/grub.cfg
insmod all_video
search --set=root --file /ZTP_IMAGE
set default="0"
set timeout=30
menuentry "Debian Live" {
linux /vmlinuz boot=live quiet nomodeset
initrd /initrd
}
EOF
Create a special file in image named DEBIAN_IMAGE
. This file will be used to help Grub figure out which device contains our live filesystem. This file name must be unique and must match the file name in our grub.cfg config.
touch $HOME/LIVE_BOOT/image/DEBIAN_IMAGE
Adding BIOS and UEFI boot support
Create a grub UEFI image.
grub-mkstandalone \
--format=x86_64-efi \
--output=$HOME/LIVE_BOOT/scratch/bootx64.efi \
--locales="" \
--fonts="" \
"boot/grub/grub.cfg=$HOME/LIVE_BOOT/scratch/grub.cfg"
Create a FAT16 UEFI boot disk image containing the EFI bootloader. Note the use of the mmd
and mcopy
commands to copy our UEFI boot loader named bootx64.efi
(cd $HOME/LIVE_BOOT/scratch && \
dd if=/dev/zero of=efiboot.img bs=1M count=10 && \
sudo mkfs.vfat efiboot.img && \
mmd -i efiboot.img efi efi/boot && \
mcopy -i efiboot.img ./bootx64.efi ::efi/boot/
)
Create a grub BIOS image.
grub-mkstandalone \
--format=i386-pc \
--output=$HOME/LIVE_BOOT/scratch/core.img \
--install-modules="linux normal iso9660 biosdisk memdisk search tar ls" \
--modules="linux normal iso9660 biosdisk search" \
--locales="" \
--fonts="" \
"boot/grub/grub.cfg=$HOME/LIVE_BOOT/scratch/grub.cfg"
Use cat
to combine a bootable Grub cdboot.img
bootloader with our boot image.
cat \
/usr/lib/grub/i386-pc/cdboot.img \
$HOME/LIVE_BOOT/scratch/core.img \
> $HOME/LIVE_BOOT/scratch/bios.img
Generate the ISO file.
xorriso \
-as mkisofs \
-iso-level 3 \
-full-iso9660-filenames \
-volid "DEBIAN_IMAGE" \
-eltorito-boot \
boot/grub/bios.img \
-no-emul-boot \
-boot-load-size 4 \
-boot-info-table \
--eltorito-catalog boot/grub/boot.cat \
--grub2-boot-info \
--grub2-mbr /usr/lib/grub/i386-pc/boot_hybrid.img \
-eltorito-alt-boot \
-e EFI/efiboot.img \
-no-emul-boot \
-append_partition 2 0xef ${HOME}/LIVE_BOOT/scratch/efiboot.img \
-output "${HOME}/LIVE_BOOT/custom-debain-distro.iso" \
-graft-points \
"${HOME}/LIVE_BOOT/image" \
/boot/grub/bios.img=$HOME/LIVE_BOOT/scratch/bios.img \
/EFI/efiboot.img=$HOME/LIVE_BOOT/scratch/efiboot.img
Making the raw disk image
We would need a raw disk image to provide to vendors that they dd
as a lot of manufacturers expect the image to be in this format for imaging clients on the factory floor
We already have built an iso file. We can use that to create our raw disk image.
Create a new sparse disk image
sudo dd if=/dev/zero of=custom-debian.img bs=1 count=0 seek=400M
Partition the disk
sudo parted -s custom-debian.img mklabel gpt
sudo parted --script --align=optimal custom-debian.img mkpart ESP fat32 1MiB 100MiB
sudo parted --script --align=optimal custom-debian.img mkpart ext4 100MiB 100%
sudo parted --script custom-debian.img set 1 boot on
# checkout the partitions
sudo parted -s custom-debian.img print
Check which loop device we can use
sudo losetup --show -f custom-debian.img
# example output
#/dev/loop15
Create loop device
sudo losetup -P /dev/loop15 custom-debian.img
Write iso to loop device, which writes to raw disk
sudo dd if=custom-debian-distro.iso of=/dev/loop15
Cleanup the loop device
sudo losetup -d /dev/loop15
now you should have have a bootable raw disk image custom-debian.img
, you can learn more about the disk running this command
fdisk -l custom-debian.img
Some Tips and Tricks if you are building this on an EC2 instance
- You need to run a
sudo apt update && apt upgrade
before installing the build tools on first EC2 provisioning or as the package has not been setup: - You need to provide your own resolv.conf as copying the resolv.conf from the host machine i.e the EC2 instance points to internal ec2 nameservers and wouldn’t work externally.
- A better instance type might help in speed of building the squashfs filesystem. The t2.micro takes about a solid 2mins.
- On an ec2 instance you cannot use the
mkfs.vfat
unless you are root, I updated the command with sudo prepended. A detail to consider. - The debian AMI ec2 instance doesn’t have
parted
command installed. I’ve addedparted
to the prereqs build tools.
References
- https://www.willhaley.com/blog/custom-debian-live-environment-grub-only/ (major thanks to Will Haley for this guide)