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/flake.nix b/flake.nix index 63167b0..3f8f634 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 { @@ -287,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 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/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. 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"; +} 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 + ''; +} 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 = [ ]; +} 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 + ]; +} 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"; +}