feat: add sd flash command and rekey ages

This commit is contained in:
Jet 2026-03-21 01:05:36 -07:00
parent 36720e2ba5
commit faf9701a86
No known key found for this signature in database
11 changed files with 190 additions and 67 deletions

View file

@ -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";
};
};
}

View file

@ -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
```

View file

@ -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
];
}

View file

@ -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;

Binary file not shown.

Binary file not shown.

View file

@ -1,7 +1,8 @@
age-encryption.org/v1
-> ssh-ed25519 Ziw7aw luObn0XSH0tR4UpGDc2QWUFGSpwVuBuGhmgCWW/IlGs
hJRSk4yw3EzD0meybEcpJ8CVmnROuriLVmTJAtd+mdM
-> ssh-ed25519 uKftJg t/1U0LiOFgtiMzxELdnv4NZKWR3O8Oj1zQKi1nWWXHg
BuBBODNVO8bq9yf5idOC7/dUTgsxPd4a56JNcbTQUIQ
--- jhW7YACeM6wl4AUih6GQ9Qx9eaOHkNIS8BYp8vroD7k
slHhu9ûGš¥8<êX†jï¤{^žÎ„”N o­Þ9êœÆy†ÉŠ}¤ÔBš  ¨®È}ZëE<C3AB>aܧ:Öžð#úÔpÕ8ZÛåyÚNåí3-×NdP³$JÅ€é<E282AC> PìÛ\
-> ssh-ed25519 Ziw7aw W4nd317o0ON6n006hwUNIops3L7VngNqDqJ1bG7tCyg
xyjvZOOzA0u3e+Cba99LR8J5JAkl6muHuspJ+b8dEog
-> ssh-ed25519 uKftJg bX0GHUPdD4hR2yxLNx8ho689or7FNHXPA8iV9Af1q2s
B91XIdsnHfAKdKfu8jmHKKptA5OomQ5sHvXfLLUeNbs
--- 8zoH1w5ywsbDN5Xt50sE4BUgXoq12mr5b4rSVYMLEB8
CŽb;*œã‚´*;í5œ#BL^ñƒT¯úE¢<45>%ÚÎçáZ8†r‰wݧ#r-îG¢éÖKÔ—t…|Ê
³ÿÑòâÙ_ßÄÊ»\s»N´CUG |º‡*i(6ÇÔA•k/µy

Binary file not shown.

View file

@ -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
¼×D<08>7|7@}TD>>[i˜_äE~Ë»0…<30>Ö´ê<C2B4>ª}0çþTã£ã.ìÝZ šmò±Øb¶E|<®½ »ñýªjó.Ž
-> 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;2†h<>&e@sÏx#d=Y?iðw½´ÉÍF<15>OÝc×/ûš(™¨àœ›¼føkØíƒ—¿}[¡¤'=B@ô‡¨Â!wÔ

View file

@ -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
];
}

Binary file not shown.