Writing Raspberry PI OS Lite 64-bit ISO that is configured for a headless first boot to a SD-card

The Raspberry PI OS Lite ISO needs to be configured to add a username and password, as well as activate ssh to allow a headless PI to be setup over a network.

Is there a better way to do this?

This script does the following:

  • Download a version of Raspberry PI OS Lite 64-bit
  • Download the ISO’s sha256
  • Verify the ISO
  • Mount the ISO inside the qube
  • Get a username and password from the user and create a config file with this info
  • Enable ssh on first boot
  • Dismount ISO
  • Create’s a script to pull the iso from the qube to dom0 and write it to the sdcard
  • Gives the user instructions on what to type in dom0’s terminal to pull the script over
  • The pulled script pulls the iso from the qube to dom0
  • The iso is written to the sdcard
  • Cleanup

set -e


SHA_NAME="$(basename "$RASPBIAN_OS_ISO_SHA256_URL")"

SOURCE_QUBE="$(xenstore-read name)"

if [ "$EUID" -ne 0 ]
  then echo "Please run using sudo"

function section_break() {
    local title=$1
    local line='================================================================================'
    local sqiggle='<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>'
    local line_length=${#line}

    printf '\n'
    printf '\n'
    printf '%s\n' "$sqiggle"

    if [[ -n $title ]]; then
        local padding=$(( (line_length - ${#title}) / 2 ))
        for ((i=1; i<=padding; i++)); do
            printf ' '

        printf '%s\n' "$title"

    printf '%s\n' "$line"
    printf '\n'

function status() {
    echo -e "-> $1"

function highlight() {
    echo -e "\e[30;47m$1\e[0m"

section_break "Fetching the compressed ISO if need be, verifying and decompressing"
status "Downloading Raspbian OS Lite 64 bit if the file does not exist locally:"
aria2c --quiet=true --show-console-readout=true -x4 -s4 -c "$RASPBIAN_OS_ISO_URL"
status "Downloading SHA256 signature for Raspbian OS Lite 64 bit, regardless"
rm -f "$SHA_NAME"
aria2c --quiet=true --show-console-readout=true  "$RASPBIAN_OS_ISO_SHA256_URL"

status "Check SHA256 sum of the compressed ISO file to its downloaded SHA256 signature"
sha256sum --check "$SHA_NAME"
# TODO check that the script fails if the ISO is altered.

status "Deleting the previous payload directory"
rm -rf "./$UTILS_DIR"

status "Decompressing compressed ISO file"
unxz --keep "./$ISO_NAME"

status "Creating and moving into a new directory named 'myUtils' to work in."

mkdir -p "./$UTILS_DIR"


status "Moving iso to transfer directory"

section_break "Mounting ISO as part of the filesystem to allow modifications"

status "Creating loop device"
LOOP_DEVICE="$(losetup --show -f -P "$ISO_FULL_PATH")"

status "Get the first partition (assumed to be the boot partition) of the loop device: $BOOT_PARTITION"

status "Wait for the loop device to be ready"
while ! [ -b "$BOOT_PARTITION" ]; do sleep 1; done

status "Mounting the boot partition"
mkdir -p /mnt/boot
mount "$BOOT_PARTITION" /mnt/boot

status "Get user information to prepare the Pi Files"
# Asking for username and password from user
highlight "Please enter a username for the Raspberry Pi:"

highlight "Please enter a password for the Raspberry Pi:"
read -s PASSWORD
ENCRYPTED_PASSWORD=$(echo "$PASSWORD" | openssl passwd -6 -stdin)

status "Creating user configuration and SSH files in the boot partition"
echo "$USERNAME:$ENCRYPTED_PASSWORD" > /mnt/boot/userconf
touch /mnt/boot/ssh

status "Flushing file system buffers"

status "Unmount the boot partition and delete the loop device"
umount /mnt/boot

# Give system some time to release resources
status "Waiting for kernel to dismount iso"

while fuser -m /mnt/boot > /dev/null 2>&1; do
    sleep 1

# Diagnostic steps before trying to delete /mnt/boot
status "Trying to remove /mnt/boot..."
rm -rf /mnt/boot

if [[ $RM_EXIT_STATUS -ne 0 ]]; then
    status "Removal operation failed. Running diagnostic commands..."
    status "List all mount to /mnt/boot:"
    mount | grep /mnt/boot

status "Finished setup for headless boot with user '$USERNAME'and SSH enabled."

section_break "Transfer scripts and ISO's to dom0 to write the ISO to the sd card"

status "The ISO will now allow the Pi to be accessible via ssh on first boot using the username and password provided."

status "Creating a symlink to the ISO file that will be easier to get to later."
# Delete the symlink if it already exists
rm -f "$ISO_SYMLINK"



status "Creating file '$REMOTE_SCRIPT_NAME' in $UTILS_DIR directory."


if [ "$EUID" -ne 0 ]
  then echo "Please run using sudo"

function status() {
    echo -e "-> $1"

function highlight() {
    echo -e "\e[30;47m$1\e[0m"

status "Fetching ISO file from Qube $SOURCE_QUBE:"

DEVICE_NAME=$(dmesg | awk '/mmcblk[0-9]/{gsub(/:$/, "", $2); print $2}'| tail -1 )
status "The device detected is: /dev/$DEVICE_NAME"

status "Is this the correct device?"
highlight "Enter 'y'/'n', or 'e' to edit the device name: "
read response
if [[ "$response" == "n" ]]; then
    echo "Please confirm the device and rerun the script."
    exit 1
elif [[ "$response" == "e" ]]; then
    echo -n "Enter the device name (e.g., mmcblk0): "
    read DEVICE_NAME
status "Writing the ISO to SD card using:"
status "dd bs=4M if=$LOCAL_ISO_FILE of=/dev/$DEVICE_NAME status=progress"
dd bs=4M if=$LOCAL_ISO_FILE of=/dev/$DEVICE_NAME status=progress

status "Flushing buffers"
status "Image has been written to the SD card."
status "Deleting this file and the ISO file"
rm -f payload.iso
rm -f myTransfer.sh
highlight "Please remove your SD card. It is ready to boot your PI from"
status "This setup assumes you have a DHCP server running."
status "The easiest way to find the IP address of the PI is to check what IP your router assigned to it"



status "Creating symlinks in /tmp to reduce typeing when transferring to dom0"

status "Setting directories in remote script"

status "Setting source qube name in remote script"

section_break "Transfer the script and iso to dom0 with the user's assistance"
echo "In dom0 terminal, type:"
highlight ">  qvm-run --pass-io $(xenstore-read name) 'cat $SCRIPT_SYMLINK' > $REMOTE_SCRIPT_NAME"
highlight "Running this script blindly is a major security vulnerability if you do not understand every line in it."
echo "Open the file with an editor to check the contents. Make sure you understand what everyline does."
highlight ">  vim $REMOTE_SCRIPT_NAME    #vim basics: ':q' to quit ':q!' to quit without saving, 'i' to edit, '<esc>' to stop editing, ':w' to save"
echo "Make the script executable:"
highlight ">  chmod u+x $REMOTE_SCRIPT_NAME"
echo "Then you can run the script:"
highlight ">  sudo ./$REMOTE_SCRIPT_NAME"
echo "The script will copy the required ISO image automatically."

highlight "Press enter to continue when you've finished copying..."

status "Deleting the payload directory $BASE_DIR/$UTILS_DIR "


Is there a reason you can’t just attach the raw block device with the SD card to a VM and do all the work without involving dom0?

That was my initial plan, but I couldn’t figure out how to connect the /mmcblk0 device directly to a qube to be able to dd the iso to it.

What’s wrong with just… assigning the block device or the reader to your VM?

The “Devices” menu should let you reassign any non-used block devices or USB reader gizmos to a VM to write from, and then you dd from in that VM.

Sorry, I’m not sure what you’re trying to work around, really.

Assigning a partition to a VM works fine for me, but assigning a block device wasn’t reliable, sometimes it would work, but more often than not it doesn’t. Moving the iso to dom0 and writing it from there was the only way I was able to reliably access the sdcard to perform a dd.

If mounting a block device to a vm is supposed to work, I must have an error somewhere else that is causing the inconsistancy of the block device. I’m not entirely sure how to track that issue down though.

Did you try from USB qube?
I run into such USB device problems myself, and ordered an extra USB pcie card that i can pass on to an appVM.
Unfortunately this is not something that can be done on notebooks.