From f4d95c595eddae5ca298915541605470a42878b6 Mon Sep 17 00:00:00 2001 From: Jet Date: Sat, 21 Mar 2026 02:09:38 -0700 Subject: [PATCH 1/2] feat!: make declarative version --- .envrc | 1 + .gitignore | 2 + README.md | 3 + flake.lock | 111 ++++++++++++++- flake.nix | 242 ++++++++++++++++++++++++--------- pi/bootstrap.nix | 8 -- pi/configuration.nix | 30 +++- pi/hardware-configuration.nix | 8 +- pi/result | 1 - remote/README.md | 23 ++-- remote/hosted-module.nix | 61 +++++++++ scripts/configure-pios-sd.sh | 98 +++++++++++++ secrets/tailscale-auth-key.age | Bin 481 -> 483 bytes 13 files changed, 493 insertions(+), 95 deletions(-) delete mode 100644 pi/bootstrap.nix delete mode 120000 pi/result create mode 100644 remote/hosted-module.nix create mode 100755 scripts/configure-pios-sd.sh diff --git a/.envrc b/.envrc index 3550a30..862b788 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ +export NIX_CONFIG="eval-cache = false" use flake diff --git a/.gitignore b/.gitignore index 1d082b3..f9f13d5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ target/ result result-* .direnv +pi-serial-*.log +serial-*.log diff --git a/README.md b/README.md index 0edbc0f..27c4a66 100644 --- a/README.md +++ b/README.md @@ -16,5 +16,8 @@ Pi (door sensor) ──webhook──> Cache ──webhook──> Discord |-----------|------------| | [`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 40cda20..b21c346 100644 --- a/flake.lock +++ b/flake.lock @@ -23,6 +23,22 @@ "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, @@ -60,6 +76,21 @@ "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": [ @@ -81,13 +112,79 @@ "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": 1773646010, - "narHash": "sha256-iYrs97hS7p5u4lQzuNWzuALGIOdkPXvjz7bviiBjUu8=", + "lastModified": 1773821835, + "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5b2c2d84341b2afb5647081c1386a80d7a8d8605", + "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", "type": "github" }, "original": { @@ -101,6 +198,8 @@ "inputs": { "agenix": "agenix", "crane": "crane", + "nixos-hardware": "nixos-hardware", + "nixos-raspberrypi": "nixos-raspberrypi", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" } @@ -112,11 +211,11 @@ ] }, "locked": { - "lastModified": 1773803479, - "narHash": "sha256-GD6i1F2vrSxbsmbS92+8+x3DbHOJ+yrS78Pm4xigW4M=", + "lastModified": 1774149071, + "narHash": "sha256-SYp8NyzwfCO3Guqmu9hPRHR1hwESlQia5nNz3lYo2qA=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "f17186f52e82ec5cf40920b58eac63b78692ac7c", + "rev": "6a031966eab3bfaa19be9e261eed5b8a79c04b18", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index cc2afa3..78f1a85 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,7 @@ 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"; @@ -12,12 +13,18 @@ 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, @@ -99,100 +106,191 @@ } ); - flash-pi-sd = pkgs.writeShellApplication { - name = "flash-pi-sd"; + bootstrapModule = + { + 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.hostName = "noisebridge-pi"; + networking.networkmanager.enable = lib.mkForce false; + networking.wireless = { + enable = true; + networks."Noisebridge".psk = "noisebridge"; + }; + + services.avahi = { + enable = true; + nssmdns4 = true; + openFirewall = true; + }; + + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "prohibit-password"; + }; + }; + + users.users.root.openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu" + ]; + }; + + flash-bootstrap-sd = pkgs.writeShellApplication { + name = "flash-bootstrap-sd"; runtimeInputs = [ - agenix.packages.${system}.default pkgs.coreutils pkgs.nix - pkgs.parted - pkgs.systemd - pkgs.util-linux pkgs.zstd ]; text = '' set -euo pipefail - PARTPROBE=${pkgs.parted}/bin/partprobe - MOUNT=${pkgs.util-linux}/bin/mount - UMOUNT=${pkgs.util-linux}/bin/umount - 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 + echo "usage: flash-bootstrap-sd /dev/sdX" >&2 exit 1 fi device="$1" flake_path=${builtins.toString ./.} - image_link="$(mktemp -u /tmp/noisebell-sd-image.XXXXXX)" - mount_dir="$(mktemp -d)" - secrets_dir="${builtins.toString ./secrets}" - key_name="bootstrap-identity.age" - rules_file="${builtins.toString ./secrets/secrets.nix}" - - cleanup() { - if "$MOUNTPOINT" -q "$mount_dir"; then - sudo "$UMOUNT" "$mount_dir" - fi - rm -rf "$mount_dir" - rm -f "$image_link" - } - trap cleanup EXIT + zstd_bin=${pkgs.zstd}/bin/zstd if [ ! -b "$device" ]; then echo "not a block device: $device" >&2 exit 1 fi - boot_part="''${device}1" - case "$device" in - *[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 bootstrap NixOS Raspberry Pi Zero 2 W image..." + image_out="$(nix build \ + --print-out-paths \ + --cores 0 \ + --max-jobs auto \ + "$flake_path#nixosConfigurations.bootstrap.config.system.build.sdImage")" - image="$(echo "$image_link"/sd-image/*.img*)" + image="$(echo "$image_out"/sd-image/*.img*)" if [ ! -f "$image" ]; then - echo "failed to locate SD image under $image_link/sd-image" >&2 + echo "failed to locate SD image under $image_out/sd-image" >&2 exit 1 fi echo "Flashing $image to $device..." if [ "''${image##*.}" = "zst" ]; then - "$ZSTD" -d --stdout "$image" | sudo dd of="$device" bs=4M conv=fsync status=progress + "$zstd_bin" -d --stdout "$image" | sudo dd of="$device" bs=16M conv=fsync status=progress else - sudo dd if="$image" of="$device" bs=4M conv=fsync status=progress + sudo dd if="$image" of="$device" bs=16M conv=fsync status=progress fi sync - sudo "$PARTPROBE" "$device" - sudo "$UDEVADM" settle + echo "Done. This is the custom bootstrap NixOS image." + ''; + }; - if "$FINDMNT" -rn "$boot_part" >/dev/null 2>&1; then - sudo "$UMOUNT" "$boot_part" + 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 - echo "Installing bootstrap age identity onto $boot_part..." - sudo "$MOUNT" "$boot_part" "$mount_dir" - ( - cd "$secrets_dir" - RULES="$rules_file" agenix -d "$key_name" - ) | sudo tee "$mount_dir/noisebell-bootstrap.agekey" >/dev/null - sudo chmod 600 "$mount_dir/noisebell-bootstrap.agekey" - sync + 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 - echo "Done. You can now move the card to the Pi and boot it." + 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" ''; }; in { packages.${system} = { - inherit noisebell-cache noisebell-discord flash-pi-sd; + inherit + noisebell-cache + noisebell-discord + flash-bootstrap-sd + pi-serial + ; default = noisebell-cache; }; @@ -208,6 +306,9 @@ 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; + }) ]; }; }; @@ -216,6 +317,7 @@ system = "aarch64-linux"; modules = [ agenix.nixosModules.default + nixos-hardware.nixosModules.raspberry-pi-3 (import ./pi/module.nix { pkg = noisebell-pi; rev = self.shortRev or "dirty"; @@ -225,34 +327,38 @@ ]; }; - nixosConfigurations.bootstrap = nixpkgs.lib.nixosSystem { - system = "aarch64-linux"; + nixosConfigurations.bootstrap = nixos-raspberrypi.lib.nixosSystem { + specialArgs = { + inherit nixos-raspberrypi; + }; modules = [ - agenix.nixosModules.default - (import ./pi/module.nix { - pkg = noisebell-pi; - rev = self.shortRev or "dirty"; - }) - ./pi/bootstrap.nix + nixos-raspberrypi.nixosModules.sd-image + bootstrapModule ]; }; devShells.${system}.default = craneLib.devShell { packages = [ - flash-pi-sd + flash-bootstrap-sd + pi-serial pkgs.nix pkgs.parted pkgs.rust-analyzer - pkgs.systemd - pkgs.util-linux + pkgs.tio pkgs.zstd - agenix.packages.${system}.default ]; }; - apps.${system}.flash-pi-sd = { - type = "app"; - program = "${flash-pi-sd}/bin/flash-pi-sd"; + apps.${system} = { + flash-bootstrap-sd = { + type = "app"; + program = "${flash-bootstrap-sd}/bin/flash-bootstrap-sd"; + }; + + pi-serial = { + type = "app"; + program = "${pi-serial}/bin/pi-serial"; + }; }; }; } diff --git a/pi/bootstrap.nix b/pi/bootstrap.nix deleted file mode 100644 index 1cab7ea..0000000 --- a/pi/bootstrap.nix +++ /dev/null @@ -1,8 +0,0 @@ -{ modulesPath, ... }: - -{ - imports = [ - "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" - ./configuration.nix - ]; -} diff --git a/pi/configuration.nix b/pi/configuration.nix index 872d9d4..cc61965 100644 --- a/pi/configuration.nix +++ b/pi/configuration.nix @@ -10,7 +10,11 @@ networks."Noisebridge".psk = "noisebridge"; }; - services.avahi.enable = false; + services.avahi = { + enable = true; + nssmdns4 = true; + openFirewall = true; + }; # Decrypted at runtime by agenix age.identityPaths = [ @@ -30,11 +34,23 @@ 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; @@ -42,6 +58,18 @@ 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 ba7d2c6..cb2a179 100644 --- a/pi/hardware-configuration.nix +++ b/pi/hardware-configuration.nix @@ -1,4 +1,10 @@ -{ config, lib, pkgs, modulesPath, ... }: +{ + config, + lib, + pkgs, + modulesPath, + ... +}: { imports = [ ]; diff --git a/pi/result b/pi/result deleted file mode 120000 index 941df79..0000000 --- a/pi/result +++ /dev/null @@ -1 +0,0 @@ -/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 7c3bcd4..f1171e7 100644 --- a/remote/README.md +++ b/remote/README.md @@ -25,11 +25,11 @@ nix build .#noisebell-discord ## NixOS deployment -The flake exports NixOS modules. Each service runs as a hardened systemd unit behind Caddy. +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. ```nix { - inputs.noisebell.url = "git+https://git.extremist.software/jet/noisebell?dir=remote"; + inputs.noisebell.url = "git+https://git.extremist.software/jet/noisebell"; outputs = { self, nixpkgs, noisebell, ... }: { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { @@ -41,19 +41,11 @@ The flake exports NixOS modules. Each service runs as a hardened systemd unit be 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"; }; }) ]; @@ -61,3 +53,14 @@ The flake exports NixOS modules. Each service runs as a hardened systemd unit be }; } ``` + +`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 new file mode 100644 index 0000000..04ca977 --- /dev/null +++ b/remote/hosted-module.nix @@ -0,0 +1,61 @@ +{ 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 new file mode 100755 index 0000000..b4501a4 --- /dev/null +++ b/scripts/configure-pios-sd.sh @@ -0,0 +1,98 @@ +#!/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 +PI_PASSWORD=noisebridge +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" || ! -d "$ROOTFS" ]]; then + echo "Expected mounted boot and root partitions." >&2 + echo "Boot: $BOOTFS" >&2 + echo "Root: $ROOTFS" >&2 + exit 1 +fi + +PASSWORD_HASH=$(mkpasswd -m sha-512 "$PI_PASSWORD") + +cat > "$BOOTFS/network-config" < "$BOOTFS/user-data" < "$BOOTFS/meta-data" <> "$BOOTFS/config.txt" +: > "$BOOTFS/ssh" +cat > "$BOOTFS/userconf.txt" < "$ROOTFS/etc/hostname" < "$ROOTFS/etc/hosts" < "$ROOTFS/home/pi/.ssh/authorized_keys" <0@@yLql>MnIsuMNzR|g}c5%x~Y#-h>x}-m#&>cadC!j zYKoDmsiCDpR&t2GZ=gb^w!T?NP)4P9ep?*)7J)^P{-J?|A&F*Hg%uT%o*@N(X5l6wPHs`Ai3K6F*@eNy9_e`jX+gzh1^!vV#gSpfW+gsl7MUqUxd9dh9_8MST)Mit3f}3)hEXBz z>G|fl9)$rGUX`Jx<|SSsMah0<#U>S%Rh|J(d8P#c#esQ|Ts&!c0vDJboH%rUNBp5V zZf6R#^G}>+*!0VIQ%EV(f(;X28+U3=TxB`^c=w89<>FIj_v}x1dAjA_?t`VtWq$H} o#}4hE`=XltzxE%$os(+b9NB$Veb&WT!?kbuvW)03d#%p8x;= delta 446 zcmaFN{E&HqZoPkGm`9?1c4CpIzEOy$L0Ob#dTDrAqKAiex^qr`UU^w*v9V!pWQ2za zm$tiaYOYC^dscadC!j zYKoDmsiCDpR&t2GZ=ix}SyWzug=uPVR8d8kTY;ggQ@y^cK~QFBsIym!wtHbnP*|3U zmx)teab+NvX>yQHQj|e(fV;Ozq-lA0L5X)>V!4+=QfWa*NLF@YrnA0tTB@^Uu2(Y1 zx(J9(re?*VDTWb7L2kh&l|KHK1387AmVyOCV|bLG}seLLT7f2wr#(i!tDz0XP)Y85uBrXJg5^W)@k-Yv Date: Sun, 22 Mar 2026 23:24:05 -0700 Subject: [PATCH 2/2] feat: deploy onto the pi and add scripts for boot --- README.md | 2 +- flake.nix | 155 ++++++++++++++------- pi/README.md | 212 +++++++++++++++++------------ scripts/configure-pios-sd.sh | 46 ++++--- scripts/deploy-full-pi.sh | 6 + scripts/deploy-pios-pi.sh | 117 ++++++++++++++++ secrets/bootstrap-identity.age | Bin 401 -> 401 bytes secrets/cache-to-pi-key.age | Bin 574 -> 574 bytes secrets/discord-token.age | 13 +- secrets/discord-webhook-secret.age | Bin 366 -> 366 bytes secrets/pi-to-cache-key.age | 21 +-- secrets/secrets.nix | 2 +- secrets/tailscale-auth-key.age | Bin 483 -> 483 bytes 13 files changed, 399 insertions(+), 175 deletions(-) create mode 100755 scripts/deploy-full-pi.sh create mode 100755 scripts/deploy-pios-pi.sh diff --git a/README.md b/README.md index 27c4a66..ec121f4 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Pi (door sensor) ──webhook──> Cache ──webhook──> Discord | Directory | What it is | |-----------|------------| -| [`pi/`](pi/) | NixOS config + Rust service for the Pi | +| [`pi/`](pi/) | Raspberry Pi OS base with laptop-built Noisebell deploy | | [`remote/`](remote/) | Server-side services (cache and Discord bot) | | [`secrets/`](secrets/) | Shared agenix-encrypted secrets and recipient rules | diff --git a/flake.nix b/flake.nix index 78f1a85..3484335 100644 --- a/flake.nix +++ b/flake.nix @@ -74,8 +74,17 @@ 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" ]; + targets = [ + "aarch64-unknown-linux-gnu" + "aarch64-unknown-linux-musl" + ]; }; piCraneLib = (crane.mkLib pkgs).overrideToolchain piRustToolchain; @@ -106,7 +115,33 @@ } ); - bootstrapModule = + 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, @@ -132,66 +167,72 @@ ]; boot.kernelParams = lib.mkAfter [ "cfg80211.ieee80211_regdom=US" ]; - networking.hostName = "noisebridge-pi"; networking.networkmanager.enable = lib.mkForce false; - networking.wireless = { - enable = true; - networks."Noisebridge".psk = "noisebridge"; - }; - - services.avahi = { - enable = true; - nssmdns4 = true; - openFirewall = true; - }; - - services.openssh = { - enable = true; - settings = { - PasswordAuthentication = false; - PermitRootLogin = "prohibit-password"; - }; - }; - - users.users.root.openssh.authorizedKeys.keys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu" - ]; }; - flash-bootstrap-sd = pkgs.writeShellApplication { - name = "flash-bootstrap-sd"; + flash-pi-sd = pkgs.writeShellApplication { + name = "flash-pi-sd"; runtimeInputs = [ + agenix.packages.${system}.default pkgs.coreutils pkgs.nix + pkgs.parted + pkgs.systemd + pkgs.util-linux + pkgs.xz pkgs.zstd ]; text = '' set -euo pipefail + PARTPROBE=${pkgs.parted}/bin/partprobe + MOUNT=${pkgs.util-linux}/bin/mount + UMOUNT=${pkgs.util-linux}/bin/umount + MOUNTPOINT=${pkgs.util-linux}/bin/mountpoint + FINDMNT=${pkgs.util-linux}/bin/findmnt + UDEVADM=${pkgs.systemd}/bin/udevadm + if [ "$#" -ne 1 ]; then - echo "usage: flash-bootstrap-sd /dev/sdX" >&2 + echo "usage: flash-pi-sd /dev/sdX" >&2 exit 1 fi device="$1" flake_path=${builtins.toString ./.} zstd_bin=${pkgs.zstd}/bin/zstd + secrets_dir=${builtins.toString ./secrets} + rules_file=${builtins.toString ./secrets/secrets.nix} + key_name="bootstrap-identity.age" + boot_mount_dir="$(mktemp -d)" + + cleanup() { + if "$MOUNTPOINT" -q "$boot_mount_dir"; then + sudo "$UMOUNT" "$boot_mount_dir" + fi + rm -rf "$boot_mount_dir" + } + trap cleanup EXIT if [ ! -b "$device" ]; then echo "not a block device: $device" >&2 exit 1 fi + boot_part="''${device}1" + case "$device" in + *[0-9]) boot_part="''${device}p1" ;; + esac + echo "Requesting sudo access before build and flash..." sudo -v echo "Sudo authentication successful." - echo "Building bootstrap NixOS Raspberry Pi Zero 2 W image..." + echo "Building full Pi NixOS image..." image_out="$(nix build \ --print-out-paths \ --cores 0 \ --max-jobs auto \ - "$flake_path#nixosConfigurations.bootstrap.config.system.build.sdImage")" + "$flake_path#nixosConfigurations.pi.config.system.build.sdImage")" image="$(echo "$image_out"/sd-image/*.img*)" if [ ! -f "$image" ]; then @@ -202,12 +243,30 @@ 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 else sudo dd if="$image" of="$device" bs=16M conv=fsync status=progress fi sync - echo "Done. This is the custom bootstrap NixOS image." + sudo "$PARTPROBE" "$device" + sudo "$UDEVADM" settle + + if "$FINDMNT" -rn "$boot_part" >/dev/null 2>&1; then + sudo "$UMOUNT" "$boot_part" + fi + + echo "Installing bootstrap age identity onto $boot_part..." + sudo "$MOUNT" "$boot_part" "$boot_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" + sync + + echo "Done. This is the full Pi NixOS image." ''; }; @@ -288,7 +347,7 @@ inherit noisebell-cache noisebell-discord - flash-bootstrap-sd + flash-pi-sd pi-serial ; default = noisebell-cache; @@ -296,6 +355,7 @@ packages.aarch64-linux = { noisebell = noisebell-pi; + noisebell-static = noisebell-pi-static; default = noisebell-pi; }; @@ -313,33 +373,26 @@ }; }; - nixosConfigurations.pi = nixpkgs.lib.nixosSystem { - system = "aarch64-linux"; - modules = [ - agenix.nixosModules.default - nixos-hardware.nixosModules.raspberry-pi-3 - (import ./pi/module.nix { - pkg = noisebell-pi; - rev = self.shortRev or "dirty"; - }) - ./pi/configuration.nix - ./pi/hardware-configuration.nix - ]; - }; - - nixosConfigurations.bootstrap = nixos-raspberrypi.lib.nixosSystem { + nixosConfigurations.pi = nixos-raspberrypi.lib.nixosSystem { specialArgs = { inherit nixos-raspberrypi; }; modules = [ nixos-raspberrypi.nixosModules.sd-image - bootstrapModule + agenix.nixosModules.default + piImageBaseModule + (import ./pi/module.nix { + pkg = noisebell-pi; + rev = self.shortRev or "dirty"; + }) + ./pi/configuration.nix ]; }; devShells.${system}.default = craneLib.devShell { packages = [ - flash-bootstrap-sd + agenix.packages.${system}.default + flash-pi-sd pi-serial pkgs.nix pkgs.parted @@ -350,9 +403,9 @@ }; apps.${system} = { - flash-bootstrap-sd = { + flash-pi-sd = { type = "app"; - program = "${flash-bootstrap-sd}/bin/flash-bootstrap-sd"; + program = "${flash-pi-sd}/bin/flash-pi-sd"; }; pi-serial = { diff --git a/pi/README.md b/pi/README.md index 7343fe1..edf2640 100644 --- a/pi/README.md +++ b/pi/README.md @@ -1,145 +1,181 @@ # Pi -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. +Rust service and deployment workflow for the Raspberry Pi at Noisebridge. -Runs NixOS with Tailscale for remote access and agenix for secrets. +The current recommended setup is: -## How it works +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 -The service watches a GPIO pin for rising/falling edges with a configurable debounce. When the door state changes, it: +This avoids the Raspberry Pi Zero 2 W NixOS boot issues while still keeping the application build reproducible. -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 +## What stays on Raspberry Pi OS -On startup it also syncs the initial state. +- bootloader +- kernel +- firmware +- Wi-Fi and local networking +- SSH base access +- Tailscale package/runtime +- Avahi package/runtime -## Setup +## What Nix manages -### Prerequisites +- building a static `noisebell` binary for `aarch64-linux` +- the exact app binary you deploy +- encrypted secrets in the repo +- repeatable deployment from your laptop -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: +## Initial Pi OS setup -```nix -boot.binfmt.emulatedSystems = [ "aarch64-linux" ]; -``` - -### 1. Flash the SD card - -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: +### 1. Flash Raspberry Pi OS Lite ```sh -nix run .#flash-pi-sd -- /dev/sdX +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 ``` -This bootstrap image already includes the normal Noisebell service, Tailscale, and the Pi config. +### 2. Configure the flashed SD card -Manual build if you need it: +Configure it for: + +- Wi-Fi on `Noisebridge` +- SSH enabled +- serial enabled if you want a recovery console + +The helper script is: ```sh -nix build .#nixosConfigurations.bootstrap.config.system.build.sdImage -dd if=result/sd-image/*.img of=/dev/sdX bs=4M status=progress +sudo scripts/configure-pios-sd.sh /run/media/jet/bootfs /run/media/jet/rootfs ``` -Boot the Pi. It connects to the Noisebridge WiFi automatically. +This setup expects SSH key login for user `pi`; it does not configure a password. -### 2. SSH host key +### 3. Boot the Pi and verify SSH -Grab the key and add it to `secrets/secrets.nix`: +After boot, verify SSH works: + +```sh +ssh pi@noisebridge-pi.local +``` + +## Add the Pi host key to age recipients + +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. + +Grab the Pi host key: ```sh ssh-keyscan noisebridge-pi.local 2>/dev/null | grep ed25519 ``` -```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 ]; -} -``` +Add that key to `secrets/secrets.nix` for: -### 3. Create secrets +- `pi-to-cache-key.age` +- `cache-to-pi-key.age` +- `tailscale-auth-key.age` -```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`: +Then refresh recipients if needed: ```sh cd secrets agenix -r ``` -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: +## Edit secrets ```sh cd secrets -agenix -d bootstrap-identity.age > /boot/noisebell-bootstrap.agekey -chmod 600 /boot/noisebell-bootstrap.agekey +agenix -e pi-to-cache-key.age +agenix -e cache-to-pi-key.age +agenix -e tailscale-auth-key.age ``` -### 5. SSH access +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. -Add your public key to `configuration.nix`: +## Deploy to Raspberry Pi OS -```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: +From your laptop: ```sh -nixos-rebuild switch --flake .#pi --target-host root@noisebell +scripts/deploy-pios-pi.sh pi@noisebridge-pi.local ``` -## Configuration +If you only know the IP: -Options under `services.noisebell` in `flake.nix`: +```sh +scripts/deploy-pios-pi.sh pi@10.21.x.x +``` -| Option | Default | Description | +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 | |---|---|---| -| `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 | +| `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 | ## API All endpoints require `Authorization: Bearer `. -**`GET /`** — door state +**`GET /`** ```json {"status": "open", "timestamp": 1710000000} diff --git a/scripts/configure-pios-sd.sh b/scripts/configure-pios-sd.sh index b4501a4..43aca2c 100755 --- a/scripts/configure-pios-sd.sh +++ b/scripts/configure-pios-sd.sh @@ -7,7 +7,6 @@ HOSTNAME=noisebridge-pi WIFI_SSID=Noisebridge WIFI_PASSWORD=noisebridge PI_USERNAME=pi -PI_PASSWORD=noisebridge SSH_KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu' if [[ $EUID -ne 0 ]]; then @@ -15,14 +14,26 @@ if [[ $EUID -ne 0 ]]; then exit 1 fi -if [[ ! -d "$BOOTFS" || ! -d "$ROOTFS" ]]; then - echo "Expected mounted boot and root partitions." >&2 +if [[ ! -d "$BOOTFS" ]]; then + echo "Expected mounted boot partition." >&2 echo "Boot: $BOOTFS" >&2 - echo "Root: $ROOTFS" >&2 exit 1 fi -PASSWORD_HASH=$(mkpasswd -m sha-512 "$PI_PASSWORD") +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/config.txt" +grep -q '^enable_uart=1$' "$CONFIG_TXT" || printf '\nenable_uart=1\n' >> "$CONFIG_TXT" : > "$BOOTFS/ssh" -cat > "$BOOTFS/userconf.txt" < "$ROOTFS/etc/hostname" < "$ROOTFS/etc/hostname" < "$ROOTFS/etc/hosts" < "$ROOTFS/etc/hosts" < "$ROOTFS/home/pi/.ssh/authorized_keys" < "$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 3912b5f60bb2a8ae77ffaef83c3c4f03f5329af2..640ad23a39881b69d89bc2e72fe058d52b0ae751 100644 GIT binary patch delta 366 zcmbQpJdt^VPQ6Elqhq;`sb#85ct&AXUUr6`p|MH2qo1=!URqUDxOb>`RjQ%8iCa#& zD_4eRm4T5(Mu2-@R7jvtYCvwLYhIaSdSb4DVW>s9p--4udRBRIk!8NCCzr0Su7bB~ zP=HBQs;hZ&l1q|nvWuaQPhogjaG`6crBSA#S*3AiUVW&KMX0Hni2>J=Uy`DKa^Jc9 zeptOv*V>J z^Q$l2JXl#7ez_xi*M&n1le4-!FU1yAUV7>fF}d_lNmcR+li0=&mHV8QXuUDDz4URW z#jaahKiRL?Q&({${mpsl6^&=!bb77Z+`<3trL^Fwe(#nuB;cMmae+&mbY79^nCC&xi!A|iz@CZ WEaS2%e$*}EoVj<=AJ)0oHv#}iyP#qK delta 366 zcmbQpJdt^VPJMA$dPr%aYpSEKv1^D?ZfJmiWo1cHo_n@_nuS4*Tb_Y;d9YJvq=BnP zK37U|PH?(Wa(G6HeqNGAVo_WDcD{>QQGH}Uo=J#-mkC#t;nm$G*F)JF zzlrr9;1%BUSz`KGbAc8A64+L*wtre3^7p>niCON-HXrgYxBq@s@h_>A-`+pizSTBM zvr|6GfBuDvPmi@`C)|oZvcKhuh=%h*VP3lg!|S>?ekOcg>#Vs{VE*^2ZJL{9Z{#nK zy?od*@?^iotQp7Cxl>ktWjN$*Hz%E`{lEN;61L*nEt?v?n4c~R(bX*qW!k48#?tKF z-NSnz{I_Oy4&$!vb(!nFu>bwEWXdo7eLPN&ZY>a*_P90T(Kqe8`ioEa&0Jz{=JJVU V)lQ}$;X8k1>U3VaMZ7e<3jl~Ao5lbD diff --git a/secrets/cache-to-pi-key.age b/secrets/cache-to-pi-key.age index 185c59893950934f01c2bda544622e24376819d5..08fe10ae918510ff7a7cf622926a5509cda90d05 100644 GIT binary patch delta 541 zcmdnTvX5ngPJKpRvXif~SyWMFg?FZtk*|eed19VHd2V=VNK!^*j%P}#Z@znAc9Ex> zBUgE$zkimie~M9%fq!A8i(z7Fc0_1NX^Kx#d0pl8K|Cey(L`D3`9CLUD11 zZfc5=si~o*f}fj{Nui@cy0@2SUTJuSVPK(tWmRNgNWFegRaBI3N`7c|azuu^XM|^- zQ?`GBnPFu)S5l?7c8+(1k+wyddq`rkWm=hIvRP1BvQuDDKvji_e^OCKaA|2+qM1u1 z$hruKO&%p_*&YVvp2bm7o;leDmbuxbMqWXdjse*vmf5*3sb)p4=HB|5CEhu{T%MNo z$@vlK#wo7m1%^HW&Vk;B+HQ`Oi507@%)zk{MxAZ(wBMpA%M?>s#UJo2Q?nZCK(H>{ptb&84fWt5E9d=w2D@=3nXJ zZ{!mh;^~^~YUE;JSy1NU7HH;{8)@ki&Sibk-dDhW>7|-|3$D311-Kn3 z{{KH?wmwVRi;}&01>4+{ygr???KsrJaE;yj-qz!s-iaG#{;Uz3aE8}!lbv{tekOa{ P>HB>?OFhnu&YA-NfYG_j delta 541 zcmdnTvX5ngPJK~cc(|u&nYm?TMM+tjZ-z^%Z;EMIl2e(nqnA%eKtxbwfMt2GZ+^b3 zFPBqZphZQ9i(i$Qc9yS+Phn}enYpEZVzx`Vah6A9Nw!mxS3#adm432$GMBEMLUD11 zZfc5=si~o*LRNB!zHgvHZemD^pNmOIcxF*Uh)IsCQ;Az( zre%6^q>-U7SCMOeS$=L>W{9J&x1*7HX?U?oT5@EnQ)PZcQD{h{cao`ZWo}-CSz&-L z$hruKO(s6U9@_cEzDCZT#X+9IiMi#KrWwIWzJ{en9>!S(-bF=5j+WZSjwQx{T)vt0 zMH$X1o}SJ=S&12$u9Zn829f%SRqmGhVUDRjVaBBq$q_+?mIdJ<79cB-eN^h5R^pYe zkYbWvUf>d(t?loZ>snl9W*(GX5M_`aR90BzoaEt^99WuJrJbi;5SUfY73O1J{DVG=4a;Z$~E!&*F!UECmqxNx4ztck>$l7 z<(KlZPW)tJXAt@MgsVO=Qp();^X>a*H)Jh5@q5y$ZQeS342MI-W}K?IG@nJBZL=QZ P2esayvaK~D9sQ>PLNUE0 diff --git a/secrets/discord-token.age b/secrets/discord-token.age index 8d442f7..804e6f6 100644 --- a/secrets/discord-token.age +++ b/secrets/discord-token.age @@ -1,8 +1,7 @@ age-encryption.org/v1 --> 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 +-> 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 diff --git a/secrets/discord-webhook-secret.age b/secrets/discord-webhook-secret.age index 6249f129d91c1740f1783b3f356f231ace492ce2..85255615a5089a6073490b121a5d6e237b73da6f 100644 GIT binary patch delta 331 zcmaFI^p0tQPJL07es-d{rFo^RcXqgWMNoM~Qm~h|rJqw}u8F>jkyEL&MOI*0epTfD3`9CLUD11 zZfc5=si~o*LaBFJiC4Npic@}GQKX+ouA6pNS+JRYQoWN;MnJxkS&4B$sfDAyxp}02 zg}b3&kgK5uSDJUKNrpvQcxaGQYH*67Sz4yMdxlY3rN6&vfn!xcd7hzhQe;Vye9wLlIOZ=Z{+_4{*`%u f-kU9{Jgt#`exA|U*5prLJ8wDp2kD&MKSLA%;7ofV delta 331 zcmaFI^p0tQPQ7-8Z+?JdmW8`|nnYL+AW}c&`t6O13fWEoDqlZ~e zHdkdtl!;4JaBgOrmw$wbaY3+QRY`iDV`gHmr)x&3TU5HIp=qFTpjWDEAeXM4LUD11 zZfc5=si~o*LaBFJiC4OUwvkJEpmADYSy7l%m6yLkcD=T9pn17-T55W!afEhdZlrmp zMVN=Bqrb5$m%fu>j(KH)Nx5aPVL?V@R)AkoYMyzCk5N@tdZ}Nzqq(C&a89y+rJ;)_ zm#(g^g1&LGM`XHDsij+qzDHhQky(m&zL9rEa++C6Rz`?#RJliKSaw#Lep+!PmxDRy zln4L*Z*`IScyQg96efms6OEo$aTP}FGZ)x2hxg}^?i8)rA&+XhpPI0XxExr@aE9Xw fQ?~y6+ndGfAFjA`wIpix{5<=ezb1XEJa-BJ5H5G| diff --git a/secrets/pi-to-cache-key.age b/secrets/pi-to-cache-key.age index fb8f9b6..612cf4f 100644 --- a/secrets/pi-to-cache-key.age +++ b/secrets/pi-to-cache-key.age @@ -1,11 +1,12 @@ age-encryption.org/v1 --> 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 +-> 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 diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 403966d..bf47478 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -1,6 +1,6 @@ let jet = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu"; - pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKmJFZAVJUjDziQ7XytVhQIv+Pm9hnbVU0flxZK17K5E"; + pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEfZfAQEFy8QU5P7deC2vWPN76YpUKcBF8fiWwuANumG"; server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB"; piBootstrap = "age1sfqn46dgztr35dtyhpzyzzam5m6kcqu495qs7fcsdxtac56pc4dsj3t862"; in diff --git a/secrets/tailscale-auth-key.age b/secrets/tailscale-auth-key.age index fd46f19d97d26beea0e69d7a227c46e2c7b10671..355816b8f7835b68845817886c2b15e671248496 100644 GIT binary patch delta 449 zcmaFN{Fr%yPJL-&a%e_Ms9~{(XSSPDxQB&Xv4Lr}qq$e1MW{<@qI+UiSaM#DS%pPJ zIag&tSW<;WKv}p)QB``0L3UcPe`bhVMVfw~Q(&;UX>ht*X>qt)N|Ik-IhU@TLUD11 zZfc5=si~o*f}fj{Nui^HYiWhCQ>CkSeujCzi%WizVZC{fS)q2GQ)*ehe^`ljP)?4K ziMFx7WsXrKS4Mh}ho4VbP_R#xhet`CyJw1zYjB`nWV%;jMpm|Ao=aGPQBg&XbE&o? z$hruKP3a--+5Wi(h5lYu{>cHs#StzUCKcwnkztPIP8OcI8Bu=aUZsAS#m<50Tp_vj z9w|=l*_Cc3j^5hAuHliPmger>-sYx0dHG2`{^gz}W<`dket}L#`CPiXx(eCap$17+ zmDv{QA^8R+$xi7-MZrM@0foM1{wZZeF1apYCXT6&rB#7B23+br-%KN=B;@rES*=p} zzq!P73)6vhPo5W_@VV-0QsT1IHPPkIO{<-zzt;w;u}xZ^eMF>cdfSILpQ~Lq`_6o_ o-f>r2&AjvZyZ7$?ut-_?yzfH3_cQ;OnEsL1IC*eUg511+0A3-a>Hq)$ delta 449 zcmaFN{Fr%yPJKbJqnWc|xL0UImUFgYR8?3;p@DW#mS?0#V3LuklbK&xMue$Ps7Xk< z30I_dXlhlNOHQbPi=(5Vv5Td*xx0C;XGTDvyG2p4UxmBALAt4rQ;3hYBbTn7LUD11 zZfc5=si~o*LRNB!zHgvHrnbIWNKi(lcYa#Fds&EORei2+PL88VmPJs3X=R$DW1(ZZ zQF>rlvZt{lmv=ytV_vbTr9oA(Wq4jlV5CV}K)O#_g|TO3YI0^lrile1?tzBMTplL% zfktVLCfS9-#UAN-0ck(zj$FFBx(eRu#)eTL z?&))h9-KIIe@FbG zIc{ePwDV7#X4v%0cvDCz(}E2XUmJI7O|)7y{do6^W98ygX7}t*cX_(y-|mB@$z^`> pe8&#$pZlVk{lE4fznzn6-W=I|R(;mf2#wVqA7rOSF?BLU0s!W