<!--
.. title: A Mac, Two Disk Images, and an SSH Prompt: OpenBSD 7.9 on the Raspberry Pi 4B
.. slug: openbsd-7-9-on-the-raspberry-pi-4b
.. date: 2026-09-07 17:06:31 UTC-05:00
.. tags: OpenBSD, OpenBSD 7.9, Raspberry Pi, Raspberry Pi 4, arm64, BSD, QEMU, macOS, U-Boot, firmware, disk images, microSD, SSH, doas, single board computers
.. category: Operating Systems
.. link:
.. description: Building and deploying OpenBSD 7.9 on a Raspberry Pi 4B from an Apple Silicon Mac, with wired DHCP, SSH, and passwordless doas. Includes a generic flashable image download that accepts your own SSH public key before boot.
.. type: text
-->

# A Mac, Two Disk Images, and an SSH Prompt: OpenBSD 7.9 on the Raspberry Pi 4B

I wanted an OpenBSD machine I could plug into the network and immediately start using. Put the card in, connect Ethernet, apply power, find its address, SSH in. An ordinary account with my key, `doas` when I needed root, and enough room on the card that installing software would not immediately become another storage project. A modest list, which is how these things usually begin.

The machine now exists. A Raspberry Pi 4 Model B Rev 1.5, four Cortex-A72 cores and 4 GB of RAM, running OpenBSD 7.9 from microSD. It answers to `rpi4b`, negotiated gigabit Ethernet, and has separate `/usr` and `/home` filesystems with about 28 GiB each. The installation happened inside a virtual machine on my Apple Silicon Mac. By the time the card reached the Pi, the operating system was installed, patched, configured, and waiting for hardware to inhabit.

<figure style="margin: 2rem 0; text-align: center;">
<img src="/images/openbsd-rpi4b/openbsd-7.9-rpi4b-terminal.png" alt="SSH terminal connected to rpi4b showing neofetch: OpenBSD 7.9 arm64, four ARM Cortex-A72 cores, and 3954 MiB of memory." width="1384" height="1050" style="display: block; max-width: 100%; height: auto; margin: 0 auto;">
<figcaption><em>The Pi over SSH, running OpenBSD 7.9. Four Cortex-A72 cores, 12 installed packages, and Puffy taking up most of the terminal.</em></figcaption>
</figure>

**Make your own card:** [Download the generic OpenBSD 7.9 image (525 MiB)](/downloads/openbsd-7.9-rpi4b-generic-20260907/openbsd-7.9-rpi4b-generic.img.gz), with [setup instructions](/downloads/openbsd-7.9-rpi4b-generic-20260907/README.md), [SHA-256 checksums](/downloads/openbsd-7.9-rpi4b-generic-20260907/SHA256), and [firmware licenses and corresponding source](/downloads/openbsd-7.9-rpi4b-generic-20260907/openbsd-7.9-rpi4b-generic-redistribution.tar.gz). This unofficial build accepts your SSH public key before boot. Its VM tests passed; a physical Pi boot test of the generic version is still pending.

The handoff from VM to hardware is the interesting part. A VM can tell you quite a lot about an operating system. It has considerably less to say about whether a Raspberry Pi will find the bootloader you put on a card.

