From faf9701a863c1d44ef263b2e8865c8bd70d4bd08 Mon Sep 17 00:00:00 2001 From: Jet Date: Sat, 21 Mar 2026 01:05:36 -0700 Subject: [PATCH] feat: add sd flash command and rekey ages --- flake.nix | 94 ++++++++++++++++++++++++++++- pi/README.md | 49 ++++++++++++--- pi/bootstrap.nix | 30 +-------- pi/configuration.nix | 21 ++++--- secrets/bootstrap-identity.age | Bin 0 -> 401 bytes secrets/cache-to-pi-key.age | Bin 476 -> 574 bytes secrets/discord-token.age | 13 ++-- secrets/discord-webhook-secret.age | Bin 366 -> 366 bytes secrets/pi-to-cache-key.age | 18 +++--- secrets/secrets.nix | 32 ++++++++-- secrets/tailscale-auth-key.age | Bin 383 -> 481 bytes 11 files changed, 190 insertions(+), 67 deletions(-) create mode 100644 secrets/bootstrap-identity.age 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 0000000000000000000000000000000000000000..3912b5f60bb2a8ae77ffaef83c3c4f03f5329af2 GIT binary patch literal 401 zcmYdHPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!+RO))YxHMCTS$}BfeELSKFOAjec zbWL^iHFga#$_)+hudFOd%5%@wPqQ${amzFCE)RCfj5Kid$mdE)&IwL8N)FFR(a%e= zNGvK1^|SB`@$ifC@%PV94b%@02{9-wst8Umapcm~)m88`cMJ3^4$v-7De)^04|Fdu z_B4vf4lXNmHc2w5$j)~$D~b%rGYK*9GU19cyt=#OdMI1tH?jT$yuy1vOH4m&F0kTX z0^7>f_D`!r{@#~6G0R=q=0pDF_TR56{w0<2+xrLGx7ucDcFITj&%aRd>9N-Agj?}P z_P1OS(QsZU%xjlmcwP6#&xFrwoi&#V%>Q1sO>?vCjr;|&mk(P;p6s`nHRCvU%F3?{ zhrI3Pq%*bum%mZMR$RMfQ^ObY(?ub=x<#Q(`xL}jn!USwcn^gC*38af+_k+fbKMvA zzn_*&`K7;)$LZ0n1tQZPw?;hrrhQj`@d>|~OYF^DKC!IY$rL1f=Z{RC&TF@bm!@|C DRD!0~ literal 0 HcmV?d00001 diff --git a/secrets/cache-to-pi-key.age b/secrets/cache-to-pi-key.age index ff1ec9e0b9afcee2a0e868f1b824f9ac066f3293..185c59893950934f01c2bda544622e24376819d5 100644 GIT binary patch delta 520 zcmcb^ypLsqPJK~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)vqa zMH$X1o}SJ=S&12$u9Zn829f%SRqmGhVUDRjVaBBq$q_+?mIdJ<6F*4QrZ(9l@%5_CwX`!2bN}5Y3FGd1ZI_Uh548ldE_`3CwlsO8D!|2 zSmdUbmsR;V=T%h{X_vc|mO6*Io0;YZ<^?Bn>FVk#xTO|4MP{XDxwr&6SLJ92x};c| zWEPs2<%fop286p5`)4JG`ji-k`I&jUa!tJc_0WvkNyoJRtuJ?9WO?yN`K7$96F=G5 z8AN_Q;i^xJlrlH|eEa^{4Ot6M{GPOGo3{=h!{N{wr)nPQACjxtot?U}RBvMQ(nEk8`PWaHwahb7D$GT10w!Qjxy5hfz*&T7h{) zGFOIskZ)F?XFZka|dmS)c0W;q4{DW&;2;pKsrrRC+t$r;AQNjYVH23)$jx(a#D z?!H+jS(Uz~MVV2>erCy;PL&?vnGwZiIVA;Y!8!h(1wMvuDXsy<`CO}RG*4#zbL{v^ zC(HlUzjX4#8K)K}tdtYcG)}+Mm}=~(DSuqxPRiNhO#9u5e{8hYuUOV@_*CQDL5<$) X9fzx#T))0!`P}s5|M!5j73-A%9`T)> 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 fc23be2f937e9df99575a55ff52e5ebbd84f2bff..6249f129d91c1740f1783b3f356f231ace492ce2 100644 GIT binary patch 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| delta 331 zcmaFI^p0tQPQAaEwzFfoi9uPppS!7Lu5U(BcxixVVyU@PmZ`U!S%znsiIG{3o12SA zB$tO*aF}m!X|Q&XQHp*}P*{1nkym9>sYOzic7Z{GX=rJHyRU^&dQiAQD3`9CLUD11 zZfc5=si~o*LaBFJiC4NpwrOy%e!hQhrK!Jnxku(N-dbAGXNhKH|@fnk`3VU+p|6G_T)Ro%70>-di<*Bj~5Hh#rFk77G-`tH2HVk eq@KE>nrqVzUOx4-JjOA(-@oiguVBm_t!@BrYI_L) 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 e262045291387621bb01497d8cad3879abab1341..c371b894149f7a3d3852335f37df1d8f4fd44cac 100644 GIT binary patch delta 447 zcmey*^pJUiPJMxYWSB>yes*G!r@m2$r$JeiWqN6NSfYoAcDi#;eqMQ5X|b_kZe)aq z3759JZ)&bdmU~)Sl4-GPa!PWUZ+W?gmql@*S5i@AxJka7e_C>IiG`b!E0?aFLUD11 zZfc5=si~o*LRNB!zHgv{YgtrYfrV*ma8yx6m|KCNYrT`ct3gm^XsEMSine=UNKjao ziI<5}UU6k0muYg4Pg0aYaDcnFNu+6cctMGGUSheIK~iZ!NJv(8Vy3gcb6TpiWv*8; z$hruKO{QkWp(%zDMnP`DCY3(^mIcl!rQVfEF8Lmn<{=hUVZ{}BdEV|R#VH2mT*cn? z;RfcemAR!ZZblxyDM79Usg8w#EJ{C@f{d48kTYWpWb@%Dfmuj?gukJavsL~SDAKRNminlQF>*PkEOp!h;da^NJ?f}l}TWtcBYGVagI+!KuMZqX>M6E zm#(g^f^l&hzknP<9}p?_GSXQsAUW{QgeSFpp} z?{SNCJ3?=_KRN2$#^#fu+I5K|HBf}-BL9v4heuhQ_Iej|sWHcI|C^Xmdn7-LYx=J( w%%_dK9QL&-UHCCMIY{G~Vr$uk6)Tu0mLAR&F;0_jW8A;`@B4O-#fugI0N$Q`=Kufz