feat: add sd flash command and rekey ages
This commit is contained in:
parent
36720e2ba5
commit
faf9701a86
11 changed files with 190 additions and 67 deletions
94
flake.nix
94
flake.nix
|
|
@ -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";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
49
pi/README.md
49
pi/README.md
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
BIN
secrets/bootstrap-identity.age
Normal file
BIN
secrets/bootstrap-identity.age
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||||
s–lHhu9û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…|Ê
|
||||||
|
pí³ÿÑòâÙ_ßÄÊ»\s»N´C‚UG
|º‡*i(6ÇÔA•k/µy
|
||||||
Binary file not shown.
|
|
@ -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Ô
|
||||||
|
|
@ -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.
Loading…
Add table
Add a link
Reference in a new issue