diff --git a/flake.nix b/flake.nix index af6bcb5..8880ef1 100644 --- a/flake.nix +++ b/flake.nix @@ -98,10 +98,88 @@ cargoArtifacts = piArtifacts; } ); + + flash-pi-sd = pkgs.writeShellApplication { + name = "flash-pi-sd"; + runtimeInputs = [ + agenix.packages.${system}.default + pkgs.coreutils + pkgs.nix + pkgs.systemd + pkgs.util-linux + pkgs.zstd + ]; + text = '' + set -euo pipefail + + if [ "$#" -ne 1 ]; then + echo "usage: flash-pi-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)" + key_file="${builtins.toString ./secrets/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 + + 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 "Building bootstrap SD image..." + nix build "$flake_path#nixosConfigurations.bootstrap.config.system.build.sdImage" -o "$image_link" + + image="$(echo "$image_link"/sd-image/*.img*)" + if [ ! -f "$image" ]; then + 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 -d --stdout "$image" | sudo dd of="$device" bs=4M conv=fsync status=progress + else + sudo dd if="$image" of="$device" bs=4M conv=fsync status=progress + fi + sync + + 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" "$mount_dir" + RULES="$rules_file" agenix -d "$key_file" | sudo tee "$mount_dir/noisebell-bootstrap.agekey" >/dev/null + sudo chmod 600 "$mount_dir/noisebell-bootstrap.agekey" + sync + + echo "Done. You can now move the card to the Pi and boot it." + ''; + }; in { packages.${system} = { - inherit noisebell-cache noisebell-discord; + inherit noisebell-cache noisebell-discord flash-pi-sd; default = noisebell-cache; }; @@ -136,7 +214,14 @@ nixosConfigurations.bootstrap = nixpkgs.lib.nixosSystem { system = "aarch64-linux"; - modules = [ ./pi/bootstrap.nix ]; + modules = [ + agenix.nixosModules.default + (import ./pi/module.nix { + pkg = noisebell-pi; + rev = self.shortRev or "dirty"; + }) + ./pi/bootstrap.nix + ]; }; devShells.${system}.default = craneLib.devShell { @@ -145,5 +230,10 @@ agenix.packages.${system}.default ]; }; + + 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 b567b65..7343fe1 100644 --- a/pi/README.md +++ b/pi/README.md @@ -26,20 +26,25 @@ 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: + +```sh +nix run .#flash-pi-sd -- /dev/sdX +``` + +This bootstrap image already includes the normal Noisebell service, Tailscale, and the Pi config. + +Manual build if you need it: + ```sh nix build .#nixosConfigurations.bootstrap.config.system.build.sdImage dd if=result/sd-image/*.img of=/dev/sdX bs=4M status=progress ``` -Boot the Pi. It connects to the Noisebridge WiFi automatically and is discoverable via mDNS as `noisebridge-pi.local`. +Boot the Pi. It connects to the Noisebridge WiFi automatically. -### 2. Find the Pi - -```sh -ping noisebridge-pi.local -``` - -### 3. SSH host key +### 2. SSH host key Grab the key and add it to `secrets/secrets.nix`: @@ -59,7 +64,7 @@ in } ``` -### 4. Create secrets +### 3. Create secrets ```sh cd secrets @@ -68,6 +73,29 @@ 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 +``` + +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 -d bootstrap-identity.age > /boot/noisebell-bootstrap.agekey +chmod 600 /boot/noisebell-bootstrap.agekey +``` + ### 5. SSH access Add your public key to `configuration.nix`: @@ -80,6 +108,9 @@ users.users.root.openssh.authorizedKeys.keys = [ ### 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 nixos-rebuild switch --flake .#pi --target-host root@noisebell ``` diff --git a/pi/bootstrap.nix b/pi/bootstrap.nix index 8abbd14..1cab7ea 100644 --- a/pi/bootstrap.nix +++ b/pi/bootstrap.nix @@ -1,32 +1,8 @@ { modulesPath, ... }: { - imports = [ "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" ]; - - hardware.enableRedistributableFirmware = true; - - networking.hostName = "noisebridge-pi"; - - networking.wireless = { - enable = true; - networks = { - "Noisebridge" = { - psk = "noisebridge"; - }; - }; - }; - - services.avahi = { - enable = true; - nssmdns4 = true; - publish = { - enable = true; - addresses = true; - }; - }; - - services.openssh.enable = true; - users.users.root.openssh.authorizedKeys.keys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu" + imports = [ + "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" + ./configuration.nix ]; } diff --git a/pi/configuration.nix b/pi/configuration.nix index 234d2fd..872d9d4 100644 --- a/pi/configuration.nix +++ b/pi/configuration.nix @@ -1,4 +1,4 @@ -{ config, pkgs, ... }: +{ config, ... }: { system.stateVersion = "24.11"; @@ -10,16 +10,14 @@ networks."Noisebridge".psk = "noisebridge"; }; - services.avahi = { - enable = true; - nssmdns4 = true; - publish = { - enable = true; - addresses = true; - }; - }; + services.avahi.enable = false; # Decrypted at runtime by agenix + age.identityPaths = [ + "/boot/noisebell-bootstrap.agekey" + "/etc/ssh/ssh_host_ed25519_key" + ]; + age.secrets.tailscale-auth-key.file = ../secrets/tailscale-auth-key.age; age.secrets.pi-to-cache-key.file = ../secrets/pi-to-cache-key.age; age.secrets.cache-to-pi-key.file = ../secrets/cache-to-pi-key.age; @@ -32,7 +30,10 @@ inboundApiKeyFile = config.age.secrets.cache-to-pi-key.path; }; - nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; services.tailscale = { enable = true; diff --git a/secrets/bootstrap-identity.age b/secrets/bootstrap-identity.age new file mode 100644 index 0000000..3912b5f Binary files /dev/null and b/secrets/bootstrap-identity.age differ diff --git a/secrets/cache-to-pi-key.age b/secrets/cache-to-pi-key.age index ff1ec9e..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 7028f4b..8d442f7 100644 --- a/secrets/discord-token.age +++ b/secrets/discord-token.age @@ -1,7 +1,8 @@ age-encryption.org/v1 --> ssh-ed25519 Ziw7aw luObn0XSH0tR4UpGDc2QWUFGSpwVuBuGhmgCWW/IlGs -hJRSk4yw3EzD0meybEcpJ8CVmnROuriLVmTJAtd+mdM --> ssh-ed25519 uKftJg t/1U0LiOFgtiMzxELdnv4NZKWR3O8Oj1zQKi1nWWXHg -BuBBODNVO8bq9yf5idOC7/dUTgsxPd4a56JNcbTQUIQ ---- jhW7YACeM6wl4AUih6GQ9Qx9eaOHkNIS8BYp8vroD7k -slHhu9G8 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 fc23be2..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 9caec4c..fb8f9b6 100644 --- a/secrets/pi-to-cache-key.age +++ b/secrets/pi-to-cache-key.age @@ -1,9 +1,11 @@ age-encryption.org/v1 --> ssh-ed25519 Ziw7aw fWhlaYGRUca7VIr427vosdJGvOyWsywZrfhYRbV2hiU -qUKuoolDRtKRs27nCSbzrDGO9q7JVuIK8LcyVlqFj/o --> ssh-ed25519 jcT/MQ yEqLDa+E44c/PSY4bGCHKsJiPGcPUNxE5ihFUcBRwVI -svHSjYLKfGvbQgQXk/P4yfo4Rh8iQP446iibaIz82Po --> ssh-ed25519 uKftJg HC2fqTtYg6WDUUuXdMKwHRBvD+bDrwtiuTbNCzOUV1I -5UX0bDfIjN2TXfZLBy7dmy8WUuoGBmkPrcx6EH2j0WA ---- uiiOp5m+x+lJR2mjawNrZgOtTs1F1EGaLKmre7BIopE -D7|7@}TD>>[i_E~˻0ִꝪ}0T.Z mbE|< j. \ 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 f535e36..403966d 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -2,11 +2,33 @@ let jet = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu"; pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKmJFZAVJUjDziQ7XytVhQIv+Pm9hnbVU0flxZK17K5E"; server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB"; + piBootstrap = "age1sfqn46dgztr35dtyhpzyzzam5m6kcqu495qs7fcsdxtac56pc4dsj3t862"; in { - "pi-to-cache-key.age".publicKeys = [ jet pi server ]; - "cache-to-pi-key.age".publicKeys = [ jet pi server ]; - "tailscale-auth-key.age".publicKeys = [ jet pi ]; - "discord-token.age".publicKeys = [ jet server ]; - "discord-webhook-secret.age".publicKeys = [ jet server ]; + "bootstrap-identity.age".publicKeys = [ jet ]; + "pi-to-cache-key.age".publicKeys = [ + jet + pi + piBootstrap + server + ]; + "cache-to-pi-key.age".publicKeys = [ + jet + pi + piBootstrap + server + ]; + "tailscale-auth-key.age".publicKeys = [ + jet + pi + piBootstrap + ]; + "discord-token.age".publicKeys = [ + jet + server + ]; + "discord-webhook-secret.age".publicKeys = [ + jet + server + ]; } diff --git a/secrets/tailscale-auth-key.age b/secrets/tailscale-auth-key.age index e262045..c371b89 100644 Binary files a/secrets/tailscale-auth-key.age and b/secrets/tailscale-auth-key.age differ