nixpi-firmware-provision
Provision the nixpi Raspberry Pi 4 SD card via the FAT FIRMWARE partition — the Cloudflare connector token and Wi-Fi credentials that a fresh flash needs. Use when asked to "reflash nixpi", "flash the
SKILL.md.claude/skills/nixpi-firmware-provision/SKILL.md
GitHubnixpi firmware provisioning
nixpi reads two operator-planted files off the SD card's FAT FIRMWARE partition at
boot (local.firmwareProvisioning, the in-tree
modules/features/firmware-secrets/ capsule),
copying each into a root-only /run file before its consumer starts:
Planted file (/boot/firmware/…) |
→ /run/… |
Consumer |
|---|---|---|
cloudflared-token (TUNNEL_TOKEN=…) |
cloudflared-token |
cloudflared-connector |
wpa_supplicant.conf (country= + network={}) |
wpa_supplicant-firmware.conf |
supplicant-wlan0 (upstream networking.supplicant) |
Why not agenix (read this first)
agenix encrypts to nixpi's SSH host key, but a NixOS SD image ships none, so every
fresh flash mints a new random host key — after which the agenix ciphertext no longer
decrypts, the tunnel never comes up, and (the tunnel being the only remote path in)
there is no way back in. The FAT partition is the one thing macOS can write, so
host-key-independent secrets live there. secrets/cloudflared-token.age stays the
operator-only vault of the token; the operator decrypts it to plant, the Pi never
decrypts it on-device.
The tools (all macOS flake apps)
nix run .#nixpi-flash -- --disk /dev/diskN [--release | --image FILE.img.zst] [--ssid NAME | --wifi-conf FILE]— fresh reflash: acquire the image → verifieddd(byte-count checked) → auto-plant token + Wi-Fi. Image source, in precedence:--image(local file);--release(download the latest CI-built image off theinstaller-latestGitHub release — needsghauth, but no Nix build and no aarch64-linux builder, so this is the Mac path); default =nix buildthe sdImage (Cachix-warms the kernel, but the final assembly still needs an aarch64-linux builder). Wi-Fi is auto-detected unless you pass--ssid/--wifi-conf(needed on a band-split network — see gotchas).nix run .#nixpi-provision [--all|--token|--wifi]— plant onto an already-mounted card.nix run .#nixpi-wifi-creds [--ssid S [--psk P]]... [--country CC]— emit awpa_supplicant.conffrom this Mac's current Wi-Fi (SSID + keychain PSK + locale country).--ssidis repeatable: each one opens anetwork={}block, the--pskafter it belongs to it, and the blocks are ranked by a descendingpriority=— first is the AP nixpi prefers, the rest are fallbacks it drops to when that one is gone. A non-ASCII SSID (an emoji one is real) is emitted as a hexssid=plus a decoded comment, because a quoted UTF-8 literal has to survive the pipe, FAT and wpa_supplicant's parser intact.nix run .#nixpi-vault-token— re-encrypt a new token (stdin/$TUNNEL_TOKEN) into the vault.
Run them from the repo root (they read the vault at secrets/cloudflared-token.age).
Scenarios
- A card new to nixpi → f3-test the WHOLE card first (runbook §3c). A verified flash covers only the image's ~4.9 GB; a fake-capacity card passes it, then corrupts once writes land past its real edge (2026-10-17: "32 GB" card, 15.2 GiB real).
- Fresh reflash →
nix run .#nixpi-flash -- --disk /dev/diskN --release, then insert + boot. (--releasedownloads the CI-prebuilt image; drop it tonix buildlocally instead — only works where an aarch64-linux builder exists. The image is secret-free, so the public release artifact is safe.) On the live capped card, redo the cap before boot (runbook §4c) — a reflash restores the image's partition table, and first boot would grow root back into the card's bad region. - Update the tunnel token (rotation):
CLOUDFLARE_API_TOKEN=… nix run .#cf-tunnel-applyprints a new token → pipe it tonix run .#nixpi-vault-token→ commit the vault →nix run .#nixpi-provision -- --tokenon a mounted card (or reflash), reboot. - Update Wi-Fi →
nix run .#nixpi-provision -- --wifion a mounted card, reboot.
Gotchas
- Re-plant every flash —
ddwipes the FAT partition.nixpi-flashdoes it for you. READ-BACK MISMATCHatchar 8390660is a false alarm (macOS auto-mountsFIRMWAREafterddand writes into it; fix deferred). It aborts before the plant, so verify the write yourself and runnixpi-provision— runbook §4b says how. Any other offset is real.- Never cap with
sdImage.expandOnBoot = false— the samemkIfgatesnix-store --load-db, leaving the store unregistered. Cap with a fence partition (§4c). - Wi-Fi needs
country=— the Pi 4 radio is rfkill-blocked without a regulatory domain. - Band-split SSID — if the Mac is joined to
<name>-5Gbut the keychain stores the base<name>PSK, Wi-Fi auto-detect (used bynixpi-flash/nixpi-provisionwith no Wi-Fi args) fails ("no saved password"). Pin it:nix run .#nixpi-flash -- --disk /dev/diskN --ssid <name>(the 2.4 GHz<name>also gives a headless Pi better range), or plant separately —nix run .#nixpi-wifi-creds -- --ssid <name> > wpa.confthennix run .#nixpi-provision -- --wifi --wifi-conf wpa.conf. Verified 2026-07-15 onBELL044. - The token is plaintext on the FAT partition at rest — accepted tradeoff for
unattended first-boot; mitigate with physical security + rotate on card loss. The
/runcopy is root-only (0600).
Full runbook
docs/nixpi-sd-flashing-runbook.md (§1 = the verified-write golden rule; §3c = the f3 card
test; §4b = the plant step; §4c = reflashing the capped card).