HardStacked
Share:

cloud-init · netplan

Raspberry Pi headless Wi-Fi without Imager on Trixie: wpa_supplicant.conf is ignored since Bookworm (and the radio stays blocked)

Published · HardStacked · 9 min

You wrote the Raspberry Pi OS image to the card with dd. You put a wpa_supplicant.conf in the boot partition, an empty file called ssh next to it, and powered up a Pi Zero 2 W with no screen and no keyboard. Nothing shows up on your network. The official documentation says the wpa_supplicant.conf trick "isn't available from Raspberry Pi OS Bookworm and onwards" and sends you to Imager. The most voted answer on Stack Exchange sends you to custom.toml. Neither tells you what to put on the card on Trixie.

Someone asked it on Raspberry Pi Stack Exchange on 2026-02-11 (question 154041, Pi Zero 2 W, Trixie, 270 views, no answer):

"How can I connect to a WIFI network at boot so I can ssh to the thing and configure it?"

The older question 144850, "Headless Wi-Fi setup without the RPi Imager: bootfs/wpa_supplicant.conf not supported since Bookworm" (2023-11-02), has 18,227 views and no accepted answer. Both were read through the Stack Exchange API on 2026-10-20.

01. The verdict

On Trixie, the Wi-Fi of the first boot comes from cloud-init. The file is called network-config (netplan format) and it sits in the boot partition. It is already on your card, entirely commented out. wpa_supplicant.conf is read by nothing, and nothing tells you so.

There is a second trap behind the first one. The Wi-Fi radio is born blocked (rfkill). On our bench, the only path that unblocked it was the cloud-init one. Fix the file without the radio, or the radio without the file, and you are still offline.

02. The bench

