feat(deploy): run the tejo from USB (disk-image-tejo-usb) #120

Merged
padreug merged 3 commits from feat/tejo-usb into dev 2026-10-06 17:35:43 +00:00
Owner

Gives the tejo the run-from-USB shape douro and batm3 already have: the stick is the system, and the factory Debian (ubilinux4) on internal storage is never touched.

nix build .#disk-image-tejo-usb built and was verified on bohm — 6.2 GB sparse / 4.0 GB actual:

  • GPT: bios_grub (0.02–1 MiB), ESP fat16 labelled ESP-USB, ext4 root labelled nixos-usb
  • MBR carries GRUB's boot.img (eb 63 90), and the ESP has both /grub/i386-pc and /grub/x86_64-efi plus /EFI/BOOT/BOOTX64.EFI

Not douro's bootloader

The tejo is the same Aaeon UP Board as sintra — tejo-installed and sintra-installed share hardware/upboard.nix — and disk-image-sintra-usb already records the finding that Aaeon firmware USB-boots in Legacy/BIOS mode: it boots the live ISO via isolinux, not the ESP. systemd-boot is UEFI-only, so a dd'd systemd-boot stick wouldn't be recognised as bootable at all. So tejo gets GRUB on a hybrid table, BIOS and UEFI. That's also the safe choice given the machine is unreachable right now and the firmware couldn't be checked directly — the image boots either path.

Commits

81a001c refactor — one USB-image helper for both bootloader shapes. disk-image-sintra-usb was a 60-line inline copy of what mkUsbDiskImage already does, and the two had drifted: the sintra image never picked up the nofail /boot or the uas/autosuspend hardening batm3 and douro carry. mkUsbDiskImage now takes partitionTableType (efi → systemd-boot, hybrid → GRUB) and grubBiosDevice; new usbGrubHybridModule + usbBusHardening. sintra-usb becomes a named config so a running stick updates in place like the others.

The hardening is scoped to the -usb configs rather than hardware/upboard.nix — that file is shared with sintra's eMMC install, and I didn't want to change a production machine's cmdline.

grub.devices is "nodev" in the config and mkForce'd to the build VM's disk only for the image: an in-place switch-to-configuration on a live stick has no /dev/vda, and GRUB's embedded core.img reads grub.cfg off the partition, so the MBR stage needs no per-generation rewrite. Plain definition rather than mkForce, because two mkForce lists merge into [ "/dev/vda" "nodev" ] instead of replacing.

c3e01c9 feat — tejo-usb + disk-image-tejo-usb, plus README on which models take which bootloader shape.

6042d69 fix — tejo had no WireGuard address. Found while checking the machine would be reachable after flashing. wg0.ips was set in hardware/douro.nix and hardware/batm3.nix, but hardware/upboard.nix is shared by tejo and sintra, so an address there would be claimed by both on the same /24 — neither got one. tejo evaluated to wg0.ips = [ ]: interface up, no IP, tunnel silently dead. Replaced both per-hardware definitions with one wireguardIpForModel table keyed on model, like the fiatCodeForModel / nfcReaderForModel tables next to it.

Verification

  • douro-usb and batm3-usb evaluate to identical kernelParams, blacklistedKernelModules, fileSystems, bootloader and autoUpgrade config as before the refactor
  • tejo-usb evaluates identically to sintra-usb, as it should
  • wg addresses after the change: douro 10.0.0.4/24 and batm3 10.0.0.5/24 unchanged, tejo now 10.0.0.3/24, sintra still deliberately empty (LAN-reachable, never had a tunnel address)
  • nixpkgs-fmt clean

Before flashing

The wg address is only half of it — the VPS maps peer pubkey to tunnel IP, so the machine needs /var/lib/wireguard/wg0.key carried over from its current Debian install, or a fresh key with its pubkey added to the VPS peer list. Both wireguard units are ConditionPathExists-guarded on that key, so a keyless first boot is clean and the tunnel starts once it's dropped in.

Expect Warning: Bill validator device /dev/ttyUSB0 not found on first boot — that's #117 (nix module defaults vs device.ts), not a real fault. tejo's actual table is validator /dev/ttyJ5 (id003), dispenser /dev/ttyJ7 (f56), GTQ 5/20/50/100.

Gives the tejo the run-from-USB shape douro and batm3 already have: the stick is the system, and the factory Debian (`ubilinux4`) on internal storage is never touched. `nix build .#disk-image-tejo-usb` built and was verified on bohm — 6.2 GB sparse / 4.0 GB actual: - GPT: `bios_grub` (0.02–1 MiB), ESP fat16 labelled **ESP-USB**, ext4 root labelled **nixos-usb** - MBR carries GRUB's boot.img (`eb 63 90`), and the ESP has both `/grub/i386-pc` and `/grub/x86_64-efi` plus `/EFI/BOOT/BOOTX64.EFI` ### Not douro's bootloader The tejo is the same Aaeon UP Board as sintra — `tejo-installed` and `sintra-installed` share `hardware/upboard.nix` — and `disk-image-sintra-usb` already records the finding that Aaeon firmware USB-boots in **Legacy/BIOS** mode: it boots the live ISO via isolinux, not the ESP. systemd-boot is UEFI-only, so a dd'd systemd-boot stick wouldn't be recognised as bootable at all. So tejo gets GRUB on a hybrid table, BIOS **and** UEFI. That's also the safe choice given the machine is unreachable right now and the firmware couldn't be checked directly — the image boots either path. ### Commits **`81a001c` refactor — one USB-image helper for both bootloader shapes.** `disk-image-sintra-usb` was a 60-line inline copy of what `mkUsbDiskImage` already does, and the two had drifted: the sintra image never picked up the `nofail` `/boot` or the `uas`/autosuspend hardening batm3 and douro carry. `mkUsbDiskImage` now takes `partitionTableType` (`efi` → systemd-boot, `hybrid` → GRUB) and `grubBiosDevice`; new `usbGrubHybridModule` + `usbBusHardening`. `sintra-usb` becomes a named config so a running stick updates in place like the others. The hardening is scoped to the `-usb` configs rather than `hardware/upboard.nix` — that file is shared with sintra's eMMC install, and I didn't want to change a production machine's cmdline. `grub.devices` is `"nodev"` in the config and mkForce'd to the build VM's disk only for the image: an in-place `switch-to-configuration` on a live stick has no `/dev/vda`, and GRUB's embedded core.img reads grub.cfg off the partition, so the MBR stage needs no per-generation rewrite. Plain definition rather than mkForce, because two mkForce lists merge into `[ "/dev/vda" "nodev" ]` instead of replacing. **`c3e01c9` feat — `tejo-usb` + `disk-image-tejo-usb`**, plus README on which models take which bootloader shape. **`6042d69` fix — tejo had no WireGuard address.** Found while checking the machine would be reachable after flashing. `wg0.ips` was set in `hardware/douro.nix` and `hardware/batm3.nix`, but `hardware/upboard.nix` is shared by tejo and sintra, so an address there would be claimed by both on the same /24 — neither got one. tejo evaluated to `wg0.ips = [ ]`: interface up, no IP, tunnel silently dead. Replaced both per-hardware definitions with one `wireguardIpForModel` table keyed on model, like the `fiatCodeForModel` / `nfcReaderForModel` tables next to it. ### Verification - `douro-usb` and `batm3-usb` evaluate to identical `kernelParams`, `blacklistedKernelModules`, `fileSystems`, bootloader and `autoUpgrade` config as before the refactor - `tejo-usb` evaluates identically to `sintra-usb`, as it should - wg addresses after the change: douro `10.0.0.4/24` and batm3 `10.0.0.5/24` unchanged, tejo now `10.0.0.3/24`, sintra still deliberately empty (LAN-reachable, never had a tunnel address) - `nixpkgs-fmt` clean ### Before flashing The wg address is only half of it — the VPS maps peer pubkey to tunnel IP, so the machine needs `/var/lib/wireguard/wg0.key` carried over from its current Debian install, or a fresh key with its pubkey added to the VPS peer list. Both wireguard units are `ConditionPathExists`-guarded on that key, so a keyless first boot is clean and the tunnel starts once it's dropped in. Expect `Warning: Bill validator device /dev/ttyUSB0 not found` on first boot — that's #117 (nix module defaults vs `device.ts`), not a real fault. tejo's actual table is validator `/dev/ttyJ5` (id003), dispenser `/dev/ttyJ7` (f56), GTQ 5/20/50/100.
disk-image-sintra-usb was a 60-line inline copy of everything
mkUsbDiskImage already does, plus the GRUB/hybrid bits the Aaeon
firmware needs — so the two implementations had already drifted: the
sintra image never picked up the `nofail` /boot that keeps a slow
ESP-USB enumeration out of emergency mode, nor the uas/autosuspend
hardening batm3.nix and douro.nix carry.

- mkUsbDiskImage takes named args with `partitionTableType` ("efi" for
  systemd-boot, "hybrid" for GRUB) and `grubBiosDevice`. The ESP relabel
  is layout-independent: the hybrid table creates the ESP first and
  bios_grub second, so it stays partition 1 either way.
- New `usbGrubHybridModule` + `usbBusHardening` modules. The hardening is
  scoped to the -usb configs rather than hardware/upboard.nix, which
  sintra's eMMC install also reads — no cmdline change on a production
  machine.
- `nixosConfigurations.sintra-usb` is now a named config, so a running
  stick can be updated in place (nix copy + switch-to-configuration)
  like batm3-usb and douro-usb.
- grub.devices is "nodev" in the config and mkForce'd to the build VM's
  disk only for the image: an in-place switch on a live stick has no
  /dev/vda, and GRUB's embedded core.img reads grub.cfg off the
  partition, so the MBR stage needs no per-generation rewrite. Plain
  definition rather than mkForce, since two mkForce lists merge into
  [ "/dev/vda" "nodev" ] instead of replacing.

douro-usb and batm3-usb evaluate to byte-identical kernelParams,
blacklistedKernelModules, fileSystems, bootloader and autoUpgrade
config as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tejo still runs its factory Debian (ubilinux4, kernel 4.9) on
internal storage and has never had bitspire on it. Rather than flash
that drive, give it the run-from-USB shape douro and batm3 already use:
the stick is the system and the internal install is never touched.

- nixosConfigurations.tejo-usb — tejo-installed + usbBootModule +
  usbBusHardening + usbGrubHybridModule. Evaluates identically to
  sintra-usb, which shares hardware/upboard.nix.
- packages.disk-image-tejo-usb — hybrid table, GRUB, BIOS + UEFI.

NOT the efi/systemd-boot shape douro uses. The tejo is the same Aaeon
UP Board as sintra, whose firmware was found to USB-boot in Legacy/BIOS
mode; systemd-boot is UEFI-only, so a dd'd systemd-boot stick would not
be recognised as bootable at all. The hybrid image boots either path, so
it is also the safe choice if the firmware turns out to differ.

README documents both bootloader shapes and which models take which.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`networking.wireguard.interfaces.wg0.ips` was set in hardware/douro.nix
and hardware/batm3.nix, but hardware/upboard.nix is shared by tejo and
sintra — an address there would be claimed by both machines on the same
/24, so neither got one. tejo therefore evaluated to `wg0.ips = [ ]`:
the interface comes up with no IP and the tunnel is silently dead. On a
machine with no other route in, that is how you lose a box.

Replace the two per-hardware definitions with one `wireguardIpForModel`
table in flake.nix, keyed on model like fiatCodeForModel /
upgradeWindowForModel / nfcReaderForModel, and give tejo 10.0.0.3/24 —
the address it answers on today under its factory Debian.

douro (10.0.0.4/24) and batm3 (10.0.0.5/24) evaluate unchanged; sintra
stays deliberately unlisted, since it is reachable on the LAN and has
never had a tunnel address.

The address is only half of it: the VPS maps peer pubkey to tunnel IP,
so the machine still needs /var/lib/wireguard/wg0.key carried over from
its previous install (or a fresh key added to the VPS peer list). Both
wireguard units are ConditionPathExists-guarded on that key, so a
keyless first boot is clean and the tunnel starts once it is dropped in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
padreug deleted branch feat/tejo-usb 2026-10-06 17:35:43 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire!120
No description provided.