Skip to main content
mfen.de

Sep 16, 2024 · infra · 6 min read

Headless Fedora on Raspberry Pi: The Hard Way

So, you’re sitting there with a Raspberry Pi, an SD card, and a MacBook, and you’re ready to install Fedora, but there’s a catch: no keyboard, no mouse, no monitor.

Let’s do it the hard (but satisfying) way: headless from first boot, SSH only, using Podman as a small Linux toolbelt on macOS.

Version note: the Fedora URLs and exact image names in this post will drift over time. If the curl commands 404, keep the shape and replace the release/version bits by browsing the Fedora Server aarch64 image directory (or by starting from Fedora’s main download page and navigating to Server → aarch64 → images).

Before you start: assumptions and common failures

This post is intentionally “the hard way”, but I don’t want it to be “the mysterious way”.

  • Podman on macOS runs inside a Linux VM. You generally won’t get direct access to your physical SD card from inside the container.
  • This guide writes to a disk image (fedora.img) inside the VM and then flashes that file from macOS.
  • The fragile part is loop devices. If you hit missing /dev/loop0 or losetup permission/device errors, you may save hours by doing the image prep on a real Linux box or a Linux VM with SD card passthrough.
  • raspberrypi.local assumes mDNS works. It’s convenient, but not guaranteed. Know how to find the IP via your router/DHCP leases.

Step 1: The MacBook + Podman Environment

macOS doesn’t natively support some of the tools needed for flashing Fedora images, so we’ll use a Fedora container to handle the heavy lifting.

mkdir -p $HOME/fedora-for-raspi && cd $HOME/fedora-for-raspi
curl -L -O https://download.fedoraproject.org/pub/fedora/linux/releases/40/Server/aarch64/images/Fedora-Server-40-1.14.aarch64.raw.xz
curl -L -O https://download.fedoraproject.org/pub/fedora/linux/releases/40/Server/aarch64/images/Fedora-Server-40-1.14-aarch64-CHECKSUM

# Fire up a privileged Fedora container
podman run --rm -it --privileged -v "$HOME/fedora-for-raspi:/fedora-for-raspi" fedora:latest bash

Inside the container, install the necessary tools:

dnf install -y arm-image-installer
cd /fedora-for-raspi

Step 1.1: Checksum / integrity (do this once)

You’re about to write a raw OS image to storage. Verify it.

Fedora’s *CHECKSUM files often contain multiple entries (and a signature block), so I like to verify only the one file I actually downloaded:

grep 'Fedora-Server-40-1.14.aarch64.raw.xz' Fedora-Server-40-1.14-aarch64-CHECKSUM | sha256sum -c -

Expect output like ...: OK.

If you prefer doing this on macOS instead of inside the container, compute the hash with shasum -a 256 Fedora-Server-40-1.14.aarch64.raw.xz and compare it to the value in the *CHECKSUM file.

Step 2: Preparing the Image

Create a disk image file and attach it to a loop device:

# Pick a size that matches (or fits within) your SD card so you can grow the root FS later.
# Example: 16G image for a 16GB+ card.
truncate -s 16G /fedora-for-raspi/fedora.img
LOOP_DEV=$(losetup -f --show /fedora-for-raspi/fedora.img)
echo "Using loop device: $LOOP_DEV"

If losetup fails with “cannot find an unused loop device” or you don’t have /dev/loop* at all, you’ve hit the macOS + VM friction. At that point, doing the image-prep steps on a real Linux host is usually faster than debugging device plumbing.

One quick thing worth trying in a VM/container before you give up:

modprobe loop 2>/dev/null || true
ls -l /dev/loop-control /dev/loop0 2>/dev/null || true

Step 3: Headless Configuration (SSH & Wi-Fi)

To make it headless, we must bake our SSH keys and Wi-Fi credentials into the image before the first boot.

Security note: this writes your Wi-Fi PSK into the image in plaintext (root-readable), and you now also have a file-backed image sitting on your laptop. Don’t do this on a shared machine or in an untrusted environment. Prefer Ethernet for first boot, use a guest network, or rotate the Wi-Fi password afterwards.

  1. SSH Key: Add your id_ed25519.pub content to /fedora-for-raspi/authorized_keys.

  2. Flash:

    arm-image-installer writes the Fedora ARM image onto the --media block device. We’re pointing it at a loop device backed by a file so we can mount and modify partitions before flashing to the real SD card.

    arm-image-installer --image=/fedora-for-raspi/Fedora-Server-40-1.14.aarch64.raw.xz \
      --media="$LOOP_DEV" --addkey=/fedora-for-raspi/authorized_keys --relabel --target=rpi4

    --target=rpi4 is correct for a Pi 4 / CM4. Adjust --target for your model; check arm-image-installer --help for supported targets.

    If your arm-image-installer build doesn’t accept .xz directly, decompress first (xz -d ...raw.xz) and pass the .raw file instead.

    Re-attach the loop device with partition mappings so lsblk can see the new partition table:

    losetup -d "$LOOP_DEV"
    LOOP_DEV=$(losetup -Pf --show /fedora-for-raspi/fedora.img)
    lsblk -f "$LOOP_DEV"
  3. Wi-Fi: Mount the root filesystem and create a NetworkManager connection file under /etc/NetworkManager/system-connections/YOUR_SSID.nmconnection.

    First, list partitions. You’ll mount the one that contains /etc/os-release:

    lsblk -f "$LOOP_DEV"

    Then mount it (replace ROOT_PART with a candidate partition path shown by lsblk). If cat /etc/os-release fails, you mounted the wrong partition: unmount and try another one.

    If you see LVM2_member (or no filesystem type at all), you’ve got an LVM layout. You’ll need partition mapping (kpartx) and LVM activation (lvm vgchange -ay) before you can mount a logical volume, and this guide stops being “quick”.

    mkdir -p /mnt/fedora-root
    mount ROOT_PART /mnt/fedora-root
    cat /mnt/fedora-root/etc/os-release

    Create a minimal NetworkManager keyfile:

    Avoid pasting Wi-Fi secrets into your shell history. One option is to type them via read -s and write them into the file without echoing:

    read -r -p "SSID: " WIFI_SSID
    read -r -s -p "Password: " WIFI_PASSWORD
    echo
    cat > "/mnt/fedora-root/etc/NetworkManager/system-connections/${WIFI_SSID}.nmconnection" <<EOF
    [connection]
    id=${WIFI_SSID}
    type=wifi
    
    [wifi]
    mode=infrastructure
    ssid=${WIFI_SSID}
    
    [wifi-security]
    key-mgmt=wpa-psk
    psk=${WIFI_PASSWORD}
    
    [ipv4]
    method=auto
    
    [ipv6]
    method=auto
    EOF

    Now lock down permissions and clear the password variable:

    chown root:root "/mnt/fedora-root/etc/NetworkManager/system-connections/${WIFI_SSID}.nmconnection"
    chmod 600 "/mnt/fedora-root/etc/NetworkManager/system-connections/${WIFI_SSID}.nmconnection"
    unset WIFI_PASSWORD

    NetworkManager can ignore these files if permissions/ownership are too loose, so don’t skip the chmod 600.

    While you’re here, sanity check which user got the SSH key. Root login is often locked; many Fedora images use a default user (commonly fedora) with key-based SSH.

    find /mnt/fedora-root -maxdepth 4 -path "*/.ssh/authorized_keys" -print

    Finally:

    umount /mnt/fedora-root
    losetup -d "$LOOP_DEV"

Step 4: First Boot

Back on macOS, flash the image file to your physical SD card.

This overwrites the entire card. Triple-check the target disk.

diskutil list
diskutil unmountDisk /dev/diskN
sudo dd if="$HOME/fedora-for-raspi/fedora.img" of=/dev/rdiskN bs=4m status=progress
sudo sync
diskutil eject /dev/diskN

Notes:

  • This dd invocation is macOS/BSD-specific. Linux dd flags and device names differ.
  • status=progress works on modern macOS (BSD dd). If your version doesn’t support it, drop the flag and run sudo pkill -INFO dd in another terminal to print stats.
  • A quick sanity check after re-inserting the card is diskutil list /dev/diskN to see the new partitions.

Boot the Pi. Then SSH in (the user and hostname depend on the image and what --addkey configured):

ssh fedora@raspberrypi.local

If mDNS doesn’t resolve .local, find the IP via your router/DHCP leases and use ssh fedora@<ip> instead.

Once in, don’t forget to:

  • Check what the root partition and filesystem actually are (lsblk -f). Fedora images vary. If the root FS is XFS, you’ll end up in xfs_growfs land; if it’s ext4, it’s resize2fs.
  • Set your hostname and timezone.
  • Run dnf update.

Headless setup complete.