Measured on 2026-10-20 on a Pi Zero 2 W. Official image 2026-10-06-raspios-trixie-arm64-lite (sha256 checked), written with dd each time over the whole card, so every pass started from a freshly written image. Journal made persistent from the first boot so we could read it afterwards. Two passes, one difference: pass 1 has a network-config the way Imager writes it, pass 2 has the old wpa_supplicant.conf (and the stock, fully commented network-config, identical to the image's, checked with cmp).

Same image, same user-data, same cmdline.txt (country CA), same network, same password. The Pi boots both times: first boot completes, and cloud-init applies the hostname.

Pass 1: network-configPass 2: wpa_supplicant.conf
Wi-Fi file readyes, a NetworkManager profile is created0 mention in the journal, nothing copied to /etc, 0 profile
lines "unblock set for type wifi"90
radiounblocked"Wi-Fi disabled by radio killswitch; disabled by state file"
wlan0activatedstays unavailable

Pass 1 was run a second time, on the same card written again from scratch: same result, 9 unblock lines again. The counts for pass 2 were computed with grep on the saved journal, read from the card on another machine, not typed on the Pi.

03. Light one: the file is ignored in silence

We read the two official images (Trixie and Bookworm, same date) without booting them. A grep for wpa_supplicant.conf through etc, usr/lib, usr/bin and usr/sbin of both root filesystems returns one file, the same in both: /usr/lib/raspi-config/init_resize.sh. No cmdline.txt calls it:

cmdline.txt as shipped: Trixie, then Bookworm
console=serial0,115200 console=tty1 root=PARTUUID=27b94958-02 rootfstype=ext4 fsck.repair=yes rootwait resize console=serial0,115200 console=tty1 root=PARTUUID=9d05ddda-02 rootfstype=ext4 fsck.repair=yes rootwait quiet init=/usr/lib/raspberrypi-sys-mods/firstboot

First line Trixie, second Bookworm. Then pass 2 confirmed it live: the file stays in the boot partition, untouched, and the journal never mentions it.

The documentation says the feature is gone, in one sentence (raspberrypi/documentation, commit ddea98e, 2026-10-13, getting-started/setting-up.adoc, line 405). It names neither cloud-init nor network-config. Searching configuration/ and getting-started/ for cloud-init, network-config or custom.toml returns 0 files.

04. Light two: the radio is born blocked

The image ships /etc/modprobe.d/rfkill_default.conf, identical on Bookworm and Trixie:

Trixie, on the Pi
$ cat /etc/modprobe.d/rfkill_default.conf
options rfkill default_state=0

Every radio starts soft-blocked. The only warning we found in the image is /etc/profile.d/wifi-check.sh. Two things make it easy to miss. It runs when a session opens, so on a headless Pi nobody ever sees it. And it stays quiet as soon as cfg80211.ieee80211_regdom= is in cmdline.txt:

/etc/profile.d/wifi-check.sh, excerpt
if grep -s -q 'cfg80211\.ieee80211_regdom=' /boot/firmware/cmdline.txt; then exit 0 fi

In pass 2 that parameter was in cmdline.txt, and the radio stayed blocked anyway. By the logic of the script the warning would not have appeared, but we had no console session open, so that part is deduced from the script, not observed.

What pass 1 shows is the other direction. In the first boot, the journal logs "unblock set for type wifi" nine times. The first one comes 25.5 seconds in, inside the cloud-init first-boot run (cloud-init-main.service), the next eight between 26 and 53 seconds. We did not establish which piece of the chain asks for it, so we won't name one. What we can say: the first-boot chain of cloud-init unblocks the radio when network-config declares a Wi-Fi, and in pass 2, where network-config declared no Wi-Fi, nothing did. The two first lines of pass 1, copied from the journal (journalctl -o short-monotonic):

journal, pass 1, first two lines
[ 25.568420] raspberrypi rfkill[832]: unblock set for type wifi [ 26.451132] raspberrypi rfkill[906]: unblock set for type wifi

So a tutorial that says "put wpa_supplicant.conf back" fixes one of two problems, and a tutorial that says "run rfkill unblock" fixes the other.

05. Two more lights

The user-data shipped on the card says it sets up "an initial user called "ubuntu" with password "ubuntu"". That is generic cloud-init text. The image's own /etc/cloud/cloud.cfg creates a user called pi with lock_passwd: True:

/etc/cloud/cloud.cfg, lines 76 to 78
default_user: name: pi lock_passwd: True

(lines 76 to 78 of the file)

No usable password. Without your own user and SSH key in user-data you will not log in. (Since April 2022 there is no default user you can use, as one answer of question 144850 reminds, pointing to the Raspberry Pi announcement of April 2022.)

And the most voted answer, custom.toml, went through firstboot and init_config from the raspberrypi-sys-mods package. The Bookworm image has them (version 20250930~bookworm). The Trixie image (1:20260914) has neither, and its cmdline.txt has no init= anymore. We read that from the image without booting it, so we say "nothing in the image reads it anymore", not "we saw it fail". The answer itself said, in 2023, that the feature was experimental and "will be replaced by cloud-init in the future".

06. The method that works

These commands are provided as is, without warranty. Test on a non critical machine or a snapshot first.

Tested twice (pass 1, then the same card written again from scratch). Write the image, and check the device name first with lsblk, because dd does not ask twice:

write the image
xz -dc 2026-10-06-raspios-trixie-arm64-lite.img.xz | sudo dd of=/dev/sdX bs=4M conv=fsync status=progress

Mount the boot partition (the FAT one) and replace network-config and user-data with the following. Change only the example values.

network-config, boot partition
network: version: 2 wifis: wlan0: dhcp4: true regulatory-domain: "CA" access-points: "MyWiFi": password: "correct horse battery staple" optional: true
user-data, boot partition
#cloud-config manage_resolv_conf: false hostname: pizero manage_etc_hosts: true user: name: admin shell: /bin/bash lock_passwd: true ssh_authorized_keys: - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...yourkey you@laptop" sudo: ALL=(ALL) NOPASSWD:ALL ssh_pwauth: false runcmd: - [ systemctl, enable, --now, ssh ]

Then add this at the end of the single line of cmdline.txt (same line, one space before it). Imager does the same when you set a country; we did not test without it:

end of the cmdline.txt line
cfg80211.ieee80211_regdom=CA

YAML wants spaces, never tabs (the shipped file says so itself). The network name goes in quotes. regulatory-domain is your country code. For SSH, our test enables it with the runcmd line. An empty file called ssh in the boot partition should also work: sshswitch.service is enabled in the image and reads it. We did not test that.

Current Imager writes this same format: password: at the access point level, regulatory-domain, the country in cmdline.txt (rpi-imager, commit 4cf6217, 2026-10-20: the network-config is built in src/customization_generator.cpp, and src/imagewriter.cpp appends cfg80211.ieee80211_regdom= to cmdline.txt whenever a country is set, with or without a network name). It adds an ethernets: eth0 block that our test did not have.

07. Check it

After the first boot, on the Pi (cloud-init 25.2):

after the first boot, on the Pi
$ cloud-init status
status: done
$ journalctl -b -o cat | grep -c "unblock set for type wifi"
9
$ /usr/sbin/rfkill list wlan
1: phy0: Wireless LAN Soft blocked: no Hard blocked: no
$ /usr/sbin/iw reg get | head -2
global country CA: DFS-FCC

And the NetworkManager side (some lines cut):

NetworkManager side, some lines cut
$ journalctl -b -o cat | grep -E "rfkill" | head -12
<info> [1791246501.5789] rfkill1: found Wi-Fi radio killswitch (at /sys/devices/platform/soc/3f300000.mmcnr/mmc_host/mmc1/mmc1:0001/mmc1:0001:1/ieee80211/phy0/rfkill1) (driver brcmfmac) <info> [1791246502.1493] manager: rfkill: Wi-Fi enabled by radio killswitch; enabled by state file

The full paths to rfkill and iw are not a style choice: in our SSH session on the Pi the PATH had no /usr/sbin, so plain rfkill and iw answered "command not found", though both packages are installed.

08. WPA2, WPA3

The password: line is a netplan shortcut that means WPA-PSK with PMF optional (netplan 1.1.2, src/parse.c). NetworkManager 1.52.1 translates wpa-psk as a transition mode: it adds SAE (WPA3) when the Wi-Fi allows it. Its own code comment says "key-mgmt=wpa-psk doesn't strictly mean WPA1/WPA2 only, but also allows WPA3 (SAE)" (nm-supplicant-config.c).

Now what we saw. On a network that was WPA3 only with PMF required, our Zero 2 W never joined. The journals we could read come from boots where we had forced the profile to SAE: "Authentication timed out" three times, then NetworkManager concluded wrong password ("no-secrets"), though the password was right. On a WPA2 network it connected the first time. We did not separate SAE from required PMF. The practical advice: if your network is WPA3 only, test the Zero 2 W on a WPA2 or transition network before you decide your password is wrong.

09. The password sits in clear text

network-config lives on a FAT partition anyone holding the card can read, and cloud-init does not erase it. After the first boot:

boot partition after the first boot
$ ls /boot/firmware | grep -E "network-config|user-data|meta-data|wpa_supplicant|^ssh"
meta-data network-config user-data

The password was also copied into /var/log/cloud-init.log on our card. Remove it from both once the Pi is online. The sudo NOPASSWD in the user-data above is the default of the pi user in the image, so harden it if the Pi stays on.

10. What we measured, and what we did not

Measured: one Pi Zero 2 W, Trixie Lite arm64 image of 2026-10-06, cloud-init 25.2, NetworkManager 1.52.1, netplan 1.1.2. Read without booting: the Bookworm image of the same day. Not measured: Pi 4 or 5, a booted Bookworm, the Desktop image, a hidden network, Ethernet.

The same shape runs through the rest of this series, where the system declares one thing and does another: a timedatectl that shows NTP service: active while the clock never got the time, an update-ca-certificates that prints 0 added, 0 removed whether your certificate was taken or ignored, and a systemctl restart sshd that says the unit doesn't exist while SSH keeps answering.

Share: