Author SHA1 Message Date
lyrathorpe 47362090c3 Merge pull request 'docs(memory): record the Task Type field now required on WSP tickets' (#105) from docs/memory-jira-task-type into main
CI / flake (push) Successful in 4m8s
Reviewed-on: #105
2026-08-25 14:33:03 +01:00
Emma Thorpe 8c5773447e docs(memory): record the Task Type field now required on WSP tickets
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 57s
Creating a WSP Task now fails with "Task Type is required to create a Task
issue". The field is customfield_15622 and the create validator enforces it
even though createmeta does not list it as required - the same trap the note
already records for Bug's versions field.
2026-08-25 14:25:24 +01:00
lyrathorpe ae44982c65 Merge pull request 'docs(memory): record the WSP-32957 PIM migration project state' (#104) from docs/memory-wsp-32957-pim-migration into main
CI / flake (push) Successful in 4m44s
Reviewed-on: #104
2026-08-24 17:54:09 +01:00
Emma Thorpe c82c1bef9c docs(memory): record the WSP-32957 PIM migration project state
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 1m35s
Long-running epic spanning multiple sessions, with several findings that were
expensive to establish and that contradict the Jira epic text — most notably
that production runs in the subscription named "Workspace Platform Technical
Preview", not the one named "Production".

Points at ~/code/WSP-32957-CONTINUATION.md for the detail rather than carrying
it here, following the pattern used for the SIBO project.
2026-08-24 17:51:38 +01:00
lyrathorpe 39b2b1d24b Merge pull request 'fix(tmux): stop clip.exe mangling non-ASCII in the WSL clipboard' (#103) from fix/tmux-clipboard-utf8 into main
CI / flake (push) Successful in 5m47s
Reviewed-on: #103
2026-08-24 13:36:15 +01:00
lyrathorpe 0022a152e3 Merge pull request 'fix(edaas): restore passwordless wheel under sudo-rs' (#102) from fix/edaas-passwordless-sudo-rs into main
CI / flake (push) Successful in 7m44s
Reviewed-on: #102
2026-08-24 13:29:39 +01:00
Emma Thorpe d9db12c4a5 fix(tmux): stop clip.exe mangling non-ASCII in the WSL clipboard
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 7m8s
tmux-yank autodetects WSL and pipes the selection to clip.exe, which
decodes its stdin as the console OEM codepage rather than UTF-8. Copying
an em dash out of a pane put "ΓÇö" on the Windows clipboard; the same
applies to every non-ASCII character.

Override the copy command to route through tmux's own buffer. With
set-clipboard on, that emits OSC 52 and the terminal receives UTF-8
directly, with no Windows-side helper in the path. Windows Terminal
honours OSC 52; verified against the running client.

Guarded on /proc/version so only WSL is affected. iTerm2 does not accept
OSC 52 by default, so the Darwin hosts keep pbcopy.

Set in the plugin's extraConfig rather than the shared block because
yank.tmux bakes the copy command into its key bindings at load time, and
home-manager emits plugin extraConfig before the run-shell.
2026-08-24 13:26:21 +01:00
Emma Thorpe 4ac9d1108b docs(shell): record the EDaaS passwordless-wheel exception
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m10s
The sudo-rs section claimed the whole fleet runs the stock "wheel, with a
password" policy, which is no longer true for the WSL host. Explain why the
NixOS-WSL default does not survive the sudo-rs swap, and mark the difference in
the per-host table.
2026-08-24 11:16:15 +01:00
Emma Thorpe 0c151943de fix(edaas): restore passwordless wheel under sudo-rs
NixOS-WSL sets `security.sudo.wheelNeedsPassword = false`, but that option
belongs to the `security.sudo` module and does not carry over to the sudo-rs
swap in modules/common-nixos.nix, whose equivalent option defaults to true.
Since that swap landed, sudo on this host prompts for the account password --
which WSL set during install and nobody knows -- so escalation only worked
through `wsl -u root`.

Set `security.sudo-rs.wheelNeedsPassword = false` on the host to match the
NixOS-WSL default. Other NixOS hosts are unaffected and keep the prompt.
2026-08-24 11:16:12 +01:00
renovate-bot dcb8a5e66a Merge pull request 'chore(deps): lock file maintenance flake inputs' (#101) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 6m6s
2026-08-24 03:10:24 +01:00
Renovate Bot bdb21a6d50 chore(deps): lock file maintenance flake inputs
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 7m14s
2026-08-24 02:02:49 +00:00
renovate-bot 1da34d6232 Merge pull request 'chore(deps): lock file maintenance flake inputs' (#100) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 4m53s
2026-08-24 00:07:42 +01:00
Renovate Bot 19e0b7f13f chore(deps): lock file maintenance flake inputs
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 4m53s
2026-08-23 23:02:35 +00:00
15 changed files with 153 additions and 636 deletions
+21 -22
View File
@@ -7,23 +7,22 @@ single flake.
Defined in the host table in [`flake.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/flake.nix): Defined in the host table in [`flake.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/flake.nix):
| Configuration | System | Machine | | Configuration | System | Machine |
| --------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | --------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) | | `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) |
| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/t400/) | | `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/t400/) |
| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/macpro31/) | | `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/macpro31/) |
| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/edaas/) | | `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/edaas/) |
| `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/rpi5/) | | `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/rpi5/) |
| `lyrathorpe-zero2w` | `aarch64-linux` | Raspberry Pi Zero 2 W "Psion sidecar": PPP over RS232 + legacy mail proxy — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/pizero2w/) | | `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/darwin/) |
| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/darwin/) |
Shared layers: `home` (home-manager: shell, git, editor), Shared layers: `home` (home-manager: shell, git, editor),
`modules/common-nixos.nix` (all NixOS hosts: fonts, nix-ld, caches), `modules/common-nixos.nix` (all NixOS hosts: fonts, nix-ld, caches),
`modules/workstation.nix` (physical graphical hosts: audio, thermald, `modules/workstation.nix` (physical graphical hosts: audio, thermald,
earlyoom, fwupd), `modules/laptop.nix` (laptops: Wi-Fi, Bluetooth, power, earlyoom, fwupd), `modules/laptop.nix` (laptops: Wi-Fi, Bluetooth, power,
lid), `modules/desktop.nix` (wired desktops: NetworkManager), and lid), `modules/desktop.nix` (wired desktops: NetworkManager), and
`modules/ssh.nix` (key-only sshd). The x86 hosts and both Raspberry Pis also `modules/ssh.nix` (key-only sshd). The x86 hosts also pull `nixos-hardware`
pull `nixos-hardware` profiles. The full module catalogue is below. profiles. The full module catalogue is below.
## Repository layout ## Repository layout
@@ -57,17 +56,17 @@ module reaches a host: **baseModules** (every NixOS host, via `flake.nix`),
**host table** (listed explicitly per host in `flake.nix`), or **transitively** **host table** (listed explicitly per host in `flake.nix`), or **transitively**
(pulled in by another module's `imports`). (pulled in by another module's `imports`).
| Module | Imported by | What it does / when to use it | | Module | Imported by | What it does / when to use it |
| ------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `common-nixos.nix` | baseModules (all NixOS) | Timezone/locale, store hygiene (auto-optimise, big download buffer, **no** auto-GC), the nix-community binary cache, `nix-ld`, **sudo-rs** in place of sudo, base CLI (`git`, `fastfetch`), and the fleet-wide font stack. | | `common-nixos.nix` | baseModules (all NixOS) | Timezone/locale, store hygiene (auto-optimise, big download buffer, **no** auto-GC), the nix-community binary cache, `nix-ld`, **sudo-rs** in place of sudo, base CLI (`git`, `fastfetch`), and the fleet-wide font stack. |
| `users.nix` | baseModules (all NixOS) | Builds `users.users` from the registry for the host's `hostUsers`; enables zsh; enables Firefox + Thunderbird **only** when `features.swayDesktop.enable` is on. Applies per-user `linger`. | | `users.nix` | baseModules (all NixOS) | Builds `users.users` from the registry for the host's `hostUsers`; enables zsh; enables Firefox + Thunderbird **only** when `features.swayDesktop.enable` is on. Applies per-user `linger`. |
| `features.nix` | baseModules (all NixOS) | Declares the feature-flag options (`features.swayDesktop.enable`, `features.claudeCode.enable`) so any host can read/set them without importing the heavy implementation module, plus the CPU capability fact they derive from (`features.cpu.microarchLevel`) and the assertion that guards it. See "CPU capability gating". | | `features.nix` | baseModules (all NixOS) | Declares the feature-flag options (`features.swayDesktop.enable`, `features.claudeCode.enable`) so any host can read/set them without importing the heavy implementation module, plus the CPU capability fact they derive from (`features.cpu.microarchLevel`) and the assertion that guards it. See "CPU capability gating". |
| `workstation.nix` | transitively (via laptop/desktop) | Form-factor-agnostic base for physical graphical hosts: turns on `swayDesktop`, Dvorak console, PipeWire, firewall (default-deny), fstrim, earlyoom, fwupd, thermald (x86), redistributable fw. | | `workstation.nix` | transitively (via laptop/desktop) | Form-factor-agnostic base for physical graphical hosts: turns on `swayDesktop`, Dvorak console, PipeWire, firewall (default-deny), fstrim, earlyoom, fwupd, thermald (x86), redistributable fw. |
| `laptop.nix` | host table (MBP, T400) | `imports` workstation.nix, then adds the portable bits: iwd Wi-Fi, lid suspend/lock, Bluetooth + blueman. | | `laptop.nix` | host table (MBP, T400) | `imports` workstation.nix, then adds the portable bits: iwd Wi-Fi, lid suspend/lock, Bluetooth + blueman. |
| `desktop.nix` | host table (Mac Pro) | `imports` workstation.nix, then swaps Wi-Fi for wired NetworkManager. Pair with `portable = false` in the host table. | | `desktop.nix` | host table (Mac Pro) | `imports` workstation.nix, then swaps Wi-Fi for wired NetworkManager. Pair with `portable = false` in the host table. |
| `sway.nix` | host table (graphical hosts) | Implementation of `features.swayDesktop`: the system Sway package, the greetd/ReGreet (cage) greeter forced to Dvorak, xdg-portal, Wayland utility packages. Home-side Sway config is in `home/sway.nix`. | | `sway.nix` | host table (graphical hosts) | Implementation of `features.swayDesktop`: the system Sway package, the greetd/ReGreet (cage) greeter forced to Dvorak, xdg-portal, Wayland utility packages. Home-side Sway config is in `home/sway.nix`. |
| `ssh.nix` | host table (T400, Mac Pro, both Pis) | Enables sshd, opens port 22, enforces a key-only policy (no password / keyboard-interactive, no root). Authorized keys come from the registry via `users.nix`. | | `ssh.nix` | host table (T400, Mac Pro, RPi5) | Enables sshd, opens port 22, enforces a key-only policy (no password / keyboard-interactive, no root). Authorized keys come from the registry via `users.nix`. |
| `firmware/` | referenced by MBP host config | Committed Apple peripheral firmware blobs for the Asahi MBP (see "MacBook (Asahi) firmware"). | | `firmware/` | referenced by MBP host config | Committed Apple peripheral firmware blobs for the Asahi MBP (see "MacBook (Asahi) firmware"). |
Form-factor decision: a **laptop** imports `laptop.nix` (default Form-factor decision: a **laptop** imports `laptop.nix` (default
`portable = true`); a **wired desktop** imports `desktop.nix` and sets `portable = true`); a **wired desktop** imports `desktop.nix` and sets
-205
View File
@@ -1,205 +0,0 @@
# Raspberry Pi Zero 2 W (`lyrathorpe-zero2w`)
Headless `aarch64-linux` "Psion sidecar": an RS232 companion for a Psion 5MX,
after [Kian Ryan's PPP modem and terminal
write-up](https://www.kianryan.co.uk/2022-11-28-psion-sidecar-ppp-modem-and-terminal/).
Two roles, split into submodules:
- **PPP link + telnet** (`serial-ppp.nix`) — `pppd` on `/dev/ttyAMA0`, the Psion
on the far end of a null-modem cable, NAT out to Wi-Fi, and a telnet login for
the Psion's terminal client.
- **Legacy mail proxy** (`email-proxy.nix`) — cleartext POP3/SMTP for the
Psion's built-in mail client, forwarded to authenticated IMAPS/SMTPS by
[legacy-email-proxy](https://code.emmathe.dev/lyrathorpe/legacy-email-proxy).
That project ships its own package and NixOS module, so `email-proxy.nix`
here is only `services.legacy-email-proxy.enable` plus a path to the
credentials — nothing about the proxy is vendored into this flake.
`sd-image.nix` in the same directory is not part of the running system: it is
the one-shot install card, built as `packages.aarch64-linux.zero2w-sd-image`.
See "Install".
## Hardware and boot
The Zero 2 W is a BCM2837 — the Pi 3's SoC — so the host table uses
`nixos-hardware`'s `raspberry-pi-3` profile for the kernel, firmware and device
tree. Boot is the same U-Boot + extlinux path as the other Pi.
Unlike the Pi 5, this host owns the firmware partition declaratively
(`hardware.raspberry-pi.firmware.enable`): every `switch` rewrites
`/boot/firmware`, including `config.txt`. Two settings there matter:
| `config.txt` | Why |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dtoverlay=disable-bt` | Moves the PL011 UART off Bluetooth onto GPIO 14/15, so `/dev/ttyAMA0` is the RS232 header. The mini UART (`ttyS0`) drifts at 115200. |
| `dtoverlay=uart0,ctsrts` | RTS/CTS on GPIO 16/17. Both `pppd` and the Psion's modem profile use hardware flow control. |
| `kernel=u-boot.bin` | `hardware.raspberry-pi.firmware.uboot.enable`. Without it the rewritten `config.txt` would have no `kernel=` line and the board would stop booting. |
`gpu_mem=16`, `start_x=0`, `camera_auto_detect=0` and `display_auto_detect=0`
hand the VideoCore the minimum: the board has 512 MB total and no display.
## Never build on the Pi
512 MB of RAM and an SD card. It cannot compile its own system, and there is
deliberately no swap partition (SD cards wear out under swap writes) — zram
takes its place. Build somewhere else and push the result:
```sh
# from a workstation, using another aarch64 machine as the builder
nixos-rebuild switch --flake .#lyrathorpe-zero2w \
--build-host lyrathorpe@lyrathorpe-rpi5 \
--target-host lyrathorpe@<pi-address> --use-remote-sudo
```
The `raspberry-pi-3` profile builds the vendor kernel from source and it is not
in the binary cache, so the first build is long (hours on the Pi 5, less on the
MacBook). Later builds reuse it. The same applies to the SD image below: it
contains that kernel, so it needs an `aarch64-linux` builder too. From an
`x86_64` box or a Mac, that means a remote builder (`nix.buildMachines`) or, on
Darwin, `nix.linux-builder.enable`.
## Install
The card is built from this flake, not downloaded. A generic NixOS image would
boot, but there would be no way into the machine afterwards: it has no Ethernet,
no wifi credentials, and this configuration hands the serial port to `pppd`, so
there is no console either. Building the host's own image sidesteps all three —
the first boot is already the real system, with the SSH key from the registry
in place.
1. **Set the SSID.** `networking.wireless.networks` in `configuration.nix` still
says `CHANGE-ME-SSID`. It is baked into the image at build time; only the PSK
is read at runtime.
2. **Build and write the card.** On an `aarch64-linux` machine (or with one
configured as a builder):
```sh
nix build .#packages.aarch64-linux.zero2w-sd-image
sudo dd if=result/sd-image/nixos-zero2w.img of=/dev/sdX bs=4M conv=fsync status=progress
```
Check `/dev/sdX` twice. `dd` does not ask.
3. **Seed the secrets before first boot.** They are not in the image. Mount the
card's second partition (the ext4 root) and write both files described under
"Secrets" below:
```sh
sudo mount /dev/sdX2 /mnt
sudo mkdir -p /mnt/var/lib/wpa_supplicant /mnt/var/lib/legacy-email-proxy
printf 'psk_home=%s\n' 'the-pre-shared-key' \
| sudo tee /mnt/var/lib/wpa_supplicant/secrets.conf > /dev/null
sudo chmod 600 /mnt/var/lib/wpa_supplicant/secrets.conf
# ... and /mnt/var/lib/legacy-email-proxy/backend.env, same permissions
sudo umount /mnt
```
Skip the PSK and the board boots with no network at all.
4. **Boot it.** Give it a few minutes on first boot — it resizes the root
partition and generates host keys on a slow card. Then:
```sh
ssh lyrathorpe@lyrathorpe-zero2w.local # mDNS; services.avahi publishes it
```
5. **Give the login user a password** (`passwd lyrathorpe`) if you want console
or telnet login; the SSH key from
[`users/registry.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/users/registry.nix)
already works without one.
6. Thereafter, rebuild from another machine as in the previous section.
`hosts/PiZero2W/hardware-configuration.nix` is a **placeholder** — but its
layout (`/` on label `NIXOS_SD`, `/boot/firmware` on label `FIRMWARE`) is
exactly what the SD image produces, so there is nothing to regenerate for a card
install. Run `nixos-generate-config` and replace it only if you deviate from
that layout.
If the board never appears on the network, it is almost always the PSK file.
Re-mount the card and check it. Failing that, a mini-HDMI monitor and a
micro-USB keyboard get you a console on `tty1` — the serial port will not,
because `pppd` holds it.
## Secrets (not in the Nix store)
Both files are created on the device, owned by root, mode `0600`. Neither is
managed by this flake; the units that read them fail loudly if they are absent.
**Wi-Fi PSK** — `/var/lib/wpa_supplicant/secrets.conf`:
```
psk_home=<the pre-shared key>
```
The SSID itself _is_ in `configuration.nix` and is currently the placeholder
`CHANGE-ME-SSID`; set it to the real network. `wpa_supplicant` resolves
`pskRaw = "ext:psk_home"` against this file at runtime.
**Mail backend** — `/var/lib/legacy-email-proxy/backend.env`, a systemd
`EnvironmentFile`:
```
BACKEND_IMAP_HOST=imap.example.com
BACKEND_IMAP_USER=someone@example.com
BACKEND_IMAP_PASS=<app password>
BACKEND_SMTP_HOST=smtp.example.com
BACKEND_SMTP_USER=someone@example.com
BACKEND_SMTP_PASS=<app password>
```
Ports and TLS default sensibly (IMAPS 993, SMTPS 465); the full variable list is
in the proxy's README.
### Why POP3 and not IMAP
The Psion's built-in mail client speaks POP only, so POP3 is what the proxy
exposes. If a third-party IMAP client is ever installed on the device, the
answer is **not** to add an IMAP frontend to the proxy: the backend is already
IMAP, so there is no protocol to translate, only TLS to remove. An `stunnel`
client (plaintext 143 on the PPP link, IMAPS 993 outbound) does that in a few
lines with no code, and credentials pass straight through — IMAP clients always
authenticate.
SMTP stays on the proxy either way. A client of this vintage cannot do SMTP
AUTH, which is exactly why the proxy injects the backend credentials.
## Psion configuration
Matches the addressing in `serial-ppp.nix` (`10.0.0.1` the Pi, `10.0.0.2` the
Psion):
- **Modem** control panel, a "Direct Cable Connection" profile: 115200 baud,
Hardware (RTS/CTS) flow control; on the Advanced tab, Terminal Detect and
Carrier Detect both **off**.
- **Internet** control panel, a new profile: Connection Type **Direct**, Manual
Login **True**. Addresses: get IP from server **False**, static **10.0.0.2**.
Get DNS from server **True** — `pppd` sends resolvers over the link
(`ms-dns`), so nothing is hard-coded on the Psion.
- Advanced: PPP extensions **False**, plain-text authentication **True**.
- Terminal client: telnet to **10.0.0.1 port 23**. It renders non-ANSI output
far better than the raw serial console does.
- Mail client: POP3 and SMTP server **10.0.0.1**, no encryption, no
authentication.
## Security
Everything on this host that the Psion talks to is unauthenticated and
unencrypted, because a 1999 palmtop speaks no TLS:
- **telnet on 23** — cleartext login, including the password.
- **POP3 on 110 / SMTP on 25** — full mailbox access and an open relay to anyone
who reaches them.
The confinement is the firewall, and it is the only thing standing there:
`ppp0` is a trusted interface, `wlan0` is not, and those ports are never opened
on it. The proxy binds `0.0.0.0` rather than `10.0.0.1` on purpose — the PPP
address only exists while the Psion is plugged in, and a bind-time dependency on
a serial cable is a restart loop waiting to happen. Do not add these ports to
`networking.firewall.allowedTCPPorts`, and do not put this board on an untrusted
network.
Only sshd (port 22, key-only, via
[`modules/ssh.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/modules/ssh.nix))
is reachable over Wi-Fi.
## Troubleshooting
| Symptom | Check |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No PPP at all | `systemctl status pppd-psion`, then `journalctl -u pppd-psion -f` while the Psion dials. `passive`/`persist` mean it waits, not fails. |
| PPP negotiates, then hangs | Flow control. Confirm `dtoverlay=uart0,ctsrts` is in `/boot/firmware/config.txt` and that the Psion's modem profile is set to Hardware. |
| `/dev/ttyAMA0` missing or is a Bluetooth device | `disable-bt` did not apply — the firmware partition was not rewritten. Confirm `/boot/firmware` is a mounted partition; the activation script skips with a warning if it is not. |
| Something else holds the port | `systemctl status serial-getty@ttyAMA0` — it is disabled in `serial-ppp.nix`, and must stay that way. |
| Mail proxy dead | `systemctl status legacy-email-proxy`. A missing `backend.env` fails the unit before it starts. |
+17 -9
View File
@@ -186,6 +186,14 @@ fleet's stock "wheel, with a password" policy. What it does **not** implement:
host aliases, LDAP/SSSD sudoers, `sudoreplay`, and most `Defaults` settings. host aliases, LDAP/SSSD sudoers, `sudoreplay`, and most `Defaults` settings.
Needing any of those means reverting to `security.sudo`. Needing any of those means reverting to `security.sudo`.
One exception to the password: the EDaaS box sets
`security.sudo-rs.wheelNeedsPassword = false`. NixOS-WSL ships that default for
`security.sudo` — WSL has no console login, so the trust boundary is the Windows
session and the Linux account password is never one the user chose — and the
option does not carry across to the `security.sudo-rs` module, which defaults to
requiring one. Without the explicit setting, `sudo` on that host prompts for a
password nobody knows.
If a host ever refuses to escalate, get a root shell that does not go through If a host ever refuses to escalate, get a root shell that does not go through
sudo (`wsl -u root -d NixOS` on the work box; the console or a serial/HDMI login sudo (`wsl -u root -d NixOS` on the work box; the console or a serial/HDMI login
elsewhere) and roll back with `nixos-rebuild switch --rollback`, or pick the elsewhere) and roll back with `nixos-rebuild switch --rollback`, or pick the
@@ -349,12 +357,12 @@ Claude to route new memories there.
## Per-host differences ## Per-host differences
| | Personal Linux (sway) | macOS | Work WSL (EDaaS) | | | Personal Linux (sway) | macOS | Work WSL (EDaaS) |
| --------------------------- | --------------------- | --------------------- | --------------------------- | | --------------------------- | --------------------- | --------------------- | ---------------------------- |
| Auto-tmux | yes (foot/TTY) | yes (iTerm2) | yes (WSL shell) | | Auto-tmux | yes (foot/TTY) | yes (iTerm2) | yes (WSL shell) |
| `kubectl` → kubecolor | no (no kubectl) | no | yes (work module) | | `kubectl` → kubecolor | no (no kubectl) | no | yes (work module) |
| `sudo` implementation | sudo-rs | Apple sudo + Touch ID | sudo-rs | | `sudo` implementation | sudo-rs (password) | Apple sudo + Touch ID | sudo-rs (passwordless wheel) |
| git email | `iam@emmathe.dev` | `iam@emmathe.dev` | `…@citrix.com` (work) | | git email | `iam@emmathe.dev` | `iam@emmathe.dev` | `…@citrix.com` (work) |
| ssh config managed | yes | yes | no (keeps corporate config) | | ssh config managed | yes | yes | no (keeps corporate config) |
| ssh-agent | yes | launchd | yes (work module) | | ssh-agent | yes | launchd | yes (work module) |
| GUI / theming (desktop.nix) | yes | no | no | | GUI / theming (desktop.nix) | yes | no | no |
Generated
+31 -52
View File
@@ -3,16 +3,16 @@
"brew-src": { "brew-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1786348930, "lastModified": 1786945682,
"narHash": "sha256-bCQJkbgsAMDp5HQystZLCq11UHiyEuoWbxKulAPYrh8=", "narHash": "sha256-VBESSoJccikdhxh3vp3SQeG7cZXTOulMvVkoSqNDEhs=",
"owner": "Homebrew", "owner": "Homebrew",
"repo": "brew", "repo": "brew",
"rev": "3ecc9eff23feebf1bc73846d74e14a122c93b66f", "rev": "5b90e281d4e0c8fbd6ca4d8358276fb305b8d0bd",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "Homebrew", "owner": "Homebrew",
"ref": "6.0.16", "ref": "6.0.18",
"repo": "brew", "repo": "brew",
"type": "github" "type": "github"
} }
@@ -25,11 +25,11 @@
}, },
"locked": { "locked": {
"dir": "pkgs/firefox-addons", "dir": "pkgs/firefox-addons",
"lastModified": 1786853140, "lastModified": 1787457768,
"narHash": "sha256-O880FlUav75Q5aNlg9znyg/avf1X/W7o/cAtZFLtpWc=", "narHash": "sha256-cbgeu5NTb6DtB+tNs4E6z6K/1XKKM90gVmlkWMJe+gY=",
"owner": "rycee", "owner": "rycee",
"repo": "nur-expressions", "repo": "nur-expressions",
"rev": "ba9568c9c0df6290dc2f34b032ab4cb575e73788", "rev": "25cfc8fdc413d73b3a47e3e86dafcad51cf5c9f9",
"type": "gitlab" "type": "gitlab"
}, },
"original": { "original": {
@@ -135,11 +135,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1784288435, "lastModified": 1787424939,
"narHash": "sha256-ReRHaLgr/uVqdD8afFSn+myXIfpHeOhP0yYe0TJqAA8=", "narHash": "sha256-O2tBn84NNuHrnqNVxx/XqsXwfYvS1YwBh+7CBnbCYsk=",
"owner": "cachix", "owner": "cachix",
"repo": "git-hooks.nix", "repo": "git-hooks.nix",
"rev": "43b3c1ab9d40fb1dbb008f451988a91e375825e9", "rev": "809414f0cdadf82cf11b06c2b29ba9b3168b3297",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -155,11 +155,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1786924861, "lastModified": 1787377438,
"narHash": "sha256-hftabkb+73OcGzvwFAjCiQorAhprs9TnU1+FkGO5CIw=", "narHash": "sha256-Sxu1NLTD/Ern6hFGLlZmtKCSct3YQXZI/lls8RE1XeM=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "09ae1b85a6db412d841d60f924b23f881f0d0a38", "rev": "65258d5c65a250189fde2e35f490d15e064c4c62",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -185,26 +185,6 @@
"type": "github" "type": "github"
} }
}, },
"legacy-email-proxy": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1787315211,
"narHash": "sha256-FuZ9nXMRtnMPO/wbjYkpsKn6K/FFc64P5XDmCyfyxGs=",
"ref": "refs/heads/main",
"rev": "f1e1373fd350fd77f1848eddfa67ed9e00724c25",
"revCount": 13,
"type": "git",
"url": "https://code.emmathe.dev/lyrathorpe/legacy-email-proxy"
},
"original": {
"type": "git",
"url": "https://code.emmathe.dev/lyrathorpe/legacy-email-proxy"
}
},
"nix-darwin": { "nix-darwin": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -231,11 +211,11 @@
"brew-src": "brew-src" "brew-src": "brew-src"
}, },
"locked": { "locked": {
"lastModified": 1786686423, "lastModified": 1787330919,
"narHash": "sha256-8q3WdB8o3VUI7rOz1OXfioXIaaWbFTAxRJAkWLlfc0s=", "narHash": "sha256-LslMncqN7uOOH5S88WZtO/EVt2HwD8ltUnfyANk+mC0=",
"owner": "zhaofengli", "owner": "zhaofengli",
"repo": "nix-homebrew", "repo": "nix-homebrew",
"rev": "ccabf79a6b9845eb72b51ea1d9c7ce3446350df3", "rev": "b00218e4aec0e5bf07d61a0bb13f842faa582d7b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -251,11 +231,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1786852476, "lastModified": 1787457452,
"narHash": "sha256-IM5CYtf86W4w8eUPpKcY/LpdHElmVBtJhaKnoTKxZEA=", "narHash": "sha256-FJh4esFS3zqNNuKwvN3t6wrJGewqp1AUF9DAEvoKPD8=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nix-index-database", "repo": "nix-index-database",
"rev": "c7962dc97b45129df8d751bedaf37beb5a17706e", "rev": "c51d5c2ba69c907a34e90c9b6b80cd2b93811745",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -292,11 +272,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1786867632, "lastModified": 1787144466,
"narHash": "sha256-ez+ubZlA1RtdjCB18a6zJ9M4u8qoPDy08EcnsW5M3Xw=", "narHash": "sha256-HHfv2/HkNSKbbSyU9iD/g8lbP6r4tl33sSw1W4rXCk0=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixos-hardware", "repo": "nixos-hardware",
"rev": "ff17823245ab9ff7bcae6acf950bd89cba82c38c", "rev": "0471accf8d0a8210b31d947497d179ecc99e0021",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -328,11 +308,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1786711500, "lastModified": 1787414105,
"narHash": "sha256-QvnceIGTBeDvDd9oCn+GvdsnkquliuwbVgpiRH68qaQ=", "narHash": "sha256-WncT27+3BOkgTaJZLnCsf3LcYf9RXMuR9ONSN4rzQ7s=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "02e08985a27c65ffd33d434eeb2e660a2e4dc84d", "rev": "a9e6d84f9c2f9012f5fe7d964a7851352300e61a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -344,11 +324,11 @@
}, },
"nixpkgs-unstable": { "nixpkgs-unstable": {
"locked": { "locked": {
"lastModified": 1786862985, "lastModified": 1787360063,
"narHash": "sha256-FBJRXmbGXiSUDvYEbfLYRkckayyZ6SK1UEqhCrIZ2Cs=", "narHash": "sha256-dt4WdcvsA8/RCe+VZZwqU0X+XMM3wBbGCWA0/sFWzGo=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e5bdc4a41d4c072fe1e3787eaa0320a384741d44", "rev": "2c423e03bbafcff28bfadc6781a4a8257f205cb5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -367,11 +347,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1786873773, "lastModified": 1787536726,
"narHash": "sha256-Hj/nkhKDv0aJly1PAUstrhrgEYn1mVSkLIYMh90r/Pc=", "narHash": "sha256-aBh5Yk9tX8ZV4k10BJr2fvTq0/+iWGegaCMUOU7YKas=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixvim", "repo": "nixvim",
"rev": "b397fb9f6950d57355d62bb92457d223464e0115", "rev": "e2c3f9f36326d07340626847543c557e2b95fb50",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -388,7 +368,6 @@
"git-hooks": "git-hooks", "git-hooks": "git-hooks",
"home-manager": "home-manager", "home-manager": "home-manager",
"kube-tmux": "kube-tmux", "kube-tmux": "kube-tmux",
"legacy-email-proxy": "legacy-email-proxy",
"nix-darwin": "nix-darwin", "nix-darwin": "nix-darwin",
"nix-homebrew": "nix-homebrew", "nix-homebrew": "nix-homebrew",
"nix-index-database": "nix-index-database", "nix-index-database": "nix-index-database",
+1 -44
View File
@@ -67,13 +67,6 @@
url = "github:jonmosco/kube-tmux"; url = "github:jonmosco/kube-tmux";
flake = false; flake = false;
}; };
# legacy-email-proxy: cleartext POP3/SMTP front end for the Psion's mail
# client, proxied to authenticated IMAPS/SMTPS. Ships its own package and
# NixOS module; the Pi Zero 2 W host just enables the service.
legacy-email-proxy = {
url = "git+https://code.emmathe.dev/lyrathorpe/legacy-email-proxy";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = outputs =
@@ -334,26 +327,6 @@
./users/lyrathorpe/home.nix ./users/lyrathorpe/home.nix
]; ];
}; };
lyrathorpe-zero2w = {
system = "aarch64-linux";
portable = false;
# Headless "Psion sidecar": PPP over RS232 plus a legacy mail proxy
# (hosts/PiZero2W/). No sway.nix; the raspberry-pi-3 profile carries
# the kernel/firmware/device tree (the Zero 2 W is the Pi 3's
# BCM2837 SoC) and ssh.nix adds key-only sshd. This board has 512 MB
# of RAM and never builds its own system -- see
# docs/hosts/pizero2w.md.
modules = [
./hosts/PiZero2W/configuration.nix
inputs.nixos-hardware.nixosModules.raspberry-pi-3
./modules/ssh.nix
];
users.lyrathorpe.homeModules = [
./home
./users/lyrathorpe/home.nix
];
};
}; };
# Darwin host table — macOS machines built via mkDarwinHost. The shared # Darwin host table — macOS machines built via mkDarwinHost. The shared
@@ -392,24 +365,8 @@
# nixpkgs instance for that system. Outputs here become per-system # nixpkgs instance for that system. Outputs here become per-system
# attrsets automatically (e.g. devShells.<system>.default). # attrsets automatically (e.g. devShells.<system>.default).
perSystem = perSystem =
{ config, pkgs, ... }:
{ {
config,
pkgs,
system,
...
}:
{
# One-shot SD card for bringing the Pi Zero 2 W up: that host's own
# configuration plus the sd-image module, so the first boot is
# already the real system. aarch64-linux only -- building it needs
# an aarch64 Linux builder. See docs/hosts/pizero2w.md.
packages = lib.optionalAttrs (system == "aarch64-linux") {
zero2w-sd-image =
((mkHost hosts.lyrathorpe-zero2w).extendModules {
modules = [ ./hosts/PiZero2W/sd-image.nix ];
}).config.system.build.sdImage;
};
# treefmt drives `nix fmt` and the formatting check below. nixfmt # treefmt drives `nix fmt` and the formatting check below. nixfmt
# stays the .nix formatter (the tree is already nixfmt-formatted); # stays the .nix formatter (the tree is already nixfmt-formatted);
# shfmt covers shell and prettier covers markdown/yaml/json. # shfmt covers shell and prettier covers markdown/yaml/json.
+1
View File
@@ -15,3 +15,4 @@
- [Dev clusters disposable](dev_clusters_disposable.md) — Lyra's dev clusters are recreatable; mutate/break freely, no confirmation needed - [Dev clusters disposable](dev_clusters_disposable.md) — Lyra's dev clusters are recreatable; mutate/break freely, no confirmation needed
- [Nix shell tooling](nix_shell_tooling.md) — any nixpkgs tool runs ad hoc via `nix run`/`nix shell nixpkgs#<pkg>`; a missing command is never a dead end - [Nix shell tooling](nix_shell_tooling.md) — any nixpkgs tool runs ad hoc via `nix run`/`nix shell nixpkgs#<pkg>`; a missing command is never a dead end
- [WSP local build and test](wsp_local_build_and_test.md) — core-services-cloud on this box: dotnet via nix, artifactory creds from `~/.artifactoryenv` sourced per command, how to tell auth failure from a code failure - [WSP local build and test](wsp_local_build_and_test.md) — core-services-cloud on this box: dotnet via nix, artifactory creds from `~/.artifactoryenv` sourced per command, how to tell auth failure from a code failure
- [WSP-32957 PIM migration](wsp_32957_pim_migration.md) — AKS RBAC to PIM + AutoPerm decommission; prod is in the Technical Preview subscription, three tenants; resume via `~/code/WSP-32957-CONTINUATION.md`
+3 -1
View File
@@ -9,7 +9,9 @@ Field map for the **WSP (Workspace Platform)** Jira project, to create tickets w
**Issue-type IDs:** Epic `10000`, Story `10004`, Task `10008`, Bug `10123`, Sub-task `10009`. **Issue-type IDs:** Epic `10000`, Story `10004`, Task `10008`, Bug `10123`, Sub-task `10009`.
**Fast path — use Task, not Bug.** A `Task` requires only `summary` (project/issuetype auto, reporter defaults to caller). A `Bug` requires six extra fields (below), so only pick Bug when it must be a Bug. The sibling infra/remediation tickets in WSP are Tasks. **Fast path — use Task, not Bug.** A `Task` requires `summary` plus **Task Type** `customfield_15622` (added since this note was first written; the create validator enforces it even though `createmeta` omits it, same trap as Bug's `versions`). Options value=id: Dev Task=34065, CQE Task=34066, Investigation=34067, Security=34068, Maintenance=34069, Release=34070 — use `Maintenance` for refactors and tidy-ups, `Dev Task` for feature work. A `Bug` requires six extra fields (below), so only pick Bug when it must be a Bug. The sibling infra/remediation tickets in WSP are Tasks.
Example Task `additional_fields`: `{"customfield_15622":{"id":"34069"},"components":[{"name":"Multicluster Platform"}]}`
**Bug required fields** (enforced by the create validator; note `createmeta` omits `versions` but the API rejects without it): **Bug required fields** (enforced by the create validator; note `createmeta` omits `versions` but the API rejects without it):
@@ -0,0 +1,58 @@
---
name: wsp-32957-pim-migration
description: State of the WSP AKS-RBAC-to-PIM migration and AutoPerm decommission, and how to resume it
metadata:
node_type: memory
type: project
---
Long-running epic (August 2026) moving WSP's AKS cluster RBAC off **AutoPerm
Manager**-maintained `wsp-*` groups onto the per-subscription **`CEO-*` Azure PIM**
groups, then retiring AutoPerm. Epic **WSP-32957**; on the critical path for Zensar
L1 on-call (WSP-32193). Work happens in **`~/code/multicluster`**
(`terraform/cluster`, `terraform/cluster-k8s-resources`, `products/*/environments/*`).
**Resume from `~/code/WSP-32957-CONTINUATION.md`** — full state, branch list,
verified object IDs, findings and next steps. Jira is the durable record; that file
is the index. Keep it updated as work lands ([[docs-keep-updated]]).
**Landed:** WSP-33141 (multicluster PR #1808, merged `b047163c`) added object-ID
inputs — `admin_group_oids` on `cluster`, and `cluster_user_group_oids` /
`cluster_viewer_group_oids` / `cluster_superuser_group_oids` on
`cluster-k8s-resources`. Supplying IDs _replaces_ the display-name lookup and leaves
the `data "azuread_group"` unread, which is what will let the legacy groups be
deleted. Nothing sets them yet, so behaviour is unchanged. Repoint branches for test
(WSP-33067) and staging (WSP-33068) are pushed but have **no PR** — both gated on
decisions, not code.
**Facts that cost real effort to establish, do not re-derive:**
- **Production runs in `fc7af6ae-…` (_Workspace Platform Technical Preview_), not
`d6d75d07-…` (_Workspace Platform Production_)**, which holds no clusters. The
epic was wrong about this for its whole life and every production `CEO-*` group
name and object ID had to change. Because `CEO-*` names embed the subscription
name, **always re-verify object IDs against live Entra rather than trusting the
epic table.**
- **Three tenants**, not two: `6f4fe054` (prod, prod JP), `335836de` (staging,
staging JP, test), `3eae2746` (dev). Each `wsp-*` name is a distinct object in
each tenant.
- `wsp-staging-cluster-admins` and `wsp-test-cluster-admins` are **empty**, so
`wsp-owner` is the _only_ path to `cluster-admin` in staging. Never drop it before
`SuperAdmin-*` is proven — hence the staging branch is split into an additive
commit and a cutover commit.
- **No break-glass exists**: `disableLocalAccounts = True` on every cluster; only the
deployment SPNs authenticate non-interactively.
- Graph **PIM-for-Groups is unreadable via `az`** (the CLI's first-party client lacks
`PrivilegedAccess.Read.AzureADGroup`, on both `v1.0` and `beta`). Use the portal or
`Connect-MgGraph -Scopes PrivilegedAccess.Read.AzureADGroup`.
**Watch for:** the epic gets rewritten by James Weldrake between sessions — re-read
the description before acting, and check which child tickets are still live
(WSP-33062/33063/33064/33066 were cancelled 2026-08-24, and dev was put out of
scope). Verified findings have repeatedly contradicted the epic text
([[copilot-review-false-positives]] is the same instinct: check against reality
first).
Queued Slack messages and the leaver report live as `~/code/*.txt` alongside the
continuation file; see the table in it for what has and has not been sent
([[workflow-review-and-comments]] — show them before they go out).
+16 -1
View File
@@ -287,7 +287,22 @@ in
plugins = with pkgs.tmuxPlugins; [ plugins = with pkgs.tmuxPlugins; [
sensible sensible
vim-tmux-navigator # Ctrl-h/j/k/l across vim splits and tmux panes vim-tmux-navigator # Ctrl-h/j/k/l across vim splits and tmux panes
yank {
# On WSL, tmux-yank pipes the selection to clip.exe, which decodes its
# stdin as the OEM codepage instead of UTF-8 -- an em dash reaches the
# Windows clipboard as three characters. Route through tmux's own
# buffer instead: with set-clipboard on, tmux emits OSC 52 and the
# terminal takes the text as UTF-8. Windows Terminal honours OSC 52;
# iTerm2 does not by default, hence the runtime guard rather than
# overriding pbcopy/xsel on every host. yank.tmux bakes the command
# into its key bindings when it loads, so this must be set first, which
# is what plugin extraConfig gives us.
plugin = yank;
extraConfig = ''
if-shell 'grep -qi microsoft /proc/version 2>/dev/null' \
"set -g @override_copy_command 'tmux load-buffer -w -'"
'';
}
extrakto # prefix+Tab: fzf-grab paths/URLs/text from the pane into the prompt extrakto # prefix+Tab: fzf-grab paths/URLs/text from the pane into the prompt
{ {
# Catppuccin Mocha statusline (v2 API: flavour + window options must be # Catppuccin Mocha statusline (v2 API: flavour + window options must be
+5
View File
@@ -60,6 +60,11 @@
## patch the script ## patch the script
systemd.services.docker-desktop-proxy.script = lib.mkForce ''${config.wsl.wslConf.automount.root}/wsl/docker-desktop/docker-desktop-user-distro proxy --docker-desktop-root ${config.wsl.wslConf.automount.root}/wsl/docker-desktop "C:\Program Files\Docker\Docker\resources"''; systemd.services.docker-desktop-proxy.script = lib.mkForce ''${config.wsl.wslConf.automount.root}/wsl/docker-desktop/docker-desktop-user-distro proxy --docker-desktop-root ${config.wsl.wslConf.automount.root}/wsl/docker-desktop "C:\Program Files\Docker\Docker\resources"'';
# NixOS-WSL's passwordless wheel default only covers `security.sudo`; the
# sudo-rs swap in common-nixos.nix needs it set again. No console login here,
# and no account password anyone knows.
security.sudo-rs.wheelNeedsPassword = false;
features.swayDesktop.enable = false; features.swayDesktop.enable = false;
# NOTE: this user's systemd --user lingering -- so the home-manager renovate # NOTE: this user's systemd --user lingering -- so the home-manager renovate
-111
View File
@@ -1,111 +0,0 @@
# Raspberry Pi Zero 2 W (aarch64) "Psion sidecar": an RS232 companion for a
# Psion 5MX. Two roles, split into submodules: ./serial-ppp.nix (PPP over the
# serial line, NAT out to wifi, telnet login) and ./email-proxy.nix (cleartext
# POP3/SMTP front end for the Psion's mail client). The raspberry-pi-3
# nixos-hardware profile (the Zero 2 W is the same BCM2837 SoC as the Pi 3) and
# key-only sshd (../../modules/ssh.nix) are layered on in the flake host table.
# Install notes: see ../../docs/hosts/pizero2w.md.
{ lib, ... }:
{
imports = [
./hardware-configuration.nix
./serial-ppp.nix
./email-proxy.nix
];
# Match the flake's nixosConfigurations attribute name so `nh os switch`
# (which selects by the local hostname) resolves without an explicit -H flag.
networking.hostName = "lyrathorpe-zero2w";
# Headless server: modules/sway.nix is not imported and
# features.swayDesktop.enable defaults to false, so this host keeps plain
# TTY/SSH login.
# Claude Code is a Node application. It runs on aarch64, but not usefully in
# 512 MB of RAM, and its closure is unwelcome on an SD card.
features.claudeCode.enable = false;
# 512 MB total and no swap partition -- SD cards wear out under swap writes.
# Compressed RAM swap instead; zstd is the best ratio-per-cycle the SoC can
# sustain.
zramSwap = {
enable = true;
algorithm = "zstd";
};
# The NixOS manual and man page index cost build time and a chunk of the card
# for a box that is administered over SSH from elsewhere.
documentation.nixos.enable = false;
# Own the firmware partition declaratively: every switch rewrites config.txt,
# the vendor device trees and the overlays below. Without this the card keeps
# whatever config.txt the flashed image wrote and the UART overlays never
# load. uboot.enable keeps the GPU firmware chainloading U-Boot -> extlinux,
# which is how the NixOS aarch64 SD image boots; leaving it off would rewrite
# config.txt without a `kernel=` line and the board would stop booting.
hardware.raspberry-pi.firmware = {
enable = true;
uboot.enable = true;
};
hardware.raspberry-pi.configtxt = {
settings.all = {
# Headless: hand the VideoCore the minimum and leave the rest to Linux.
# start_x/camera_auto_detect otherwise reserve VRAM for a camera stack
# this board does not have.
gpu_mem = 16;
start_x = 0;
camera_auto_detect = false;
# Left on, the firmware auto-loads the KMS display overlay, which wants
# more VRAM than this board can spare for a monitor it will never have.
display_auto_detect = false;
};
# Replaces the profile's default (vc4-kms-v3d), which is display hardware
# this host never uses.
deviceTreeOverlays.all = [
# Move the PL011 UART off Bluetooth and onto GPIO 14/15, so /dev/ttyAMA0
# is the RS232 header. The mini UART (ttyS0) derives its baud rate from
# the core clock and drifts at 115200.
{ disable-bt = { }; }
# RTS/CTS on GPIO 16/17: the Psion's modem profile uses hardware flow
# control, and so does pppd in ./serial-ppp.nix.
{ uart0.ctsrts = true; }
];
};
# Wifi is the Pi's uplink and the route the Psion reaches the internet over
# (./serial-ppp.nix masquerades onto it).
networking.interfaces.wlan0.useDHCP = true;
networking.wireless = {
enable = true;
interfaces = [ "wlan0" ];
# PSKs stay out of the Nix store: wpa_supplicant reads them at runtime from
# this file, which is created on the device (root-owned, 0600) and contains
# psk_home=<the pre-shared key>
# See ../../docs/hosts/pizero2w.md.
secretsFile = "/var/lib/wpa_supplicant/secrets.conf";
networks."CHANGE-ME-SSID".pskRaw = "ext:psk_home";
};
# The board takes a DHCP lease over wifi, so its address moves. mDNS makes it
# findable as lyrathorpe-zero2w.local instead of hunting through the router's
# lease table -- which matters most on first boot, when it is the only way in.
services.avahi = {
enable = true;
openFirewall = true;
publish = {
enable = true;
addresses = true;
workstation = true;
};
};
# Default-deny inbound. sshd opens 22 (../../modules/ssh.nix); everything the
# Psion talks to is reached over the PPP link, which ./serial-ppp.nix marks
# trusted.
networking.firewall.enable = true;
# See `man configuration.nix` / the stateVersion docs before changing.
system.stateVersion = "26.05";
}
-25
View File
@@ -1,25 +0,0 @@
# legacy-email-proxy: a cleartext POP3 (110) and SMTP (25) front end for the
# Psion's built-in mail client, forwarded to authenticated IMAPS/SMTPS.
#
# The package, the systemd unit and its hardening all live upstream
# (https://code.emmathe.dev/lyrathorpe/legacy-email-proxy); this host only
# enables the service and points it at the credentials.
{ inputs, ... }:
{
imports = [ inputs.legacy-email-proxy.nixosModules.default ];
services.legacy-email-proxy = {
enable = true;
# The listeners are unauthenticated and unencrypted by design, so the
# firewall is what confines them: ppp0 is trusted, wlan0 is not, and 110/25
# are never opened there (./serial-ppp.nix). They stay on the default
# 0.0.0.0 rather than the PPP address because 10.0.0.1 exists only while
# the Psion is plugged in, and a bind-time dependency on a serial cable is
# a restart loop waiting to happen.
# Backend hostnames and credentials. Kept out of the Nix store: created on
# the device, root-owned 0600. See ../../docs/hosts/pizero2w.md.
environmentFile = "/var/lib/legacy-email-proxy/backend.env";
};
}
-33
View File
@@ -1,33 +0,0 @@
# PLACEHOLDER hardware configuration for the Raspberry Pi Zero 2 W.
#
# This file is NOT the real generated config -- it exists only so the host
# evaluates in CI before the Pi is provisioned. The machine will not boot from
# it as-is. On first install, regenerate this file on the device with
# nixos-generate-config --root /mnt
# and replace this placeholder with the output (commit it). See ../../docs/hosts/pizero2w.md.
#
# Like every hardware-configuration.nix in this repo, this file is excluded from
# the formatter and linters (see the pre-commit/treefmt excludes in flake.nix).
{ modulesPath, ... }:
{
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
nixpkgs.hostPlatform = "aarch64-linux";
# The Zero 2 W boots from an SD card with a FAT firmware partition and an ext4
# root. Labels match the conventional sd-image layout; the real generated
# config will use by-uuid device paths instead.
fileSystems."/" = {
device = "/dev/disk/by-label/NIXOS_SD";
fsType = "ext4";
};
fileSystems."/boot/firmware" = {
device = "/dev/disk/by-label/FIRMWARE";
fsType = "vfat";
};
# 512 MB of RAM and an SD card: no swap partition (SD cards wear out under
# swap writes). zram takes its place; see ../../hosts/PiZero2W/configuration.nix.
swapDevices = [ ];
}
-44
View File
@@ -1,44 +0,0 @@
# SD-card image of this host, used exactly once: to bring the board up.
#
# Deliberately NOT imported by ./configuration.nix. The flake extends the host
# with it (see packages.aarch64-linux.zero2w-sd-image in ../../flake.nix), so
# the card carries the host's own kernel, config.txt and SSH keys rather than a
# generic installer that then has to be reconfigured over a console this host
# does not have -- pppd owns the serial port (./serial-ppp.nix).
#
# It does not carry the runtime secrets. Seed those into the card's root
# partition before first boot; see ../../docs/hosts/pizero2w.md.
{
config,
lib,
modulesPath,
...
}:
{
imports = [ "${modulesPath}/installer/sd-card/sd-image.nix" ];
# sd-image.nix pulls in profiles/all-hardware.nix, which is every driver and
# firmware blob NixOS knows about. The raspberry-pi-3 profile already carries
# what this board has, and the card is small.
hardware.enableAllHardware = lib.mkForce false;
image.baseName = "nixos-zero2w";
sdImage = {
# Compressing costs a long single-threaded pass and buys nothing: the image
# is written straight to a card with dd.
compressImage = false;
# The default 30 MiB does not hold the vendor GPU firmware, U-Boot and the
# BCM2837 device trees and overlays that nixos-hardware installs here.
firmwareSize = 128;
# The firmware partition is populated by nixos-hardware's firmware module
# (it takes over sdImage.populateFirmwareCommands); the root side is the
# stock extlinux install, which no longer arrives with it.
populateRootCommands = ''
mkdir -p ./files/boot
${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot
'';
};
}
-89
View File
@@ -1,89 +0,0 @@
# The serial half of the Psion sidecar: a PPP link to a Psion 5MX over
# /dev/ttyAMA0 (RS232 level shifter on the GPIO header, 115200 8N1 with
# RTS/CTS), masqueraded out of wifi, plus a telnet login for the Psion's
# terminal client.
#
# Cleartext telnet and unauthenticated PPP are safe *only* because the link is
# a two-node cable: the peer is a machine from 1999 that speaks no TLS. Nothing
# here is exposed to wlan0.
{ pkgs, ... }:
let
# Point-to-point addresses for the serial link; nothing else routes here.
piAddress = "10.0.0.1";
psionAddress = "10.0.0.2";
in
{
# pppd needs exclusive use of the port. NixOS starts a getty on any serial
# console named in boot.kernelParams; ttyAMA0 is not one today, but disable it
# explicitly so a later kernel-param change cannot silently steal the line.
systemd.services."serial-getty@ttyAMA0".enable = false;
services.pppd = {
enable = true;
peers.psion.config = ''
/dev/ttyAMA0
115200
${piAddress}:${psionAddress}
# Hardware flow control, matching the Psion's modem profile.
crtscts
# A null-modem cable has no carrier detect and no peer to authenticate.
local
noauth
# The systemd unit is Type=notify, so pppd must stay in the foreground.
nodetach
lock
# Wait for the Psion rather than failing when it is unplugged, and keep
# waiting for the next time it is plugged back in.
passive
persist
maxfail 0
holdoff 1
# Hand the Psion resolvers over the link, so its Internet profile can set
# "get DNS from server = True" instead of hard-coding them.
ms-dns 1.1.1.1
ms-dns 8.8.8.8
'';
};
# The Psion's route to the internet. The original write-up used pppd's
# proxyarp instead; NAT keeps the Psion out of the LAN broadcast domain and
# does not depend on what the wifi router tolerates.
networking.nat = {
enable = true;
externalInterface = "wlan0";
internalIPs = [ "${psionAddress}/32" ];
};
# Everything the Psion connects to (telnet here, POP3/SMTP in
# ./email-proxy.nix) is reachable over the PPP link and nowhere else.
networking.firewall.trustedInterfaces = [ "ppp0" ];
# The Psion's terminal client speaks telnet over TCP, which it renders far
# better than the raw serial console. Socket-activated, one process per
# connection; busybox's telnetd in inetd mode hands straight over to login.
systemd.sockets.telnetd = {
description = "Telnet login socket for the Psion";
wantedBy = [ "sockets.target" ];
listenStreams = [ "${piAddress}:23" ];
socketConfig = {
Accept = true;
# ppp0 (and with it 10.0.0.1) only exists while the Psion is connected;
# FreeBind lets the socket be listening before that.
FreeBind = true;
};
};
systemd.services."telnetd@" = {
description = "Telnet login for the Psion";
serviceConfig = {
ExecStart = "-${pkgs.busybox}/bin/busybox telnetd -i -l ${pkgs.shadow}/bin/login";
StandardInput = "socket";
StandardError = "journal";
};
};
}