The Boot USB Stick · Part 2
Your Own Debian Live Image: Boot It and SSH Straight In
A Debian live image that boots from a USB stick and lets you in over ssh with your keys: building with live-build, the live.local name, pitfalls and security.

Build the image with live-build: kernel and firmware from backports, your authorized_keys for root, sshd enabled at boot and the mDNS name live.local. Boot a machine from the stick and a minute later run ssh root@live.local from your laptop — no monitor or keyboard on the machine itself.
Stick in the port, power on — and you’re at that machine’s console from your own laptop.
Why bother
A stock Debian or Ubuntu live image boots a desktop and waits for someone at the keyboard. To reach it over the network you have to sit down at the machine, install and start an ssh server, set a password and find the address. Your own image does all of that by itself:
- a machine without a monitor — a server, a mini PC, an x86 router: boot from the stick and it’s on the network;
- a broken system — Windows or Linux won’t boot, and the disk needs to be mounted, repaired or copied over the network;
- hardware checks — disks (SMART), memory, network cards, Wi-Fi, before you install a system;
- comfort — working in your own terminal, with your clipboard and history, rather than at someone else’s keyboard.
What goes into the image:
- Debian stable with the kernel and firmware from backports: new network cards and Wi-Fi are recognised where the stable installer doesn’t know them yet;
- an ssh server started at boot; root gets in only with your keys;
- the network name
live.local(mDNS via Avahi): no need to know the IP; - the machine’s addresses on the login screen as soon as DHCP hands them out;
- disk and network tools: mdadm, lvm2, cryptsetup, smartctl, nvme-cli, parted, testdisk, ddrescue, ntfs-3g, rsync, tmux, mc, iftop, tcpdump, mtr, iperf3;
- a three-entry boot menu: a light XFCE desktop (the default, after 5
seconds), a console (less memory, faster) and a console in
nomodesetsafe mode for when the screen stays black. ssh works in all three.
The finished image goes onto the multiboot stick from part one of the series, next to the installers.
What you need to build it
- a computer or virtual machine running Debian (Ubuntu will do too);
- the
live-buildpackage, root and access todeb.debian.org; - about 10 GB free in the working folder, for example in
/var/tmp; - time: about 25 minutes for the first build, about 10 for a rebuild with the package cache;
- your public ssh keys in one
authorized_keysfile.
Below are the key steps of the build script. Each block is part of one bash script run as root from an empty working folder.
Building with live-build
1. The base
DIST=trixie # Debian 13
LIVE_USER=sysadmin # example: use your own
LIVE_PASSWORD=sysadmin # example: be sure to change it
lb config \
--distribution "$DIST" \
--archive-areas "main contrib non-free non-free-firmware" \
--debian-installer none \
--binary-images iso-hybrid \
--bootloaders "grub-efi,syslinux" \
--memtest none \
--bootappend-live "boot=live components hostname=live username=$LIVE_USER locales=en_US.UTF-8 keyboard-layouts=us timezone=Etc/UTC"
grub-efi boots the image in UEFI, syslinux in BIOS, and iso-hybrid
produces a file you can either write to a stick or drop into Ventoy. Set
your own language, keyboard layouts and time zone in --bootappend-live.
2. Kernel and firmware from backports
A="deb http://deb.debian.org/debian $DIST-backports main contrib non-free non-free-firmware"
echo "$A" > config/archives/backports.list.chroot
echo "$A" > config/archives/backports.list.binary
for t in chroot binary; do cat > config/archives/backports.pref.$t <<PREF
Package: linux-image-* linux-headers-* linux-kbuild-* linux-base* linux-binary-* linux-modules-* firmware-* amd64-microcode intel-microcode
Pin: release n=$DIST-backports
Pin-Priority: 990
PREF
done
Without this step the image gets the stable kernel and simply won’t see network cards released after it. Priority 990 applies only to the kernel and firmware: the rest of the system stays stable.
3. Packages
Put your list in a separate file, for example
config/package-lists/custom.list.chroot:
openssh-server avahi-daemon libnss-mdns network-manager sudo
firmware-linux firmware-linux-nonfree firmware-misc-nonfree firmware-realtek firmware-iwlwifi
mdadm lvm2 cryptsetup smartmontools nvme-cli parted gdisk ntfs-3g testdisk gddrescue
rsync tmux htop mc iftop tcpdump mtr-tiny dnsutils iperf3 ethtool lshw
xorg lightdm xfce4 xfce4-terminal network-manager-gnome firefox-esr gparted
4. Keys and ssh
install -d -m 700 config/includes.chroot/root/.ssh config/includes.chroot/etc/skel/.ssh
install -m 600 authorized_keys config/includes.chroot/root/.ssh/authorized_keys
install -m 600 authorized_keys config/includes.chroot/etc/skel/.ssh/authorized_keys
install -d config/includes.chroot/etc/ssh/sshd_config.d
cat > config/includes.chroot/etc/ssh/sshd_config.d/10-live.conf <<'CONF'
PasswordAuthentication yes # the user: key or password
PermitRootLogin prohibit-password # root: key only
CONF
Everything under config/includes.chroot/ lands in the image as is.
Through /etc/skel the keys also reach the user that live-config creates
at boot. If you don’t need password logins over the network, write
PasswordAuthentication no.
5. Hooks: services, host keys and the password
cat > config/hooks/normal/9000-live-ssh.hook.chroot <<'HOOK'
#!/bin/sh
set -e
systemctl enable ssh.service avahi-daemon.service NetworkManager.service
rm -f /etc/ssh/ssh_host_* # host keys are generated anew on every boot
HOOK
# live-config hard-codes the password "live" in its script — swap in the hash of yours
openssl passwd -6 "$LIVE_PASSWORD" > config/includes.chroot/etc/live-password.hash
cat > config/hooks/normal/9010-live-password.hook.chroot <<'HOOK'
#!/bin/sh
set -e
f=/usr/lib/live/config/0030-user-setup
h=$(cat /etc/live-password.hash); rm -f /etc/live-password.hash
sed -i "s|^\(\s*_PASSWORD=\).*|\1'${h}'|" "$f"
grep -qF "$h" "$f"
HOOK
chmod +x config/hooks/normal/*.hook.chroot
install -d config/includes.chroot/etc/sudoers.d
echo "$LIVE_USER ALL=(ALL) NOPASSWD:ALL" > config/includes.chroot/etc/sudoers.d/live-user
chmod 440 config/includes.chroot/etc/sudoers.d/live-user
Hooks run inside the future system during the build. The last line of the password hook is a check: if live-config changes the file format, the build fails instead of shipping an image with the old password.
6. The address on the login screen
install -d config/includes.chroot/etc/NetworkManager/dispatcher.d
cat > config/includes.chroot/etc/NetworkManager/dispatcher.d/90-live-issue <<'DISP'
#!/bin/sh
mkdir -p /run/issue.d
{ echo " ssh root@live.local"; ip -4 -br addr show scope global | awk '{print " "$1" "$3}'; echo; } \
> /run/issue.d/50-live-ip.issue
agetty --reload 2>/dev/null || true
DISP
chmod 755 config/includes.chroot/etc/NetworkManager/dispatcher.d/90-live-issue
When NetworkManager gets an address, the script rewrites the console
greeting, so the addresses appear right on the login screen. Put the same
ip -4 -br addr into /etc/profile.d/ to print them on console login
too.
7. The boot menu
Copy the boot loader templates into config/bootloaders/ and edit them:
cp -r /usr/share/live/build/bootloaders/{grub-pc,syslinux_common,isolinux} config/bootloaders/
sed -i 's/^timeout .*/timeout 50/' config/bootloaders/isolinux/isolinux.cfg # 5 seconds
sed -i 's/^set default=0$/set default=0\nset timeout=5/' config/bootloaders/grub-pc/config.cfg
In config/bootloaders/grub-pc/grub.cfg, three menu entries take the
place of @LINUX_LIVE@:
menuentry "Live — XFCE desktop (ssh on)" {
linux @KERNEL_LIVE@ @APPEND_LIVE@
initrd @INITRD_LIVE@
}
menuentry "Live — console (ssh on)" {
linux @KERNEL_LIVE@ @APPEND_LIVE@ systemd.unit=multi-user.target
initrd @INITRD_LIVE@
}
menuentry "Live — console, safe mode (nomodeset)" {
linux @KERNEL_LIVE@ @APPEND_LIVE@ nomodeset systemd.unit=multi-user.target
initrd @INITRD_LIVE@
}
For BIOS, the same three entries go into
config/bootloaders/syslinux_common/live.cfg.in in syslinux syntax
(label, menu label, linux, initrd, append).
8. Build and check
lb build 2>&1 | tee build.log
for p in live-boot live-config openssh-server; do
chroot chroot dpkg -s "$p" >/dev/null || { echo "$p is missing from the image"; exit 1; }
done
sha256sum *.hybrid.iso
Before putting the image on a stick, test it in QEMU in both BIOS and
UEFI mode (OVMF firmware from the ovmf package), forwarding ssh to port
2222:
qemu-system-x86_64 -m 4G -enable-kvm -cdrom live-image-amd64.hybrid.iso \
-netdev user,id=n0,hostfwd=tcp::2222-:22 -device virtio-net,netdev=n0
# UEFI: add -bios /usr/share/ovmf/OVMF.fd
ssh -p 2222 root@127.0.0.1 # should let you in with your key
live-build pitfalls
live.list.chrootis taken. live-build keeps its own packages (live-boot, live-config) in it. Name your list that way and it overwrites the internal one: the image builds but won’t boot as a live system. Use any other name.- The menu waits forever. The isolinux template has
timeout 0, i.e. no automatic boot, and the grub template has no timeout at all. For a machine without a monitor that means “never boots” — set the timeout explicitly. - Not every placeholder is substituted in
grub.cfg.@KERNEL_LIVE@,@INITRD_LIVE@and@APPEND_LIVE@work, but there are no failsafe variants as in syslinux. Build the grub safe mode yourself:@APPEND_LIVE@plusnomodeset. - live-config turns off password logins for ssh. It writes
PasswordAuthentication noitself. Your own file insshd_config.dis read before the main config and therefore wins. - The password “live” is hard-coded in a live-config script
(
0030-user-setup). The hook from step 5 substitutes yours. \4in/etc/issueshows 127.0.0.1. agetty takes the address when it draws the screen, i.e. before DHCP. That’s why the NetworkManager script from step 6 writes the address.
Using it
Boot the machine from the stick and pick the image in the Ventoy menu.
A minute or two later, from a laptop on the same network:
ssh root@live.localWith two such machines on the network, Avahi names the second one
live-2.local. If the name doesn’t resolve, the address is on the machine’s screen; on the machine itselfip -br ashows it andnmtuisets up Wi-Fi or a static address.The live system gets new host keys on every boot, so ssh will complain that the key changed. To avoid cleaning
known_hostsevery time, give the live system its own block in~/.ssh/config:Host live.local live-*.local User root StrictHostKeyChecking no UserKnownHostsFile /dev/null
The image has all the main disk tools. How to read smartctl output for
an NVMe drive — data written and Percentage Used — is covered in the
guide on SSD wear: it’s about
Macs, but NVMe SMART fields are the same everywhere.
Security
- The default password is just an example. The
sysadmin/sysadminpair above shows where the name and password are set; use different ones in your build. If an image was already built with a simple password, change it right after boot withpasswd, especially on someone else’s network: the user has passwordless sudo, so that password equals root access. - root: keys only. root has no password, and
PermitRootLogin prohibit-passwordwon’t let root in with a password even if one is set. - Host keys are new on every boot. That way the image carries no
private keys shared by every copy. The flip side: ssh can’t tell your
live machine from an impostor. Use it on your own network and don’t
enable agent forwarding (
ssh -A) to the live system. - The image holds only public keys. They aren’t secret, but they
decide who gets in: before building, check that
authorized_keyshas nothing extra.
A case from practice
A Windows 11 laptop stopped booting after a botched disk cleanup. The
folder left over from a previous update was being deleted after taking
ownership of it recursively (takeown and icacls with /r and /t).
Inside were junctions pointing to real system folders, and the commands
followed them: permissions on ProgramData, the default profile and
part of the user’s profile were replaced with “this user only”. The
SYSTEM account lost access, and the services the shell depends on stopped
starting. No files were lost.
The repair was done from the live image over ssh, all of it from a terminal on another computer:
- boot from the stick,
ssh root@live.localfrom another computer; - the NTFS volume was marked dirty, so
ntfsfixfirst, then mounting read-write withntfs-3g; - before any change, a backup of every folder’s current permissions so it could be rolled back;
- permissions on system folders restored by inheritance from the parent, top down, and the special ones per Microsoft’s documentation;
- reboot into Windows,
DISMandsfcchecks — clean.
Fixing this from Windows is hard: it no longer starts, and the recovery environment only offers a blunt permission reset that loses the special settings. The live image with ssh gave a full Linux with access to the disk and the chance to work from one’s own terminal with one’s own scripts.
The lesson for the future: delete leftover Windows update folders
($WINDOWS.~BT, Windows.old) with the built-in Disk Cleanup, not by
changing permissions and deleting by hand.
Next in the series: Windows without a Microsoft account — which edition to put on the stick so that setup gives you an ordinary local account.