diff --git a/.envrc b/.envrc index 862b788..3550a30 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1 @@ -export NIX_CONFIG="eval-cache = false" use flake diff --git a/.gitignore b/.gitignore index f9f13d5..1d082b3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,3 @@ target/ result result-* .direnv -pi-serial-*.log -serial-*.log diff --git a/README.md b/README.md index ec121f4..0edbc0f 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,7 @@ Pi (door sensor) ──webhook──> Cache ──webhook──> Discord | Directory | What it is | |-----------|------------| -| [`pi/`](pi/) | Raspberry Pi OS base with laptop-built Noisebell deploy | +| [`pi/`](pi/) | NixOS config + Rust service for the Pi | | [`remote/`](remote/) | Server-side services (cache and Discord bot) | -| [`secrets/`](secrets/) | Shared agenix-encrypted secrets and recipient rules | Each directory has its own README with setup and configuration details. - -For hosted deployment, another repo such as `../extremist-software` imports `noisebell.nixosModules.default`. That host repo provides deployment-specific values like domains, ports, and the Pi address, while the Noisebell module itself points `agenix` at the encrypted files in `secrets/` and consumes the decrypted runtime files on the target machine. diff --git a/flake.lock b/flake.lock index b21c346..40cda20 100644 --- a/flake.lock +++ b/flake.lock @@ -23,22 +23,6 @@ "type": "github" } }, - "argononed": { - "flake": false, - "locked": { - "lastModified": 1729566243, - "narHash": "sha256-DPNI0Dpk5aym3Baf5UbEe5GENDrSmmXVdriRSWE+rgk=", - "owner": "nvmd", - "repo": "argononed", - "rev": "16dbee54d49b66d5654d228d1061246b440ef7cf", - "type": "github" - }, - "original": { - "owner": "nvmd", - "repo": "argononed", - "type": "github" - } - }, "crane": { "locked": { "lastModified": 1773857772, @@ -76,21 +60,6 @@ "type": "github" } }, - "flake-compat": { - "locked": { - "lastModified": 1767039857, - "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, "home-manager": { "inputs": { "nixpkgs": [ @@ -112,79 +81,13 @@ "type": "github" } }, - "nixos-hardware": { - "locked": { - "lastModified": 1774018263, - "narHash": "sha256-HHYEwK1A22aSaxv2ibhMMkKvrDGKGlA/qObG4smrSqc=", - "owner": "NixOS", - "repo": "nixos-hardware", - "rev": "2d4b4717b2534fad5c715968c1cece04a172b365", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "master", - "repo": "nixos-hardware", - "type": "github" - } - }, - "nixos-images": { - "inputs": { - "nixos-stable": [ - "nixos-raspberrypi", - "nixpkgs" - ], - "nixos-unstable": [ - "nixos-raspberrypi", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1747747741, - "narHash": "sha256-LUOH27unNWbGTvZFitHonraNx0JF/55h30r9WxqrznM=", - "owner": "nvmd", - "repo": "nixos-images", - "rev": "cbbd6db325775096680b65e2a32fb6187c09bbb4", - "type": "github" - }, - "original": { - "owner": "nvmd", - "ref": "sdimage-installer", - "repo": "nixos-images", - "type": "github" - } - }, - "nixos-raspberrypi": { - "inputs": { - "argononed": "argononed", - "flake-compat": "flake-compat", - "nixos-images": "nixos-images", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1773704510, - "narHash": "sha256-Kq0WPitNekYzouyd8ROlZb63cpSg/+Ep2XxkV0YlABU=", - "owner": "nvmd", - "repo": "nixos-raspberrypi", - "rev": "b5c77d506bed55250a4642ce6c8b395dd29ef06b", - "type": "github" - }, - "original": { - "owner": "nvmd", - "ref": "main", - "repo": "nixos-raspberrypi", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1773821835, - "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", + "lastModified": 1773646010, + "narHash": "sha256-iYrs97hS7p5u4lQzuNWzuALGIOdkPXvjz7bviiBjUu8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", + "rev": "5b2c2d84341b2afb5647081c1386a80d7a8d8605", "type": "github" }, "original": { @@ -198,8 +101,6 @@ "inputs": { "agenix": "agenix", "crane": "crane", - "nixos-hardware": "nixos-hardware", - "nixos-raspberrypi": "nixos-raspberrypi", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" } @@ -211,11 +112,11 @@ ] }, "locked": { - "lastModified": 1774149071, - "narHash": "sha256-SYp8NyzwfCO3Guqmu9hPRHR1hwESlQia5nNz3lYo2qA=", + "lastModified": 1773803479, + "narHash": "sha256-GD6i1F2vrSxbsmbS92+8+x3DbHOJ+yrS78Pm4xigW4M=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "6a031966eab3bfaa19be9e261eed5b8a79c04b18", + "rev": "f17186f52e82ec5cf40920b58eac63b78692ac7c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 3484335..cc2afa3 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,6 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - nixos-hardware.url = "github:NixOS/nixos-hardware/master"; agenix = { url = "github:ryantm/agenix"; inputs.nixpkgs.follows = "nixpkgs"; @@ -13,18 +12,12 @@ url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; - nixos-raspberrypi = { - url = "github:nvmd/nixos-raspberrypi/main"; - inputs.nixpkgs.follows = "nixpkgs"; - }; }; outputs = { self, nixpkgs, - nixos-hardware, - nixos-raspberrypi, agenix, crane, rust-overlay, @@ -74,17 +67,8 @@ overlays = [ rust-overlay.overlays.default ]; }; - muslPkgs = import nixpkgs { - inherit system; - crossSystem.config = "aarch64-unknown-linux-musl"; - overlays = [ rust-overlay.overlays.default ]; - }; - piRustToolchain = pkgs.rust-bin.stable.latest.default.override { - targets = [ - "aarch64-unknown-linux-gnu" - "aarch64-unknown-linux-musl" - ]; + targets = [ "aarch64-unknown-linux-gnu" ]; }; piCraneLib = (crane.mkLib pkgs).overrideToolchain piRustToolchain; @@ -115,61 +99,6 @@ } ); - piStaticArgs = { - inherit src; - pname = "noisebell-pi-static"; - version = "0.1.0"; - strictDeps = true; - doCheck = false; - - CARGO_BUILD_TARGET = "aarch64-unknown-linux-musl"; - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER = "${muslPkgs.stdenv.cc.targetPrefix}cc"; - TARGET_CC = "${muslPkgs.stdenv.cc.targetPrefix}cc"; - CC_aarch64_unknown_linux_musl = "${muslPkgs.stdenv.cc.targetPrefix}cc"; - HOST_CC = "${pkgs.stdenv.cc.nativePrefix}cc"; - - depsBuildBuild = [ muslPkgs.stdenv.cc ]; - cargoExtraArgs = "-p noisebell"; - }; - - piStaticArtifacts = piCraneLib.buildDepsOnly piStaticArgs; - - noisebell-pi-static = piCraneLib.buildPackage ( - piStaticArgs - // { - cargoArtifacts = piStaticArtifacts; - } - ); - - piImageBaseModule = - { - lib, - nixos-raspberrypi, - pkgs, - ... - }: - { - imports = with nixos-raspberrypi.nixosModules; [ - default - usb-gadget-ethernet - ]; - - system.stateVersion = "24.11"; - - boot.loader.raspberry-pi = { - variant = "02"; - firmwarePackage = nixos-raspberrypi.packages.${pkgs.stdenv.hostPlatform.system}.raspberrypifw; - bootloader = "kernel"; - }; - boot.supportedFilesystems = lib.mkForce [ - "ext4" - "vfat" - ]; - boot.kernelParams = lib.mkAfter [ "cfg80211.ieee80211_regdom=US" ]; - - networking.networkmanager.enable = lib.mkForce false; - }; - flash-pi-sd = pkgs.writeShellApplication { name = "flash-pi-sd"; runtimeInputs = [ @@ -179,7 +108,6 @@ pkgs.parted pkgs.systemd pkgs.util-linux - pkgs.xz pkgs.zstd ]; text = '' @@ -191,6 +119,7 @@ MOUNTPOINT=${pkgs.util-linux}/bin/mountpoint FINDMNT=${pkgs.util-linux}/bin/findmnt UDEVADM=${pkgs.systemd}/bin/udevadm + ZSTD=${pkgs.zstd}/bin/zstd if [ "$#" -ne 1 ]; then echo "usage: flash-pi-sd /dev/sdX" >&2 @@ -199,17 +128,18 @@ device="$1" flake_path=${builtins.toString ./.} - zstd_bin=${pkgs.zstd}/bin/zstd - secrets_dir=${builtins.toString ./secrets} - rules_file=${builtins.toString ./secrets/secrets.nix} + image_link="$(mktemp -u /tmp/noisebell-sd-image.XXXXXX)" + mount_dir="$(mktemp -d)" + secrets_dir="${builtins.toString ./secrets}" key_name="bootstrap-identity.age" - boot_mount_dir="$(mktemp -d)" + rules_file="${builtins.toString ./secrets/secrets.nix}" cleanup() { - if "$MOUNTPOINT" -q "$boot_mount_dir"; then - sudo "$UMOUNT" "$boot_mount_dir" + if "$MOUNTPOINT" -q "$mount_dir"; then + sudo "$UMOUNT" "$mount_dir" fi - rm -rf "$boot_mount_dir" + rm -rf "$mount_dir" + rm -f "$image_link" } trap cleanup EXIT @@ -223,30 +153,20 @@ *[0-9]) boot_part="''${device}p1" ;; esac - echo "Requesting sudo access before build and flash..." - sudo -v - echo "Sudo authentication successful." + echo "Building bootstrap SD image..." + nix build "$flake_path#nixosConfigurations.bootstrap.config.system.build.sdImage" -o "$image_link" - echo "Building full Pi NixOS image..." - image_out="$(nix build \ - --print-out-paths \ - --cores 0 \ - --max-jobs auto \ - "$flake_path#nixosConfigurations.pi.config.system.build.sdImage")" - - image="$(echo "$image_out"/sd-image/*.img*)" + image="$(echo "$image_link"/sd-image/*.img*)" if [ ! -f "$image" ]; then - echo "failed to locate SD image under $image_out/sd-image" >&2 + echo "failed to locate SD image under $image_link/sd-image" >&2 exit 1 fi echo "Flashing $image to $device..." if [ "''${image##*.}" = "zst" ]; then - "$zstd_bin" -d --stdout "$image" | sudo dd of="$device" bs=16M conv=fsync status=progress - elif [ "''${image##*.}" = "xz" ]; then - xz -d -c "$image" | sudo dd of="$device" bs=16M conv=fsync status=progress + "$ZSTD" -d --stdout "$image" | sudo dd of="$device" bs=4M conv=fsync status=progress else - sudo dd if="$image" of="$device" bs=16M conv=fsync status=progress + sudo dd if="$image" of="$device" bs=4M conv=fsync status=progress fi sync @@ -258,104 +178,26 @@ fi echo "Installing bootstrap age identity onto $boot_part..." - sudo "$MOUNT" "$boot_part" "$boot_mount_dir" + sudo "$MOUNT" "$boot_part" "$mount_dir" ( cd "$secrets_dir" RULES="$rules_file" agenix -d "$key_name" - ) | sudo tee "$boot_mount_dir/noisebell-bootstrap.agekey" >/dev/null - sudo chmod 600 "$boot_mount_dir/noisebell-bootstrap.agekey" + ) | sudo tee "$mount_dir/noisebell-bootstrap.agekey" >/dev/null + sudo chmod 600 "$mount_dir/noisebell-bootstrap.agekey" sync - echo "Done. This is the full Pi NixOS image." - ''; - }; - - pi-serial = pkgs.writeShellApplication { - name = "pi-serial"; - runtimeInputs = [ - pkgs.coreutils - pkgs.procps - pkgs.tio - pkgs.util-linux - ]; - text = '' - set -euo pipefail - - baud_rate=115200 - data_bits=8 - stop_bits=1 - parity=none - flow_control=none - serial_tools="screen tio minicom picocom" - - port="" - - if [ "$#" -gt 1 ]; then - echo "usage: pi-serial [device]" >&2 - exit 1 - fi - - if [ "$#" -eq 1 ]; then - port="$1" - else - for candidate in /dev/serial/by-id/* /dev/ttyUSB* /dev/ttyACM*; do - if [ -e "$candidate" ]; then - port="$candidate" - break - fi - done - fi - - if [ -z "$port" ]; then - echo "No serial device found." >&2 - echo "Check the adapter and run: ls -l /dev/serial/by-id /dev/ttyUSB* /dev/ttyACM* 2>/dev/null" >&2 - exit 1 - fi - - log_file="pi-serial-$(date +%Y%m%d-%H%M%S).log" - - echo "Stopping old serial sessions for this user" - for tool in $serial_tools; do - pkill -x -u "$USER" "$tool" 2>/dev/null || true - done - sleep 1 - - echo "Waiting for port to become free: $port" - while fuser "$port" >/dev/null 2>&1; do - sleep 1 - done - - echo "Using serial port: $port" - echo "Logging to: $log_file" - echo "Start this before powering the Pi." - - exec sudo tio \ - -b "$baud_rate" \ - -d "$data_bits" \ - -s "$stop_bits" \ - -p "$parity" \ - -f "$flow_control" \ - -t \ - --log \ - --log-file "$log_file" \ - "$port" + echo "Done. You can now move the card to the Pi and boot it." ''; }; in { packages.${system} = { - inherit - noisebell-cache - noisebell-discord - flash-pi-sd - pi-serial - ; + inherit noisebell-cache noisebell-discord flash-pi-sd; default = noisebell-cache; }; packages.aarch64-linux = { noisebell = noisebell-pi; - noisebell-static = noisebell-pi-static; default = noisebell-pi; }; @@ -366,52 +208,51 @@ imports = [ (import ./remote/cache-service/module.nix noisebell-cache) (import ./remote/discord-bot/module.nix noisebell-discord) - (import ./remote/hosted-module.nix { - inherit self agenix; - }) ]; }; }; - nixosConfigurations.pi = nixos-raspberrypi.lib.nixosSystem { - specialArgs = { - inherit nixos-raspberrypi; - }; + nixosConfigurations.pi = nixpkgs.lib.nixosSystem { + system = "aarch64-linux"; modules = [ - nixos-raspberrypi.nixosModules.sd-image agenix.nixosModules.default - piImageBaseModule (import ./pi/module.nix { pkg = noisebell-pi; rev = self.shortRev or "dirty"; }) ./pi/configuration.nix + ./pi/hardware-configuration.nix + ]; + }; + + nixosConfigurations.bootstrap = nixpkgs.lib.nixosSystem { + system = "aarch64-linux"; + modules = [ + agenix.nixosModules.default + (import ./pi/module.nix { + pkg = noisebell-pi; + rev = self.shortRev or "dirty"; + }) + ./pi/bootstrap.nix ]; }; devShells.${system}.default = craneLib.devShell { packages = [ - agenix.packages.${system}.default flash-pi-sd - pi-serial pkgs.nix pkgs.parted pkgs.rust-analyzer - pkgs.tio + pkgs.systemd + pkgs.util-linux pkgs.zstd + agenix.packages.${system}.default ]; }; - apps.${system} = { - flash-pi-sd = { - type = "app"; - program = "${flash-pi-sd}/bin/flash-pi-sd"; - }; - - pi-serial = { - type = "app"; - program = "${pi-serial}/bin/pi-serial"; - }; + apps.${system}.flash-pi-sd = { + type = "app"; + program = "${flash-pi-sd}/bin/flash-pi-sd"; }; }; } diff --git a/pi/README.md b/pi/README.md index edf2640..7343fe1 100644 --- a/pi/README.md +++ b/pi/README.md @@ -1,181 +1,145 @@ # Pi -Rust service and deployment workflow for the Raspberry Pi at Noisebridge. +Rust service and NixOS config for the Raspberry Pi at Noisebridge. Reads a magnetic door sensor via GPIO, serves the current state over HTTP, and pushes changes to the cache service. -The current recommended setup is: +Runs NixOS with Tailscale for remote access and agenix for secrets. -1. run Raspberry Pi OS Lite on the Pi -2. keep the Pi itself free of Nix -3. build a static `aarch64` Noisebell binary on your laptop with Nix -4. copy the binary, secrets, and systemd service to the Pi over SSH +## How it works -This avoids the Raspberry Pi Zero 2 W NixOS boot issues while still keeping the application build reproducible. +The service watches a GPIO pin for rising/falling edges with a configurable debounce. When the door state changes, it: -## What stays on Raspberry Pi OS +1. Updates in-memory state (atomics) +2. POSTs `{"status": "open", "timestamp": ...}` to the cache service with a Bearer token +3. Retries with exponential backoff on failure -- bootloader -- kernel -- firmware -- Wi-Fi and local networking -- SSH base access -- Tailscale package/runtime -- Avahi package/runtime +On startup it also syncs the initial state. -## What Nix manages +## Setup -- building a static `noisebell` binary for `aarch64-linux` -- the exact app binary you deploy -- encrypted secrets in the repo -- repeatable deployment from your laptop +### Prerequisites -## Initial Pi OS setup +If you're building on an x86_64 machine, you need binfmt emulation for aarch64. On NixOS, add this to your system config and rebuild: -### 1. Flash Raspberry Pi OS Lite - -```sh -curl -L "https://downloads.raspberrypi.org/raspios_lite_arm64_latest" | xz -d -c | sudo dd of=/dev/sdb bs=16M conv=fsync status=progress && sync +```nix +boot.binfmt.emulatedSystems = [ "aarch64-linux" ]; ``` -### 2. Configure the flashed SD card +### 1. Flash the SD card -Configure it for: - -- Wi-Fi on `Noisebridge` -- SSH enabled -- serial enabled if you want a recovery console - -The helper script is: +Preferred: one command builds the bootstrap image, writes it to the SD card, and installs the +bootstrap agenix identity onto the boot partition so the full Pi system can come up on first boot: ```sh -sudo scripts/configure-pios-sd.sh /run/media/jet/bootfs /run/media/jet/rootfs +nix run .#flash-pi-sd -- /dev/sdX ``` -This setup expects SSH key login for user `pi`; it does not configure a password. +This bootstrap image already includes the normal Noisebell service, Tailscale, and the Pi config. -### 3. Boot the Pi and verify SSH - -After boot, verify SSH works: +Manual build if you need it: ```sh -ssh pi@noisebridge-pi.local +nix build .#nixosConfigurations.bootstrap.config.system.build.sdImage +dd if=result/sd-image/*.img of=/dev/sdX bs=4M status=progress ``` -## Add the Pi host key to age recipients +Boot the Pi. It connects to the Noisebridge WiFi automatically. -The deploy flow decrypts secrets locally on your laptop, but the Pi host key should still be a recipient for the Pi-facing secrets so the repo stays accurate. +### 2. SSH host key -Grab the Pi host key: +Grab the key and add it to `secrets/secrets.nix`: ```sh ssh-keyscan noisebridge-pi.local 2>/dev/null | grep ed25519 ``` -Add that key to `secrets/secrets.nix` for: +```nix +# secrets/secrets.nix +let + pi = "ssh-ed25519 AAAA..."; +in +{ + "api-key.age".publicKeys = [ pi ]; + "inbound-api-key.age".publicKeys = [ pi ]; + "tailscale-auth-key.age".publicKeys = [ pi ]; +} +``` -- `pi-to-cache-key.age` -- `cache-to-pi-key.age` -- `tailscale-auth-key.age` +### 3. Create secrets -Then refresh recipients if needed: +```sh +cd secrets +agenix -e api-key.age # key for POSTing to the cache +agenix -e inbound-api-key.age # key the cache uses to poll us +agenix -e tailscale-auth-key.age # tailscale auth key +``` + +### 4. Bootstrap agenix identity + +The Pi uses a dedicated bootstrap age identity stored at `/boot/noisebell-bootstrap.agekey` to +decrypt its runtime secrets, so first boot does not depend on the machine's freshly generated SSH +host key. + +To refresh recipients after changing `secrets/secrets.nix`: ```sh cd secrets agenix -r ``` -## Edit secrets +If you use `nix run .#flash-pi-sd -- /dev/sdX`, this file is installed automatically. + +To install the bootstrap identity manually onto a flashed card before first boot: ```sh cd secrets -agenix -e pi-to-cache-key.age -agenix -e cache-to-pi-key.age -agenix -e tailscale-auth-key.age +agenix -d bootstrap-identity.age > /boot/noisebell-bootstrap.agekey +chmod 600 /boot/noisebell-bootstrap.agekey ``` -These stay encrypted in git. The deploy script decrypts them locally on your laptop and copies the plaintext files to the Pi as root-only files. +### 5. SSH access -## Deploy to Raspberry Pi OS +Add your public key to `configuration.nix`: -From your laptop: +```nix +users.users.root.openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAA..." +]; +``` + +### 6. Deploy + +After first boot, the Pi should already be running the normal service stack from the flashed image. +Use this only for later updates: ```sh -scripts/deploy-pios-pi.sh pi@noisebridge-pi.local +nixos-rebuild switch --flake .#pi --target-host root@noisebell ``` -If you only know the IP: +## Configuration -```sh -scripts/deploy-pios-pi.sh pi@10.21.x.x -``` +Options under `services.noisebell` in `flake.nix`: -That script: - -1. builds `.#packages.aarch64-linux.noisebell-static` locally -2. decrypts the Pi-facing secrets locally with `agenix` -3. uploads the binary and secrets to the Pi -4. installs Tailscale and Avahi if needed -5. writes `/etc/noisebell/noisebell.env` -6. installs `noisebell.service` -7. enables and starts the service -8. runs `tailscale up` with the decrypted auth key - -## Files written on the Pi - -The deploy script creates: - -- `/opt/noisebell/releases//noisebell` -- `/opt/noisebell/current` -> current release symlink -- `/etc/noisebell/pi-to-cache-key` -- `/etc/noisebell/cache-to-pi-key` -- `/etc/noisebell/tailscale-auth-key` -- `/etc/noisebell/noisebell.env` -- `/etc/systemd/system/noisebell.service` - -All secret files are root-only. - -## Tailscale - -Tailscale is kept on Raspberry Pi OS rather than NixOS. - -The deploy script: - -- installs the Tailscale package if missing -- enables `tailscaled` -- runs `tailscale up --auth-key=... --hostname=noisebridge-pi` - -So Tailscale stays part of the base OS, while its auth key is still managed as an encrypted `age` secret in this repo. - -## Later updates - -Normal iteration is just rerunning the deploy script: - -```sh -scripts/deploy-pios-pi.sh pi@noisebridge-pi.local -``` - -That rebuilds the binary locally, uploads a new release, refreshes secrets, and restarts the service. - -## Service configuration - -The deployed service uses these environment variables: - -| Variable | Default | Description | +| Option | Default | Description | |---|---|---| -| `NOISEBELL_GPIO_PIN` | `17` | GPIO pin number | -| `NOISEBELL_DEBOUNCE_SECS` | `5` | Debounce delay in seconds | -| `NOISEBELL_PORT` | `80` | HTTP server port | -| `NOISEBELL_ENDPOINT_URL` | required | Webhook URL to POST state changes to | -| `NOISEBELL_RETRY_ATTEMPTS` | `3` | Webhook retry count | -| `NOISEBELL_RETRY_BASE_DELAY_SECS` | `1` | Exponential backoff base delay | -| `NOISEBELL_HTTP_TIMEOUT_SECS` | `10` | Outbound request timeout | -| `NOISEBELL_BIND_ADDRESS` | `0.0.0.0` | HTTP bind address | -| `NOISEBELL_ACTIVE_LOW` | `true` | Low GPIO = door open | +| `endpointUrl` | required | Webhook URL to POST state changes to | +| `apiKeyFile` | required | Outbound API key file (agenix secret) | +| `inboundApiKeyFile` | required | Inbound API key file for GET auth | +| `gpioPin` | `17` | GPIO pin number | +| `debounceSecs` | `5` | Debounce delay in seconds | +| `port` | `8080` | HTTP server port | +| `retryAttempts` | `3` | Webhook retry count | +| `retryBaseDelaySecs` | `1` | Exponential backoff base delay | +| `httpTimeoutSecs` | `10` | Outbound request timeout | +| `bindAddress` | `0.0.0.0` | HTTP bind address | +| `activeLow` | `true` | Low GPIO = door open (depends on wiring) | +| `restartDelaySecs` | `5` | systemd restart delay on failure | +| `watchdogSecs` | `30` | systemd watchdog timeout | ## API All endpoints require `Authorization: Bearer `. -**`GET /`** +**`GET /`** — door state ```json {"status": "open", "timestamp": 1710000000} diff --git a/pi/bootstrap.nix b/pi/bootstrap.nix new file mode 100644 index 0000000..1cab7ea --- /dev/null +++ b/pi/bootstrap.nix @@ -0,0 +1,8 @@ +{ modulesPath, ... }: + +{ + imports = [ + "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" + ./configuration.nix + ]; +} diff --git a/pi/configuration.nix b/pi/configuration.nix index cc61965..872d9d4 100644 --- a/pi/configuration.nix +++ b/pi/configuration.nix @@ -10,11 +10,7 @@ networks."Noisebridge".psk = "noisebridge"; }; - services.avahi = { - enable = true; - nssmdns4 = true; - openFirewall = true; - }; + services.avahi.enable = false; # Decrypted at runtime by agenix age.identityPaths = [ @@ -34,23 +30,11 @@ inboundApiKeyFile = config.age.secrets.cache-to-pi-key.path; }; - hardware.enableRedistributableFirmware = true; - nix.settings.experimental-features = [ "nix-command" "flakes" ]; - boot.kernelParams = [ - "console=ttyS0,115200n8" - "console=ttyAMA0,115200n8" - "console=tty0" - "boot.shell_on_fail" - "loglevel=7" - "systemd.log_level=debug" - "systemd.log_target=console" - ]; - services.tailscale = { enable = true; authKeyFile = config.age.secrets.tailscale-auth-key.path; @@ -58,18 +42,6 @@ services.openssh.enable = true; - system.activationScripts.pi-zero-2-dtb-compat.text = '' - for dtb_dir in /boot/nixos/*-dtbs/broadcom; do - if [ -d "$dtb_dir" ]; then - if [ -f "$dtb_dir/bcm2837-rpi-zero-2-w.dtb" ] && [ ! -e "$dtb_dir/bcm2837-rpi-zero-2.dtb" ]; then - cp "$dtb_dir/bcm2837-rpi-zero-2-w.dtb" "$dtb_dir/bcm2837-rpi-zero-2.dtb" - elif [ -f "$dtb_dir/bcm2837-rpi-3-b.dtb" ] && [ ! -e "$dtb_dir/bcm2837-rpi-zero-2.dtb" ]; then - cp "$dtb_dir/bcm2837-rpi-3-b.dtb" "$dtb_dir/bcm2837-rpi-zero-2.dtb" - fi - fi - done - ''; - networking.firewall = { trustedInterfaces = [ "tailscale0" ]; allowedUDPPorts = [ config.services.tailscale.port ]; diff --git a/pi/hardware-configuration.nix b/pi/hardware-configuration.nix index cb2a179..ba7d2c6 100644 --- a/pi/hardware-configuration.nix +++ b/pi/hardware-configuration.nix @@ -1,10 +1,4 @@ -{ - config, - lib, - pkgs, - modulesPath, - ... -}: +{ config, lib, pkgs, modulesPath, ... }: { imports = [ ]; diff --git a/pi/result b/pi/result new file mode 120000 index 0000000..941df79 --- /dev/null +++ b/pi/result @@ -0,0 +1 @@ +/nix/store/pmrzmz2b2hsffk62icl3c0ni56gpi3qs-nixos-image-sd-card-26.05.20260308.9dcb002-aarch64-linux.img.zst \ No newline at end of file diff --git a/remote/README.md b/remote/README.md index f1171e7..7c3bcd4 100644 --- a/remote/README.md +++ b/remote/README.md @@ -25,11 +25,11 @@ nix build .#noisebell-discord ## NixOS deployment -The flake exports a NixOS module for the hosted remote machine. It imports `agenix`, declares the Noisebell secrets from `secrets/*.age`, and wires the cache and Discord services together with sensible defaults. Each service runs as a hardened systemd unit behind Caddy. +The flake exports NixOS modules. Each service runs as a hardened systemd unit behind Caddy. ```nix { - inputs.noisebell.url = "git+https://git.extremist.software/jet/noisebell"; + inputs.noisebell.url = "git+https://git.extremist.software/jet/noisebell?dir=remote"; outputs = { self, nixpkgs, noisebell, ... }: { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { @@ -41,11 +41,19 @@ The flake exports a NixOS module for the hosted remote machine. It imports `agen enable = true; domain = "cache.noisebell.example.com"; piAddress = "http://noisebell-pi:80"; + piApiKeyFile = "/run/secrets/noisebell-pi-api-key"; + inboundApiKeyFile = "/run/secrets/noisebell-inbound-api-key"; + outboundWebhooks = [{ + url = "http://localhost:3001/webhook"; + secretFile = "/run/secrets/noisebell-discord-webhook-secret"; + }]; }; services.noisebell-discord = { enable = true; domain = "discord.noisebell.example.com"; + discordTokenFile = "/run/secrets/noisebell-discord-token"; channelId = "123456789012345678"; + webhookSecretFile = "/run/secrets/noisebell-discord-webhook-secret"; }; }) ]; @@ -53,14 +61,3 @@ The flake exports a NixOS module for the hosted remote machine. It imports `agen }; } ``` - -`nixosModules.default` handles these secrets automatically: - -| Secret file | Deployed on | Used for | -|-------------|-------------|----------| -| `secrets/pi-to-cache-key.age` | Pi + remote | Pi authenticates to cache `/webhook` | -| `secrets/cache-to-pi-key.age` | Pi + remote | cache authenticates to Pi GET endpoints | -| `secrets/discord-webhook-secret.age` | remote | cache authenticates to Discord bot `/webhook` | -| `secrets/discord-token.age` | remote | Discord bot login | - -When `extremist-software` builds a system using the Noisebell flake input, Nix uses the checked-out flake source for that input. The module points `agenix` at encrypted files inside that Noisebell source tree, such as `${inputs.noisebell}/secrets/discord-token.age`. At activation time `agenix` decrypts them locally on the target host into runtime paths like `/run/agenix/noisebell-discord-token`. The service modules then read those local decrypted files when systemd starts them. diff --git a/remote/hosted-module.nix b/remote/hosted-module.nix deleted file mode 100644 index 04ca977..0000000 --- a/remote/hosted-module.nix +++ /dev/null @@ -1,61 +0,0 @@ -{ self, agenix }: -{ config, lib, ... }: - -let - cfgCache = config.services.noisebell-cache; - cfgDiscord = config.services.noisebell-discord; -in -{ - imports = [ agenix.nixosModules.default ]; - - users.groups.noisebell = { }; - - users.users.noisebell-cache.extraGroups = lib.mkIf cfgCache.enable [ "noisebell" ]; - users.users.noisebell-discord.extraGroups = lib.mkIf cfgDiscord.enable [ "noisebell" ]; - - age.secrets.noisebell-pi-to-cache-key = { - file = "${self}/secrets/pi-to-cache-key.age"; - group = "noisebell"; - mode = "0440"; - }; - - age.secrets.noisebell-cache-to-pi-key = { - file = "${self}/secrets/cache-to-pi-key.age"; - group = "noisebell"; - mode = "0440"; - }; - - age.secrets.noisebell-discord-token = { - file = "${self}/secrets/discord-token.age"; - group = "noisebell"; - mode = "0440"; - }; - - age.secrets.noisebell-discord-webhook-secret = { - file = "${self}/secrets/discord-webhook-secret.age"; - group = "noisebell"; - mode = "0440"; - }; - - services.noisebell-cache = lib.mkIf cfgCache.enable { - piApiKeyFile = lib.mkDefault config.age.secrets.noisebell-cache-to-pi-key.path; - inboundApiKeyFile = lib.mkDefault config.age.secrets.noisebell-pi-to-cache-key.path; - outboundWebhooks = lib.mkDefault ( - lib.optional cfgDiscord.enable { - url = "http://127.0.0.1:${toString cfgDiscord.port}/webhook"; - secretFile = config.age.secrets.noisebell-discord-webhook-secret.path; - } - ); - }; - - services.noisebell-discord = lib.mkIf cfgDiscord.enable ( - { - discordTokenFile = lib.mkDefault config.age.secrets.noisebell-discord-token.path; - webhookSecretFile = lib.mkDefault config.age.secrets.noisebell-discord-webhook-secret.path; - } - // lib.optionalAttrs cfgCache.enable { - cacheUrl = lib.mkDefault "http://127.0.0.1:${toString cfgCache.port}"; - imageBaseUrl = lib.mkDefault "https://${cfgCache.domain}/image"; - } - ); -} diff --git a/scripts/configure-pios-sd.sh b/scripts/configure-pios-sd.sh deleted file mode 100755 index 43aca2c..0000000 --- a/scripts/configure-pios-sd.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BOOTFS=${1:-/run/media/jet/bootfs} -ROOTFS=${2:-/run/media/jet/rootfs} -HOSTNAME=noisebridge-pi -WIFI_SSID=Noisebridge -WIFI_PASSWORD=noisebridge -PI_USERNAME=pi -SSH_KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu' - -if [[ $EUID -ne 0 ]]; then - echo "Run with sudo: sudo $0 [bootfs] [rootfs]" >&2 - exit 1 -fi - -if [[ ! -d "$BOOTFS" ]]; then - echo "Expected mounted boot partition." >&2 - echo "Boot: $BOOTFS" >&2 - exit 1 -fi - -CONFIG_TXT="$BOOTFS/config.txt" -if [[ ! -f "$CONFIG_TXT" && -f "$BOOTFS/firmware/config.txt" ]]; then - CONFIG_TXT="$BOOTFS/firmware/config.txt" -fi - -if [[ ! -f "$CONFIG_TXT" ]]; then - echo "Could not find config.txt in $BOOTFS" >&2 - exit 1 -fi - -ROOTFS_READY=0 -if [[ -d "$ROOTFS" && -f "$ROOTFS/etc/shadow" ]]; then - ROOTFS_READY=1 -fi - -cat > "$BOOTFS/network-config" < "$BOOTFS/user-data" < "$BOOTFS/meta-data" <> "$CONFIG_TXT" -: > "$BOOTFS/ssh" - -if [[ "$ROOTFS_READY" -eq 1 ]]; then - cat > "$ROOTFS/etc/hostname" < "$ROOTFS/etc/hosts" < "$ROOTFS/home/pi/.ssh/authorized_keys" <&2 - exit 1 -fi - -if ! command -v agenix >/dev/null 2>&1; then - echo "agenix is required in your shell to decrypt secrets locally" >&2 - exit 1 -fi - -echo "Decrypting Pi secrets locally..." -( - cd "$REPO_ROOT/secrets" - RULES="$REPO_ROOT/secrets/secrets.nix" agenix -d pi-to-cache-key.age > "$TMP_DIR/pi-to-cache-key" - RULES="$REPO_ROOT/secrets/secrets.nix" agenix -d cache-to-pi-key.age > "$TMP_DIR/cache-to-pi-key" - RULES="$REPO_ROOT/secrets/secrets.nix" agenix -d tailscale-auth-key.age > "$TMP_DIR/tailscale-auth-key" -) -chmod 600 "$TMP_DIR"/* - -echo "Preparing remote directories on $TARGET_HOST..." -ssh "${SSH_OPTS[@]}" "$TARGET_HOST" "mkdir -p '$REMOTE_TMP_DIR' && rm -f '$REMOTE_TMP_DIR/noisebell' '$REMOTE_TMP_DIR/pi-to-cache-key' '$REMOTE_TMP_DIR/cache-to-pi-key' '$REMOTE_TMP_DIR/tailscale-auth-key' && sudo mkdir -p '$REMOTE_RELEASE_DIR' /etc/noisebell /opt/noisebell/releases /var/lib/noisebell" - -echo "Uploading binary and secret files..." -scp "${SSH_OPTS[@]}" "$BIN_PATH" "$TARGET_HOST:$REMOTE_TMP_DIR/noisebell" -scp "${SSH_OPTS[@]}" "$TMP_DIR/pi-to-cache-key" "$TARGET_HOST:$REMOTE_TMP_DIR/pi-to-cache-key" -scp "${SSH_OPTS[@]}" "$TMP_DIR/cache-to-pi-key" "$TARGET_HOST:$REMOTE_TMP_DIR/cache-to-pi-key" -scp "${SSH_OPTS[@]}" "$TMP_DIR/tailscale-auth-key" "$TARGET_HOST:$REMOTE_TMP_DIR/tailscale-auth-key" - -echo "Installing service and Tailscale on $TARGET_HOST..." -ssh "${SSH_OPTS[@]}" "$TARGET_HOST" "REMOTE_RELEASE_DIR='$REMOTE_RELEASE_DIR' REMOTE_CURRENT_LINK='$REMOTE_CURRENT_LINK' REMOTE_TMP_DIR='$REMOTE_TMP_DIR' bash -s" <<'EOF' -set -euo pipefail - -sudo apt-get update -sudo apt-get install -y curl rsync avahi-daemon - -if ! command -v tailscale >/dev/null 2>&1; then - curl -fsSL https://tailscale.com/install.sh | sh -fi -sudo systemctl enable --now ssh avahi-daemon tailscaled - -sudo install -m 755 "$REMOTE_TMP_DIR/noisebell" "$REMOTE_RELEASE_DIR/noisebell" -sudo mv "$REMOTE_TMP_DIR/pi-to-cache-key" /etc/noisebell/pi-to-cache-key -sudo mv "$REMOTE_TMP_DIR/cache-to-pi-key" /etc/noisebell/cache-to-pi-key -sudo mv "$REMOTE_TMP_DIR/tailscale-auth-key" /etc/noisebell/tailscale-auth-key -sudo chown root:root /etc/noisebell/pi-to-cache-key /etc/noisebell/cache-to-pi-key /etc/noisebell/tailscale-auth-key -sudo chmod 600 /etc/noisebell/pi-to-cache-key /etc/noisebell/cache-to-pi-key /etc/noisebell/tailscale-auth-key - -sudo tee /etc/noisebell/noisebell.env >/dev/null <<'ENVEOF' -NOISEBELL_GPIO_PIN=17 -NOISEBELL_DEBOUNCE_SECS=5 -NOISEBELL_PORT=80 -NOISEBELL_RETRY_ATTEMPTS=3 -NOISEBELL_RETRY_BASE_DELAY_SECS=1 -NOISEBELL_HTTP_TIMEOUT_SECS=10 -NOISEBELL_ENDPOINT_URL=https://noisebell.extremist.software/webhook -NOISEBELL_BIND_ADDRESS=0.0.0.0 -NOISEBELL_ACTIVE_LOW=true -RUST_LOG=info -ENVEOF -sudo chmod 600 /etc/noisebell/noisebell.env - -sudo tee /etc/systemd/system/noisebell.service >/dev/null </dev/null || true - -echo "Noisebell deployed on Raspberry Pi OS." -EOF - -echo "Done." diff --git a/secrets/bootstrap-identity.age b/secrets/bootstrap-identity.age index 640ad23..3912b5f 100644 Binary files a/secrets/bootstrap-identity.age and b/secrets/bootstrap-identity.age differ diff --git a/secrets/cache-to-pi-key.age b/secrets/cache-to-pi-key.age index 08fe10a..185c598 100644 Binary files a/secrets/cache-to-pi-key.age and b/secrets/cache-to-pi-key.age differ diff --git a/secrets/discord-token.age b/secrets/discord-token.age index 804e6f6..8d442f7 100644 --- a/secrets/discord-token.age +++ b/secrets/discord-token.age @@ -1,7 +1,8 @@ age-encryption.org/v1 --> ssh-ed25519 Ziw7aw 756HU1sPe5g3sa0YYfzMnXiToT5K+nfAPhfABEetgRI -E8dqhn7hN77qM0PhHMEAsZySd1hfk0w1tlsiWj4aEOQ --> ssh-ed25519 uKftJg eGfmzvHseauAFPOR1QXfdmaQy5TjpNsoBWq27mbO50w -KRuGUW65uQ5+IdREyg6X1oj0P5IkuuxFEl1WylGpAHc ---- 2Ya08payqNiMCEqBXrbKEA53ETupxwgUNRcMNu9IP6k -&vďtBL.HړY PS3T4D s,9e4Tp,G?KtJmwF' 0Xy<>\^mr \ No newline at end of file +-> ssh-ed25519 Ziw7aw W4nd317o0ON6n006hwUNIops3L7VngNqDqJ1bG7tCyg +xyjvZOOzA0u3e+Cba99LR8J5JAkl6muHuspJ+b8dEog +-> ssh-ed25519 uKftJg bX0GHUPdD4hR2yxLNx8ho689or7FNHXPA8iV9Af1q2s +B91XIdsnHfAKdKfu8jmHKKptA5OomQ5sHvXfLLUeNbs +--- 8zoH1w5ywsbDN5Xt50sE4BUgXoq12mr5b4rSVYMLEB8 +Cb;*ゴ*;5#BL^TE%Z8rwݧ#r-GKԗt| +p_ʻ\sNCUG |*i(6Ak/y \ No newline at end of file diff --git a/secrets/discord-webhook-secret.age b/secrets/discord-webhook-secret.age index 8525561..6249f12 100644 Binary files a/secrets/discord-webhook-secret.age and b/secrets/discord-webhook-secret.age differ diff --git a/secrets/pi-to-cache-key.age b/secrets/pi-to-cache-key.age index 612cf4f..fb8f9b6 100644 --- a/secrets/pi-to-cache-key.age +++ b/secrets/pi-to-cache-key.age @@ -1,12 +1,11 @@ age-encryption.org/v1 --> ssh-ed25519 Ziw7aw ZaLRvgj6V9ukim0lfxHftVUvCXi7tIXPn5O/2nzQqCE -cem5AxKMkYOs8iifYP80hkbr5km7bFOdjCt7Ym6lQcs --> ssh-ed25519 NFB4qA ssMeOzGjehzTeppIGHpzPViIKObSwnXw6OZ1DfXs6Ew -Y813udN4YGDMszEC8FVZz7Na6XQigVNFTdusLomMusg --> X25519 qmoLWSdRljn6daPlUyqk9TOOvBaUx42CvqcpXe/xUCE -7xMN5RbYnpgw3+/pHyCiEyEhyUmQOwa1zSlAbuVwlQo --> ssh-ed25519 uKftJg Fv8M0RogkcYWd46bJY3OJCoCFAW8QMjzLueDZowylSA -R3w6E2RvDmgaKKhxqWHjEeIQxNSCHzX7+nLb3Ls+iHs ---- 13dp1N6I6pPdDx+FrxsT+ZS5rsFfrK3x0F7Rs6vN6/I -BI8 -|9/tqX[ ;*ƷJ2|S($ө΁S*m \ No newline at end of file +-> ssh-ed25519 Ziw7aw TMz4xBHmy5btMk5ETWyvjZcjj0QwF3F6Iqt2QmkwVwk +KvcY409ZakEUO7OrmDrwseMQv/w7i3B7uDiXjls/5qE +-> ssh-ed25519 jcT/MQ Y5TqVlHy3fM+CmQIBu1x18C3wjHtRZCHJ9dhFyVAR0I +WPqxEZrpAm0wLYT4s9e1uxSuDjhwUwIOL4BvDtvqytU +-> X25519 ZqSHgIStt6Ru3osVvMA1M5sydoY+CeZ56temQyCFIVY +4tPA6VITlAzJxCFGVreKK1B6rrHm+ka4ELwnzYrMKbQ +-> ssh-ed25519 uKftJg OR8VgPUuEvS/0Gc5c92IlAp4DKKYcRzBbSh1tX9ddzg +DZYYx9ngwEUTmj/JaP2XnCQHjpPY8WYgOEDlOfZPLeA +--- 4iDfaqdSLiW0doVijoZC5ckxiCmsmVWJi5Kvaxic2Ng +VV;2h&e@sx#d=Y?iw½FOc/(fk틃}['=B@!w \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix index bf47478..403966d 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -1,6 +1,6 @@ let jet = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu"; - pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEfZfAQEFy8QU5P7deC2vWPN76YpUKcBF8fiWwuANumG"; + pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKmJFZAVJUjDziQ7XytVhQIv+Pm9hnbVU0flxZK17K5E"; server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB"; piBootstrap = "age1sfqn46dgztr35dtyhpzyzzam5m6kcqu495qs7fcsdxtac56pc4dsj3t862"; in diff --git a/secrets/tailscale-auth-key.age b/secrets/tailscale-auth-key.age index 355816b..c371b89 100644 Binary files a/secrets/tailscale-auth-key.age and b/secrets/tailscale-auth-key.age differ