Repository navigation
Conversation
… into /var Bringing up the Pi 5's onboard Wi-Fi on Avocado OS has two traps that a first attempt runs into. /etc on a running device is an overlay whose writable layer lives in /run, so a wpa_supplicant config written there disappears on reboot and the stock unit, which reads /etc, cannot be used. And networkd starts before the extension merge, so on a cold boot it never sees the extension's wlan0 file and leaves the interface unmanaged. The reference ships the radio, wpa_supplicant and a unit that reads its config from /var/lib/wifi-station, staying inactive until provisioning writes it. The unit asks networkd to reload once it starts, which is the first point that runs after the merge. The credentials never enter the image or an extension: scripts/provision-wifi.sh derives the WPA key on the provisioning host, because wpa_passphrase needs a terminal and prints nothing over ssh, and sends only the key to a 0600 file in a 0700 directory. A reference users copy should not teach passphrases in images. Tested on a Pi 5 running the 2026 next feed: the unit joins a WPA2-PSK network, the key appears in no process argument, journal, /run, /etc or /usr, and the device rejoins after a cold reboot without help. Signed-off-by: Javier Tia <javier@peridio.com>
Avocado References — Build Check
Totals: 1 ✅ passed · 0 ❌ failed Per-reference logs: workflow run |
| # only after the merge, which makes it the first point that can ask networkd to | ||
| # re-read its configuration. The leading "-" keeps a reload failure from | ||
| # failing the Wi-Fi unit. | ||
| ExecStartPost=-/usr/bin/networkctl reload |
There was a problem hiding this comment.
The extension on_merge hook is a better place for this reload than ExecStartPost. The comment above says this unit is the first point after the merge, but avocadoctl runs on_merge commands after every merge. rubicon/avocado.yaml already runs networkctl reload there.
How it can occur: with ExecStartPost, networkd reloads only when wifi-station.service starts. A user changes 25-wlan0.network (for example a new route metric or a static IP) and runs avocado deploy. The unit is already running, so it does not restart and networkd keeps the old file.
Impact: the change has no effect until a reboot, and the user thinks the deploy did not work. Users who copy this pattern for an interface without a creds-gated unit get no reload at all.
Suggested fix, in the wifi-station extension in avocado.yaml:
on_merge:
- networkctl reload
- systemctl restart --no-block wifi-station.service
on_unmerge:
- systemctl stop --no-block wifi-station.serviceThen remove ExecStartPost and its comment. The on_unmerge line also stops wpa_supplicant when the extension is removed from the runtime. Without it, the process continues to run from a deleted /usr until reboot.
There was a problem hiding this comment.
Agreed on the deploy case: with ExecStartPost alone a changed 25-wlan0.network waits for a reboot. I added an on_merge hook that restarts wifi-station.service and an on_unmerge hook that stops it, so a deploy restarts the unit and the reload runs again.
I kept networkctl reload in ExecStartPost instead of moving it to on_merge. On a cold boot with credentials stored, the merge hook ran before D-Bus was up and the journal shows Warning: Command 'networkctl reload' failed: Failed to connect to system bus: Connection refused. wlan0 then stayed unmanaged. With the reload in the unit, a cold boot gives Network File: /usr/lib/systemd/network/25-wlan0.network and wifi-station active. Fixed in 4b8dfe6.
| enable_services: | ||
| - wifi-station.service | ||
| packages: | ||
| kernel-module-brcmfmac: "*" |
There was a problem hiding this comment.
This extension ships the brcmfmac modules but has no modprobe: list. avocadoctl runs depmod after the merge, but it loads only the modules listed in modprobe:. udev coldplug for the SDIO radio runs before the merge, so no process loads brcmfmac from this sysext. rubicon/avocado.yaml sets modprobe: [brcmfmac] for this reason.
The cold-boot test passed, so the module probably comes from the BSP extension or the rootfs. That means one of two things:
- These packages are not necessary, and two copies of the module can get out of sync after a kernel or BSP update.
- The load order works by chance.
Impact if the load order works by chance: after a feed update changes the order, wlan0 does not appear and wifi-station.service waits on the device until the job times out.
Suggested fix: run modinfo -n brcmfmac and lsmod on the device to find which file loads. Then add this to the extension:
modprobe:
- brcmfmacOr, if the BSP already ships the module and the firmware, remove the kernel-module-* and linux-firmware-rpidistro-* packages here.
There was a problem hiding this comment.
The module comes from the BSP extension, so I removed the kernel-module-*, linux-firmware-rpidistro-* and wireless-regdb-static packages here. avocado-bsp already carries them and loads them with a udev trigger at merge. The extension keeps wpa-supplicant and iw, and a comment says where the radio comes from. After the change, brcmfmac loaded on a cold boot and wlan0 appeared. Fixed in 4b8dfe6.
| # any character is safe. | ||
| conf=$(python3 -I - "$IN" <<'PY' | ||
| import hashlib, sys | ||
| lines = open(sys.argv[1], encoding="utf-8").read().split("\n") |
There was a problem hiding this comment.
split("\n") keeps \r and trailing spaces.
How it can occur: the user saves wifi.txt with CRLF line endings (a Windows editor, or a file copied from a Windows share), or adds a trailing space.
Impact: \r becomes part of the SSID and the passphrase, so the derived key is wrong. The shape check on the device still prints psk lines of 64 hex: 1, so the script reports success. The 4-way handshake then fails, and nothing tells the user the cause.
Suggested fix:
lines = open(sys.argv[1], encoding="utf-8").read().splitlines()
...
if len(ssid) > 32:
sys.exit("SSID must be 32 bytes or less")
print("SSID: " + repr(lines[0]), file=sys.stderr)splitlines() removes the \r. Printing repr() of the SSID lets the user see the exact value that the script used. The 32-byte check rejects an SSID that the AP cannot have.
There was a problem hiding this comment.
I could not reproduce \r reaching the key. The script reads the file in text mode, where Python's universal newlines turn \r\n into \n, and the new test tests/test-provision-wifi.sh checks a CRLF input. I did not switch to splitlines(): it also splits on form feed and other separators, which would cut a passphrase that contains one. The script now prints the SSID it stored, so a trailing space shows up in the output, and it rejects an SSID over 32 bytes. Trailing spaces are kept on purpose, since a passphrase can end in one.
The extension repeated the radio's kernel modules, firmware and regulatory database that the BSP extension already ships and loads, so a version bump had to be made in two places. It also never restarted wifi-station on a deploy, so a changed network file waited for a reboot. Drop the radio packages and keep wpa-supplicant and iw. Add on_merge and on_unmerge hooks for the unit. The networkd reload stays in the unit's ExecStartPost: at a cold boot the merge hook runs before D-Bus is up and `networkctl reload` fails there with "Failed to connect to system bus", which left wlan0 unmanaged. provision-wifi.sh now rejects an SSID over 32 bytes, prints the SSID it stored so a typo is visible, and keeps a passphrase containing a form feed intact by splitting on newlines only. tests/test-provision-wifi.sh covers these cases and the file ownership and modes. Signed-off-by: Javier Tia <javier@peridio.com>
Problem
Bringing up the Pi 5's onboard Wi-Fi on Avocado OS runs into two traps, and nothing in the references shows how to avoid them:
/etcon a running device is an overlay whose writable layer is in/run, so awpa_supplicantconfig written there is gone after a reboot, and the stock unit (which reads/etc/wpa_supplicant) cannot be used.systemd-networkdstarts before the extension merge, so on a cold boot it never sees the extension'swlan0network file and leaves the interface unmanaged.There is also no example of keeping network credentials out of the image and out of extensions.
Change
Adds the
pi5-provisioned-wifireference:wifi-stationextension with the Broadcom drivers and firmware,wpa_supplicant,iw, a networkd file forwlan0(DHCP, route metric 200) andwifi-station.service. The unit reads/var/lib/wifi-station/wpa_supplicant.confand stays inactive when the file is absent. It runsnetworkctl reloadafter it starts, which is the first point after the merge.scripts/provision-wifi.sh, which derives the WPA key on the provisioning host and sends only that key and the SSID over SSH into a0600file in a0700directory. The passphrase never reaches the device.wpa_passphraseis not used because it needs a terminal on stdin and printed nothing over SSH.getting_started.mdwith install, build, deploy, provisioning, verification, how to change the credentials, and what the protection does not cover: anyone who can read the device's storage can read the key, since encrypted/varis not enabled and the Pi 5 has no TPM.Testing
On a Pi 5 running the 2026
nextfeed (kernel 6.12.87, avocado-cli 1.0.0-rc.4), built and deployed withavocado buildandavocado deploy:network blocks: 1 psk lines of 64 hex: 1 quoted psk lines: 0) and leftroot:root 700on the directory androot:root 600on the file.wpa_state=COMPLETEDon a WPA2-PSK network,wlan0routable by DHCP, and 3 of 3 pings to 1.1.1.1 throughwlan0with 0% loss.wlan0was routable, and the same 3 of 3 pings succeeded. An earlier build of the same extension without thenetworkctl reloadline leftwlan0unmanaged on a cold boot./run,/etcand/usr. The only copy is the file in/var.IEEE/password).Not tested: WPA3-only networks, hidden networks, and a country code. A Pi 5 outside this setup would need its own regulatory domain; this reference leaves the world default.
avocado.lockis in the reference's.gitignore, as no other reference tracks it.