OpenBSD already supports the Pi 4 through its arm64 port. The official installation media includes the Raspberry Pi firmware, so the starting point was conveniently unexotic: `install79.img` from the OpenBSD 7.9 arm64 release, its signed checksum manifest, and the release public key. The [arm64 platform page](https://www.openbsd.org/arm64.html) documents the support and the firmware requirements.

I wanted the installation completed before deployment. An installer image gets you to the place where you can install an operating system; the image I wanted would get me to an SSH prompt. That meant running the installer somewhere, making the configuration decisions there, and preserving its finished disk as the thing I would eventually write to microSD.

The Mac made a good workshop. QEMU ran a generic ARM64 `virt` machine using Apple's Hypervisor Framework acceleration, with four virtual CPUs, 2 GB of RAM, EDK2 UEFI firmware, and virtio disk and network devices. SSH was forwarded to a port on the Mac's loopback interface. I could inspect the console and connect to the guest without giving every intermediate version of the build a presence on the LAN.

Before installation, we verified the release material. The helper checked the Ed25519 signature on the manifest and the SHA-256 digest of the installer. It also compared the release public keys obtained from the official download location and OpenBSD's source repository. The original downloads and verification results stayed in the project directory with the scripts.

There would eventually be several images with very similar names. Keeping a record of which bytes had actually been checked seemed like a better plan than trusting my future ability to remember what “final” meant.

The installer set the hostname to `rpi4b`, created my `alex` account, enabled SSH, and selected `America/Chicago`. We installed the release sets and retained the answer file and customization scripts. OpenBSD then booted as a normal installed guest. At that point, the account, services, and filesystems were all available for testing, and the Pi-specific boot work was still ahead.

QEMU was providing the guest with UEFI firmware. The Pi would need to reach OpenBSD through its own boot chain, using files on the card. Installing under the generic virtual machine did not automatically arrange all of those files for the physical board.

The customization script mounted the target's FAT boot partition and populated it with the Pi firmware, device trees, overlays, and U-Boot supplied by the official installer. It checked for the Pi 4's `bcm2711-rpi-4-b.dtb` and the EFI loader at `efi/boot/bootaa64.efi`. The boot configuration fit in four lines:

```ini
arm_64bit=1
enable_uart=1
dtoverlay=disable-bt
kernel=u-boot.bin
```

We compared the resulting configuration, `start4.elf`, `fixup4.dat`, Pi 4 device tree, U-Boot, Bluetooth-disabling overlay, and EFI loader against the signed installer. All matched. There was a known source for the firmware on this card, and a check that the files had survived the assembly process intact.

The guest could demonstrate that OpenBSD started and accepted a login. The firmware comparison established that the Pi's boot files matched their source. The eventual hardware boot would connect those observations.

The network configuration had a similar wrinkle. QEMU's network card appeared as `vio0`. The Pi's onboard Ethernet appeared as `bse0`, through OpenBSD's [Broadcom GENET driver](https://man.openbsd.org/bse). A perfectly functional DHCP configuration for a virtual network card would be of limited assistance to a physical interface with a different name.

So the image kept its `vio0` configuration for future VM testing and gained an `/etc/hostname.bse0` containing:

```text
inet autoconf
inet6 autoconf
```

IPv4 DHCP and IPv6 autoconfiguration. That was the entire wired-network setup. No static address embedded in an image that might eventually land on another network, and no Wi-Fi credentials to get right before the first login. I would find `rpi4b` in the router's lease table. Automatic `.local` discovery was not configured, so the lease table was part of the intended procedure.

For SSH, I used the same arrangement I wanted to live with afterward. My public key went into `/home/alex/.ssh/authorized_keys`; the private key stayed on the Mac. The server started automatically, with public-key authentication enabled and password authentication, keyboard-interactive authentication, and direct root login disabled.

There is consequently no default password for this image. Someone repeating the build needs to supply their own public key. These are the access choices for my installation, rather than a claim about what every OpenBSD installation does by default.

Root access was one line in `/etc/doas.conf`:

```text
permit nopass alex as root
```

I like how little explanation that line needs. `alex` may become root, and another password prompt is not involved. The [doas manual](https://man.openbsd.org/doas.conf) describes how to restrict rules to particular commands or arguments, but this account is the machine's administrator. Its rule deliberately permits all commands as root. Anyone holding the corresponding login key has that authority.

The test was equally compact:

```sh
doas -n id -u
```

It printed `0`. The `-n` makes it noninteractive, so a successful result establishes that the permission actually works without a prompt. We also checked the running SSH daemon and its effective configuration. A line in a file is an intention; being able to log in and run the command is the behavior I was trying to buy with it.

Then there was the card, and the apparently small request to use the rest of it.

The portable image was 14 GiB: 15,032,385,536 bytes. Its layout included root, swap, separate `/usr` and `/home` filesystems, and a firmware partition. It also retained about 7.1 GiB of unallocated space inside the image. Writing it onto a larger card would reproduce that layout faithfully, including its unused space.

There was no automatic first-boot expansion in this build. The image had exactly the filesystem sizes we gave it. A larger card would not persuade OpenBSD to reconsider them on its own.

I wanted the extra capacity divided between `/usr` and `/home`, where installed software and my own work would have room to accumulate. Root, swap, and the boot area could stay as they were. So we made a second image sized to the actual card: **63,864,569,856 bytes**.

That number matters more than the “64 GB” on the packaging. Cards sold under the same capacity label can differ in their exact size. An image built to the last usable part of one card needs another card at least that large. The portable version is the smaller starting point; the expanded version is the one prepared for this deployment.

Here is what `df -h` reported for the filesystems, with swap and the boot partition included for context:

| Filesystem or area | Portable image | Expanded image |
|---|---:|---:|
| `/` | 1.3 GiB | 1.3 GiB |
| `/usr` | 2.9 GiB | 28.3 GiB |
| `/home` | 1.9 GiB | 27.4 GiB |
| Swap | 256 MiB | 256 MiB |
| Firmware partition | 260 MiB | 260 MiB |

The added space was divided equally. The final totals differ because `/usr` started larger. Filesystem sizes are also slightly smaller than their containing partitions, which explains why multiplying a displayed number back into sectors will not reproduce the disklabel.

The complication was physical ordering. `/home` sat after `/usr`. Giving `/usr` more contiguous space required relocating `/home` first. The friendly phrase “expand both partitions” had turned into a sequence where the order of operations mattered quite a bit.

We did this offline, in an OpenBSD installer VM, with the original image attached read-only alongside the larger target image and installer. The scripts checked the disk capacity, partition offsets, filesystem types, and disk identifier against the expected layout before proceeding. They were intentionally specific to these images. I would rather have a resize script decline a disk it does not recognize than improvise with its contents.

First came extending the OpenBSD area in the outer partition table and enlarging the disklabel boundary. Then `/home` was recreated at its final location, populated from the read-only source, and checked for matching contents and ownership. Only after that copy was verified could `/usr` grow into the space that had become available.

For `/usr`, the script checked the existing filesystem, enlarged its partition, ran a `growfs` dry run, performed the growth, and checked the filesystem again. OpenBSD's [growfs documentation](https://man.openbsd.org/growfs) calls for an unmounted filesystem and a filesystem check afterward. Booting the installer gave us an environment where the target's `/usr` was simply another unmounted disk partition.

The expanded filesystems passed their checks. The disk identifier, root, swap, and firmware area were preserved. A normal boot of the expanded image then confirmed that `/usr` and `/home` mounted where they belonged, at their new sizes.

Root still has about 1.3 GiB, and `/var` lives there in this layout. That is a choice with consequences if the machine later becomes a collector of logs or service data. For the current installation it leaves ample room, but I have not allocated 28 GiB to every directory just by putting a larger card underneath them.

Before packaging, we applied the available updates. The September 7, 2026 build had patches 001 through 014 installed, with no further patches reported by the update check. That statement has a date attached for a reason; anyone deploying later should check the [OpenBSD 7.9 errata](https://www.openbsd.org/errata79.html) and run the update tools again.

We also waited for kernel relinking to finish. Then the image needed to forget the parts of its identity that belonged to the VM: its generated SSH host keys and temporary DHCP lease. The resolver configuration was reset, and the Pi would generate fresh host keys at its own boot. A first-boot `fw_update` would detect firmware needs in the physical environment.

My authorized public key stayed. That key says who may administer the machine. The host keys say which machine has answered the connection. Cloning the first is the point of this personalized image; generating the second during deployment gives the running system its own SSH identity.

Both variants booted independently in QEMU. SSH login worked, `sshd` was running, `doas` returned root, and the effective SSH settings and filesystem mounts matched the intended configuration. Each guest shut down cleanly before packaging. We compressed the raw images with gzip and verified that decompression reproduced the originals, along with checks of the partition table and boot partition.

The compressed files were about 644 MiB and 738 MiB. Their expanded disk sizes were considerably larger, but empty space is cooperative material for a compressor. A SHA-256 manifest recorded the packaged artifacts.

Writing the expanded image from macOS used the usual whole-device procedure:

```sh
diskutil list external physical
diskutil info /dev/diskN
diskutil unmountDisk /dev/diskN
sudo dd if=images/openbsd-7.9-rpi4b-64gb.img of=/dev/rdiskN bs=4m
sync
diskutil eject /dev/diskN
```

`N` must be the verified card identifier, and the target must be large enough. That command replaces the whole device's contents. It takes the raw `.img`; a gzip file needs decompression first. On macOS, Control-T asks `dd` for a progress report, which is useful when the terminal has spent several minutes giving every appearance of having lost interest.

The entire 63,864,569,856-byte write completed. I stopped the subsequent full-card readback before it finished, and the card was safely ejected. The records therefore say exactly that: complete image-package verification, complete card write, incomplete card readback verification. The physical boot that followed supplied different evidence; it did not retroactively checksum the unread part of the card.

With the card in the Pi and Ethernet connected, the machine booted. The router assigned `10.1.1.32` on my `10.1.1.0/24` network, under the hostname `rpi4b`. I connected with:

```sh
ssh -i ~/.ssh/id_ed25519 alex@10.1.1.32
```

OpenBSD 7.9, arm64. The boot log identified the Pi 4 Model B Rev 1.5 and all four Cortex-A72 cores. `rcctl check sshd` returned a running service. `doas -n id -u` printed `0`. `/usr` mounted at 28.3 GiB and `/home` at 27.4 GiB.

`bse0` reported `1000baseT full-duplex`, with the link active. That is a negotiated link speed, not a throughput benchmark. The session established that wired networking and SSH worked on the real board. The expanded image received this physical test; the portable variant had its independent VM validation but was not separately booted on the Pi.

That was the handoff completed: the boot files found the operating system, the operating system found its hardware, and the configuration I had tested on the Mac became the configuration of a machine on the network.

## A download for the next card

An image containing my login key had a rather limited audience. For the downloadable version, we created fresh filesystems, copied the system into them, and removed my account, keys, history, and inherited machine identities. Fresh filesystems also keep deleted personal files out of the free space. Each deployment generates its own SSH host keys.

The generic archive is about **525 MiB**, expanding to **63,864,569,856 bytes**. Check your card's actual capacity; a “64 GB” label is not enough. This version keeps separate `/usr` and `/home` filesystems, reporting about 28.4 GiB and 27.4 GiB after fresh formatting. There is **no automatic expansion** on larger cards.

Download the image and `SHA256` from the links above into one directory. On macOS, verify the download before decompressing it:

```sh
shasum -a 256 --ignore-missing -c SHA256
gzip -dc openbsd-7.9-rpi4b-generic.img.gz > openbsd-7.9-rpi4b-generic.img
```

Continue only if verification reports the image as `OK`. Linux uses `sha256sum --ignore-missing -c SHA256`. The manifest also covers the companion files; missing companions are skipped. Allow roughly 64 GB of free space for decompression.

Flash the resulting `.img` using the whole-card procedure above, substituting the generic filename. Reinsert the card afterward and find its small FAT boot volume. Copy your **public** key into that volume's root directory, replacing `BOOT_VOLUME` with its actual mounted name:

```sh
cp ~/.ssh/id_ed25519.pub /Volumes/BOOT_VOLUME/authorized_keys
```

The private key stays on your computer. Optionally add a plain-text `setup.conf` beside `authorized_keys`:

```ini
username=openbsd
hostname=openbsd-rpi4b
```

Those are the defaults; the timezone is UTC. Safely eject the card, boot the Pi with Ethernet connected, and find its address in the DHCP leases. Then:

```sh
ssh -i ~/.ssh/id_ed25519 openbsd@PI_ADDRESS
doas -s
```

There is **no default password**. SSH stays disabled until a valid public key is supplied, then permits key login for the owner. Direct root SSH login is disabled; `doas` grants root without a password. Missing or malformed setup input can be corrected on the FAT partition and retried at the next boot.

Disposable VM copies passed owner setup, SSH, `doas`, reboot persistence, and installation of a signed package through `pkg_add`. Unrelated keys and root logins were rejected. The [validation record](/downloads/openbsd-7.9-rpi4b-generic-20260907/VALIDATION.json) and [first-boot helper source](/downloads/openbsd-7.9-rpi4b-generic-20260907/openbsd-7.9-rpi4b-generic-firstboot.tar.gz) are included. The generic image retains the boot firmware from my working card, but it still needs its own physical Pi test. The successful hardware session above belongs to the personalized build.

For now, `rpi4b` has an address and an SSH prompt. I can install software, copy a project over, or become root without reaching for a monitor. The card arrived with the installation work already behind it, which was the whole ambition when this was still a modest list.
