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; 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 in
{ {
packages.${system} = { packages.${system} = {
inherit noisebell-cache noisebell-discord; inherit noisebell-cache noisebell-discord flash-pi-sd;
default = noisebell-cache; default = noisebell-cache;
}; };
@ -136,7 +214,14 @@
nixosConfigurations.bootstrap = nixpkgs.lib.nixosSystem { nixosConfigurations.bootstrap = nixpkgs.lib.nixosSystem {
system = "aarch64-linux"; 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 { devShells.${system}.default = craneLib.devShell {
@ -145,5 +230,10 @@
agenix.packages.${system}.default 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 ### 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 ```sh
nix build .#nixosConfigurations.bootstrap.config.system.build.sdImage nix build .#nixosConfigurations.bootstrap.config.system.build.sdImage
dd if=result/sd-image/*.img of=/dev/sdX bs=4M status=progress 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 ### 2. SSH host key
```sh
ping noisebridge-pi.local
```
### 3. SSH host key
Grab the key and add it to `secrets/secrets.nix`: Grab the key and add it to `secrets/secrets.nix`:
@ -59,7 +64,7 @@ in
} }
``` ```
### 4. Create secrets ### 3. Create secrets
```sh ```sh
cd secrets 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 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 ### 5. SSH access
Add your public key to `configuration.nix`: Add your public key to `configuration.nix`:
@ -80,6 +108,9 @@ users.users.root.openssh.authorizedKeys.keys = [
### 6. Deploy ### 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 ```sh
nixos-rebuild switch --flake .#pi --target-host root@noisebell nixos-rebuild switch --flake .#pi --target-host root@noisebell
``` ```

View file

@ -1,32 +1,8 @@
{ modulesPath, ... }: { modulesPath, ... }:
{ {
imports = [ "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" ]; imports = [
"${modulesPath}/installer/sd-card/sd-image-aarch64.nix"
hardware.enableRedistributableFirmware = true; ./configuration.nix
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"
]; ];
} }

View file

@ -1,4 +1,4 @@
{ config, pkgs, ... }: { config, ... }:
{ {
system.stateVersion = "24.11"; system.stateVersion = "24.11";
@ -10,16 +10,14 @@
networks."Noisebridge".psk = "noisebridge"; networks."Noisebridge".psk = "noisebridge";
}; };
services.avahi = { services.avahi.enable = false;
enable = true;
nssmdns4 = true;
publish = {
enable = true;
addresses = true;
};
};
# Decrypted at runtime by agenix # 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.tailscale-auth-key.file = ../secrets/tailscale-auth-key.age;
age.secrets.pi-to-cache-key.file = ../secrets/pi-to-cache-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; 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; 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 = { services.tailscale = {
enable = true; enable = true;

Binary file not shown.

Binary file not shown.

View file

@ -1,7 +1,8 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 Ziw7aw luObn0XSH0tR4UpGDc2QWUFGSpwVuBuGhmgCWW/IlGs -> ssh-ed25519 Ziw7aw W4nd317o0ON6n006hwUNIops3L7VngNqDqJ1bG7tCyg
hJRSk4yw3EzD0meybEcpJ8CVmnROuriLVmTJAtd+mdM xyjvZOOzA0u3e+Cba99LR8J5JAkl6muHuspJ+b8dEog
-> ssh-ed25519 uKftJg t/1U0LiOFgtiMzxELdnv4NZKWR3O8Oj1zQKi1nWWXHg -> ssh-ed25519 uKftJg bX0GHUPdD4hR2yxLNx8ho689or7FNHXPA8iV9Af1q2s
BuBBODNVO8bq9yf5idOC7/dUTgsxPd4a56JNcbTQUIQ B91XIdsnHfAKdKfu8jmHKKptA5OomQ5sHvXfLLUeNbs
--- jhW7YACeM6wl4AUih6GQ9Qx9eaOHkNIS8BYp8vroD7k --- 8zoH1w5ywsbDN5Xt50sE4BUgXoq12mr5b4rSVYMLEB8
slHhu9ûGš¥8<êX†jï¤{^žÎ„”N o­Þ9êœÆy†ÉŠ}¤ÔBš  ¨®È}ZëE<C3AB>aܧ:Öžð#úÔpÕ8ZÛåyÚNåí3-×NdP³$JÅ€é<E282AC> PìÛ\ 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 age-encryption.org/v1
-> ssh-ed25519 Ziw7aw fWhlaYGRUca7VIr427vosdJGvOyWsywZrfhYRbV2hiU -> ssh-ed25519 Ziw7aw TMz4xBHmy5btMk5ETWyvjZcjj0QwF3F6Iqt2QmkwVwk
qUKuoolDRtKRs27nCSbzrDGO9q7JVuIK8LcyVlqFj/o KvcY409ZakEUO7OrmDrwseMQv/w7i3B7uDiXjls/5qE
-> ssh-ed25519 jcT/MQ yEqLDa+E44c/PSY4bGCHKsJiPGcPUNxE5ihFUcBRwVI -> ssh-ed25519 jcT/MQ Y5TqVlHy3fM+CmQIBu1x18C3wjHtRZCHJ9dhFyVAR0I
svHSjYLKfGvbQgQXk/P4yfo4Rh8iQP446iibaIz82Po WPqxEZrpAm0wLYT4s9e1uxSuDjhwUwIOL4BvDtvqytU
-> ssh-ed25519 uKftJg HC2fqTtYg6WDUUuXdMKwHRBvD+bDrwtiuTbNCzOUV1I -> X25519 ZqSHgIStt6Ru3osVvMA1M5sydoY+CeZ56temQyCFIVY
5UX0bDfIjN2TXfZLBy7dmy8WUuoGBmkPrcx6EH2j0WA 4tPA6VITlAzJxCFGVreKK1B6rrHm+ka4ELwnzYrMKbQ
--- uiiOp5m+x+lJR2mjawNrZgOtTs1F1EGaLKmre7BIopE -> ssh-ed25519 uKftJg OR8VgPUuEvS/0Gc5c92IlAp4DKKYcRzBbSh1tX9ddzg
¼×D<08>7|7@}TD>>[i˜_äE~Ë»0…<30>Ö´ê<C2B4>ª}0çþTã£ã.ìÝZ šmò±Øb¶E|<®½ »ñýªjó.Ž 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"; jet = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu";
pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKmJFZAVJUjDziQ7XytVhQIv+Pm9hnbVU0flxZK17K5E"; pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKmJFZAVJUjDziQ7XytVhQIv+Pm9hnbVU0flxZK17K5E";
server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB"; server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB";
piBootstrap = "age1sfqn46dgztr35dtyhpzyzzam5m6kcqu495qs7fcsdxtac56pc4dsj3t862";
in in
{ {
"pi-to-cache-key.age".publicKeys = [ jet pi server ]; "bootstrap-identity.age".publicKeys = [ jet ];
"cache-to-pi-key.age".publicKeys = [ jet pi server ]; "pi-to-cache-key.age".publicKeys = [
"tailscale-auth-key.age".publicKeys = [ jet pi ]; jet
"discord-token.age".publicKeys = [ jet server ]; pi
"discord-webhook-secret.age".publicKeys = [ jet server ]; 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.