From 2fc39a5f153dbdd7931472f8de43badd56c44cb7 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Tue, 16 Jun 2026 13:25:02 +0100 Subject: [PATCH 1/7] feat(rpi5): add placeholder hardware-configuration Committed so the lyrathorpe-rpi5 host evaluates in CI before the Pi is provisioned. It is a placeholder, not a bootable config: on first install, regenerate it on the device with nixos-generate-config and replace this file. Excluded from formatters/linters by the existing hardware-configuration.nix rules. --- .../machine/RPi5/hardware-configuration.nix | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 system/machine/RPi5/hardware-configuration.nix diff --git a/system/machine/RPi5/hardware-configuration.nix b/system/machine/RPi5/hardware-configuration.nix new file mode 100644 index 0000000..8ec7411 --- /dev/null +++ b/system/machine/RPi5/hardware-configuration.nix @@ -0,0 +1,31 @@ +# PLACEHOLDER hardware configuration for the Raspberry Pi 5. +# +# 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 ./README.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 Raspberry Pi 5 boots from an SD card / USB 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"; + }; + + swapDevices = [ ]; +} -- 2.52.0 From 1cb83717757586163ca2df47a8bc037d1a05ec62 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Tue, 16 Jun 2026 13:25:31 +0100 Subject: [PATCH 2/7] feat(rpi5): add Docker host with LAN-restricted network socket Enable Docker and expose the daemon over TCP 2375 by extending the systemd docker.socket ListenStream (avoids the daemon.json hosts vs unit -H fd:// conflict). The port is not added to allowedTCPPorts; instead an nftables rule accepts it only from the trusted LAN subnet. Plain 2375 is root-equivalent, so the source restriction is the only safeguard -- mTLS on 2376 is the documented upgrade path. --- system/machine/RPi5/docker.nix | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 system/machine/RPi5/docker.nix diff --git a/system/machine/RPi5/docker.nix b/system/machine/RPi5/docker.nix new file mode 100644 index 0000000..ff4dc88 --- /dev/null +++ b/system/machine/RPi5/docker.nix @@ -0,0 +1,34 @@ +# Docker host with the daemon socket exposed over the network. +# +# SECURITY: the daemon listens on plain TCP 2375 with NO TLS and NO auth. Access +# to that port is root-equivalent on this host (the Docker API can mount the +# host filesystem and run privileged containers). The ONLY thing protecting it +# is the nftables rule below, which accepts 2375 solely from the trusted LAN +# subnet. Do not widen that subnet to anything you do not fully trust. The +# secure upgrade path is mutual TLS on 2376 (--tlsverify with client certs); +# that needs out-of-band cert provisioning and is intentionally not wired here. +{ ... }: +{ + virtualisation.docker.enable = true; + + # Expose the daemon over TCP by extending systemd socket activation rather than + # setting daemon.settings.hosts. The NixOS docker unit starts dockerd with + # `-H fd://` and takes its listeners from this socket; putting `hosts` in + # daemon.json as well would conflict with that and dockerd would refuse to + # start. Adding the TCP listener here keeps a single source of truth. + # The leading "" resets the unit's default (unix-socket-only) ListenStream list. + systemd.sockets.docker.socketConfig.ListenStream = [ + "" + "/run/docker.sock" + "0.0.0.0:2375" + ]; + + # Source-restricted firewall rule for the Docker TCP port. 2375 is deliberately + # NOT added to networking.firewall.allowedTCPPorts (that would open it to every + # source); instead nftables accepts it only from the trusted subnet. Adjust the + # CIDR to match the LAN that should reach the Docker API. + networking.nftables.enable = true; + networking.firewall.extraInputRules = '' + ip saddr 10.187.1.0/24 tcp dport 2375 accept + ''; +} -- 2.52.0 From 108f7b9528ccc6bf4f9070cceb892ccab41b673c Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Tue, 16 Jun 2026 13:25:57 +0100 Subject: [PATCH 3/7] feat(rpi5): add nginx reverse-proxy module Enable nginx with the recommended proxy/TLS/optimisation/gzip settings and a declarative virtualHosts table -- each proxied service is a Nix entry, so the routing lives in-repo. Ships one HTTP-only example vhost; enableACME/forceSSL are present but commented, to be flipped per-vhost once a DNS name and cert exist. Opens 80 and 443. --- system/machine/RPi5/reverse-proxy.nix | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 system/machine/RPi5/reverse-proxy.nix diff --git a/system/machine/RPi5/reverse-proxy.nix b/system/machine/RPi5/reverse-proxy.nix new file mode 100644 index 0000000..ef57877 --- /dev/null +++ b/system/machine/RPi5/reverse-proxy.nix @@ -0,0 +1,39 @@ +# Native nginx reverse proxy. The proxy configuration is declarative Nix: +# every proxied service is an entry under services.nginx.virtualHosts, so the +# whole routing table lives in this file and is built/version-controlled with +# the rest of the system. +# +# To add a proxied service, add another virtualHosts."" entry following +# the example below. To serve it over HTTPS, uncomment enableACME + forceSSL on +# that vhost once it has a real DNS name and the ACME HTTP-01/DNS-01 challenge +# can be satisfied (see security.acme for the account/email and DNS settings). +{ ... }: +{ + services.nginx = { + enable = true; + recommendedProxySettings = true; # sane proxy_set_header defaults (Host, X-Forwarded-*) + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + + virtualHosts = { + # Example reverse-proxy vhost. Replace the name and upstream with a real + # service (e.g. a container published by the Docker host on this machine). + "example.lan" = { + # enableACME = true; # request a Let's Encrypt cert for this host + # forceSSL = true; # redirect HTTP -> HTTPS once the cert exists + locations."/" = { + proxyPass = "http://127.0.0.1:8080"; + proxyWebsockets = true; # forward Upgrade/Connection for WebSocket apps + }; + }; + }; + }; + + # Public reverse-proxy ports. 443 is opened now so flipping a vhost to TLS + # needs no firewall change. + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; +} -- 2.52.0 From b56641aaee76a213e7f436ef7d08022d331ef599 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Tue, 16 Jun 2026 13:26:31 +0100 Subject: [PATCH 4/7] feat(rpi5): add host configuration (boot, network, sshd) Tie the RPi5 submodules together: import hardware-config, docker.nix and reverse-proxy.nix; pin networking.hostName to the flake attr name so nh resolves; use U-Boot/extlinux boot (raspberry-pi-5 profile supplies kernel + firmware); enable key-only sshd and a default-deny firewall opening 22. Headless -- swaywm.nix is not imported, so swayDesktop stays off. --- system/machine/RPi5/configuration.nix | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 system/machine/RPi5/configuration.nix diff --git a/system/machine/RPi5/configuration.nix b/system/machine/RPi5/configuration.nix new file mode 100644 index 0000000..0ab4c72 --- /dev/null +++ b/system/machine/RPi5/configuration.nix @@ -0,0 +1,40 @@ +# Raspberry Pi 5 (aarch64) headless server. Two roles, split into submodules: +# ./docker.nix (Docker host with a network socket) and ./reverse-proxy.nix +# (native nginx). The raspberry-pi-5 nixos-hardware profile (kernel, firmware, +# device tree) and key-only sshd (../../modules/ssh.nix) are layered on in the +# flake host table. Install notes: see ./README.md. +{ ... }: +{ + imports = [ + ./hardware-configuration.nix + ./docker.nix + ./reverse-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-rpi5"; + + # Headless server: the Sway desktop is intentionally not set up. swaywm.nix is + # not imported and features.swayDesktop.enable defaults to false (declared in + # system/modules/features.nix), so this host keeps plain TTY/SSH login. + + # Raspberry Pi boots via U-Boot + extlinux, not GRUB/systemd-boot. The + # raspberry-pi-5 nixos-hardware profile supplies the kernel, firmware and + # device tree. + boot.loader.grub.enable = false; + boot.loader.generic-extlinux-compatible.enable = true; + + # Remote administration. Key-only policy and the authorized key come from + # ../../modules/ssh.nix; here we just enable the daemon and open the port. + services.openssh.enable = true; + + # Default-deny inbound. Open only SSH here; the Docker and nginx submodules + # open their own ports (Docker via a source-restricted nftables rule, nginx + # via 80/443). List-valued, so these merge with the submodule definitions. + networking.firewall.enable = true; + networking.firewall.allowedTCPPorts = [ 22 ]; + + # See `man configuration.nix` / the stateVersion docs before changing. + system.stateVersion = "26.05"; +} -- 2.52.0 From 3470751c3ea78d45f5dfcbc9e5fe752b9e0ea187 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Tue, 16 Jun 2026 13:29:15 +0100 Subject: [PATCH 5/7] refactor(modules): declare swayDesktop feature flag in a base module lyrathorpe/user.nix reads features.swayDesktop.enable on every host, but the option was declared inside lyrathorpe/swaywm.nix -- so a host that does not import swaywm.nix (a headless server) would fail evaluation. Move the option declaration to a new always-imported system/modules/features.nix and wire it into baseModules; swaywm.nix keeps only its implementation (config) block. Headless hosts can now omit swaywm.nix and the flag defaults to false. --- flake.nix | 1 + lyrathorpe/swaywm.nix | 6 +++--- system/modules/features.nix | 12 ++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 system/modules/features.nix diff --git a/flake.nix b/flake.nix index 63167b0..ce10500 100644 --- a/flake.nix +++ b/flake.nix @@ -114,6 +114,7 @@ baseModules = [ ./lyrathorpe/user.nix ./system/modules/common-nixos.nix + ./system/modules/features.nix commonModule home-manager.nixosModules.home-manager { diff --git a/lyrathorpe/swaywm.nix b/lyrathorpe/swaywm.nix index 9c5551b..22d27c5 100644 --- a/lyrathorpe/swaywm.nix +++ b/lyrathorpe/swaywm.nix @@ -11,9 +11,9 @@ let ctp = import ./catppuccin-mocha.nix; in { - options = { - features.swayDesktop.enable = lib.mkEnableOption "Enable Sway Desktop"; - }; + # The features.swayDesktop.enable option is declared in + # system/modules/features.nix (so headless hosts can read/set it without + # importing this module). This module only provides its implementation. config = lib.mkIf cfg.enable { programs.sway = { enable = true; diff --git a/system/modules/features.nix b/system/modules/features.nix new file mode 100644 index 0000000..486c4b8 --- /dev/null +++ b/system/modules/features.nix @@ -0,0 +1,12 @@ +# Feature-flag option declarations shared by every NixOS host (imported via +# baseModules in flake.nix). Declaring the flags here -- rather than inside the +# module that implements them -- means a host can read or set a flag without +# importing the (often large) implementation module. In particular, +# features.swayDesktop.enable is read by lyrathorpe/user.nix on every host, but a +# headless host (e.g. the Pi) must be able to leave it at its default without +# pulling in lyrathorpe/swaywm.nix. The implementation lives in swaywm.nix, +# gated on this flag. +{ lib, ... }: +{ + options.features.swayDesktop.enable = lib.mkEnableOption "the Sway desktop"; +} -- 2.52.0 From 277dfa425124ea0c587305a3178c3a60c39e2aeb Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Tue, 16 Jun 2026 13:31:16 +0100 Subject: [PATCH 6/7] feat(flake): register lyrathorpe-rpi5 host Add the aarch64-linux Raspberry Pi 5 host to the host table: the RPi5 machine config, the raspberry-pi-5 nixos-hardware profile, and key-only sshd. Headless, so no swaywm.nix; base home modules only. --- flake.nix | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/flake.nix b/flake.nix index ce10500..3f8f634 100644 --- a/flake.nix +++ b/flake.nix @@ -288,6 +288,22 @@ ./lyrathorpe/home/work.nix ]; }; + + lyrathorpe-rpi5 = { + system = "aarch64-linux"; + username = "lyrathorpe"; + fullName = "Lyra Thorpe"; + portable = false; + # Headless server: Docker host + nginx reverse proxy. No swaywm.nix + # (no desktop); the raspberry-pi-5 profile supplies kernel/firmware, + # ssh.nix adds key-only sshd. + modules = [ + ./system/machine/RPi5/configuration.nix + inputs.nixos-hardware.nixosModules.raspberry-pi-5 + ./system/modules/ssh.nix + ]; + homeModules = [ ./lyrathorpe/home ]; + }; }; # Darwin host table — macOS machines built via mkDarwinHost. The shared -- 2.52.0 From efa9aa93da89bc49be531dd223ef15dfe7dd8159 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Tue, 16 Jun 2026 13:32:11 +0100 Subject: [PATCH 7/7] docs(rpi5): add install notes and update host table Add system/machine/RPi5/README.md (flash/boot, regenerate hardware-config, Docker-socket security caveat and remote-client usage, how to add a reverse-proxy vhost). Add lyrathorpe-rpi5 to the README host table and note that the swayDesktop flag now lives in system/modules/features.nix so headless hosts keep TTY login. --- README.md | 25 +++++++------ system/machine/RPi5/README.md | 68 +++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 system/machine/RPi5/README.md diff --git a/README.md b/README.md index 257e36c..61e43ac 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,14 @@ single flake. Defined in the host table in [`flake.nix`](./flake.nix): -| Configuration | System | Machine | -| --------------------- | ---------------- | --------------------------------------------------------------------------- | -| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) | -| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](./system/machine/T400/README.md) | -| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](./system/machine/MacPro31/README.md) | -| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) | -| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) | +| Configuration | System | Machine | +| --------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------- | +| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) | +| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](./system/machine/T400/README.md) | +| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](./system/machine/MacPro31/README.md) | +| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) | +| `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](./system/machine/RPi5/README.md) | +| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) | Shared layers: `lyrathorpe/home` (home-manager: shell, git, editor), `system/modules/common-nixos.nix` (all NixOS hosts: fonts, nix-ld, caches), @@ -41,11 +42,13 @@ darwin-rebuild switch --flake .#lyrathorpe-mac ## Login / greeter Graphical (Sway) hosts log in through a Wayland greeter — `greetd` running -ReGreet inside the `cage` kiosk compositor — configured centrally in +ReGreet inside the `cage` kiosk compositor — implemented in [`lyrathorpe/swaywm.nix`](./lyrathorpe/swaywm.nix), gated on -`features.swayDesktop.enable`. The greeter is forced to Dvorak to match the -console and Sway session. Hosts with `features.swayDesktop.enable = false` (the -WSL work box) keep plain TTY login. The target account needs a password +`features.swayDesktop.enable` (the option is declared in +[`system/modules/features.nix`](./system/modules/features.nix), so headless hosts +can leave it off without importing `swaywm.nix`). The greeter is forced to Dvorak +to match the console and Sway session. Headless hosts (the WSL work box and the +Raspberry Pi server) keep plain TTY login. The target account needs a password (`passwd `) before it can log in. ## MacBook (Asahi) firmware diff --git a/system/machine/RPi5/README.md b/system/machine/RPi5/README.md new file mode 100644 index 0000000..5238292 --- /dev/null +++ b/system/machine/RPi5/README.md @@ -0,0 +1,68 @@ +# Raspberry Pi 5 (`lyrathorpe-rpi5`) + +Headless `aarch64-linux` server with two roles: + +- **Docker host** — daemon exposed over the network (`docker.nix`). +- **nginx reverse proxy** — declarative `virtualHosts` (`reverse-proxy.nix`). + +## Install + +1. Flash a NixOS `aarch64` SD image (or USB) and boot the Pi. The + `raspberry-pi-5` profile from `nixos-hardware` (wired in the flake host table) + supplies the kernel, firmware and device tree; boot is U-Boot + extlinux. +2. Partition/mount the target, then **regenerate the hardware config on the + device** and replace the committed placeholder: + ```sh + nixos-generate-config --root /mnt + # copy /mnt/etc/nixos/hardware-configuration.nix over + # system/machine/RPi5/hardware-configuration.nix in this repo, then commit + ``` + `hardware-configuration.nix` in this directory is a **placeholder** committed + only so the host evaluates in CI. The machine will not boot correctly until it + is replaced with the generated one. +3. Set the host name to match the flake attribute (already done in + `configuration.nix`: `lyrathorpe-rpi5`) and build: + ```sh + sudo nixos-rebuild switch --flake .#lyrathorpe-rpi5 + # or, once the hostname is live: + nh os switch + ``` +4. Give the login user a password (`passwd lyrathorpe`) and confirm the key in + `system/modules/ssh.nix` is the one you will connect with. + +## Docker socket (security) + +The daemon listens on **plain TCP `2375`, no TLS, no auth**. Access is +root-equivalent on this host. The only protection is the nftables rule in +`docker.nix`, which accepts `2375` **only** from the trusted LAN subnet +(`10.187.1.0/24` by default — change it to match your network). Do not widen +that subnet to anything untrusted. + +From a LAN client: + +```sh +export DOCKER_HOST=tcp://lyrathorpe-rpi5:2375 +docker info +``` + +The secure upgrade path is mutual TLS on `2376` (`--tlsverify` with a CA and +client certs); it needs out-of-band cert provisioning and is intentionally not +wired here. + +## Adding a reverse-proxy site + +Each proxied service is a Nix entry in `reverse-proxy.nix`: + +```nix +services.nginx.virtualHosts."app.example.lan" = { + # enableACME = true; forceSSL = true; # once a DNS name + cert exist + locations."/" = { + proxyPass = "http://127.0.0.1:8080"; # e.g. a local container + proxyWebsockets = true; + }; +}; +``` + +The example vhost is HTTP-only by design. Turn on `enableACME`/`forceSSL` +per-vhost once the host has a real DNS name and the ACME challenge can be met; +`443` is already open in the firewall. -- 2.52.0