feat: deploy onto the pi and add scripts for boot

This commit is contained in:
Jet 2026-03-22 23:24:05 -07:00
parent f4d95c595e
commit 16ad3c6181
No known key found for this signature in database
13 changed files with 399 additions and 175 deletions

View file

@ -14,7 +14,7 @@ Pi (door sensor) ──webhook──> Cache ──webhook──> Discord
| Directory | What it is | | Directory | What it is |
|-----------|------------| |-----------|------------|
| [`pi/`](pi/) | NixOS config + Rust service for the Pi | | [`pi/`](pi/) | Raspberry Pi OS base with laptop-built Noisebell deploy |
| [`remote/`](remote/) | Server-side services (cache and Discord bot) | | [`remote/`](remote/) | Server-side services (cache and Discord bot) |
| [`secrets/`](secrets/) | Shared agenix-encrypted secrets and recipient rules | | [`secrets/`](secrets/) | Shared agenix-encrypted secrets and recipient rules |

155
flake.nix
View file

@ -74,8 +74,17 @@
overlays = [ rust-overlay.overlays.default ]; overlays = [ rust-overlay.overlays.default ];
}; };
muslPkgs = import nixpkgs {
inherit system;
crossSystem.config = "aarch64-unknown-linux-musl";
overlays = [ rust-overlay.overlays.default ];
};
piRustToolchain = pkgs.rust-bin.stable.latest.default.override { piRustToolchain = pkgs.rust-bin.stable.latest.default.override {
targets = [ "aarch64-unknown-linux-gnu" ]; targets = [
"aarch64-unknown-linux-gnu"
"aarch64-unknown-linux-musl"
];
}; };
piCraneLib = (crane.mkLib pkgs).overrideToolchain piRustToolchain; piCraneLib = (crane.mkLib pkgs).overrideToolchain piRustToolchain;
@ -106,7 +115,33 @@
} }
); );
bootstrapModule = piStaticArgs = {
inherit src;
pname = "noisebell-pi-static";
version = "0.1.0";
strictDeps = true;
doCheck = false;
CARGO_BUILD_TARGET = "aarch64-unknown-linux-musl";
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER = "${muslPkgs.stdenv.cc.targetPrefix}cc";
TARGET_CC = "${muslPkgs.stdenv.cc.targetPrefix}cc";
CC_aarch64_unknown_linux_musl = "${muslPkgs.stdenv.cc.targetPrefix}cc";
HOST_CC = "${pkgs.stdenv.cc.nativePrefix}cc";
depsBuildBuild = [ muslPkgs.stdenv.cc ];
cargoExtraArgs = "-p noisebell";
};
piStaticArtifacts = piCraneLib.buildDepsOnly piStaticArgs;
noisebell-pi-static = piCraneLib.buildPackage (
piStaticArgs
// {
cargoArtifacts = piStaticArtifacts;
}
);
piImageBaseModule =
{ {
lib, lib,
nixos-raspberrypi, nixos-raspberrypi,
@ -132,66 +167,72 @@
]; ];
boot.kernelParams = lib.mkAfter [ "cfg80211.ieee80211_regdom=US" ]; boot.kernelParams = lib.mkAfter [ "cfg80211.ieee80211_regdom=US" ];
networking.hostName = "noisebridge-pi";
networking.networkmanager.enable = lib.mkForce false; networking.networkmanager.enable = lib.mkForce false;
networking.wireless = {
enable = true;
networks."Noisebridge".psk = "noisebridge";
}; };
services.avahi = { flash-pi-sd = pkgs.writeShellApplication {
enable = true; name = "flash-pi-sd";
nssmdns4 = true;
openFirewall = true;
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "prohibit-password";
};
};
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu"
];
};
flash-bootstrap-sd = pkgs.writeShellApplication {
name = "flash-bootstrap-sd";
runtimeInputs = [ runtimeInputs = [
agenix.packages.${system}.default
pkgs.coreutils pkgs.coreutils
pkgs.nix pkgs.nix
pkgs.parted
pkgs.systemd
pkgs.util-linux
pkgs.xz
pkgs.zstd pkgs.zstd
]; ];
text = '' text = ''
set -euo pipefail set -euo pipefail
PARTPROBE=${pkgs.parted}/bin/partprobe
MOUNT=${pkgs.util-linux}/bin/mount
UMOUNT=${pkgs.util-linux}/bin/umount
MOUNTPOINT=${pkgs.util-linux}/bin/mountpoint
FINDMNT=${pkgs.util-linux}/bin/findmnt
UDEVADM=${pkgs.systemd}/bin/udevadm
if [ "$#" -ne 1 ]; then if [ "$#" -ne 1 ]; then
echo "usage: flash-bootstrap-sd /dev/sdX" >&2 echo "usage: flash-pi-sd /dev/sdX" >&2
exit 1 exit 1
fi fi
device="$1" device="$1"
flake_path=${builtins.toString ./.} flake_path=${builtins.toString ./.}
zstd_bin=${pkgs.zstd}/bin/zstd zstd_bin=${pkgs.zstd}/bin/zstd
secrets_dir=${builtins.toString ./secrets}
rules_file=${builtins.toString ./secrets/secrets.nix}
key_name="bootstrap-identity.age"
boot_mount_dir="$(mktemp -d)"
cleanup() {
if "$MOUNTPOINT" -q "$boot_mount_dir"; then
sudo "$UMOUNT" "$boot_mount_dir"
fi
rm -rf "$boot_mount_dir"
}
trap cleanup EXIT
if [ ! -b "$device" ]; then if [ ! -b "$device" ]; then
echo "not a block device: $device" >&2 echo "not a block device: $device" >&2
exit 1 exit 1
fi fi
boot_part="''${device}1"
case "$device" in
*[0-9]) boot_part="''${device}p1" ;;
esac
echo "Requesting sudo access before build and flash..." echo "Requesting sudo access before build and flash..."
sudo -v sudo -v
echo "Sudo authentication successful." echo "Sudo authentication successful."
echo "Building bootstrap NixOS Raspberry Pi Zero 2 W image..." echo "Building full Pi NixOS image..."
image_out="$(nix build \ image_out="$(nix build \
--print-out-paths \ --print-out-paths \
--cores 0 \ --cores 0 \
--max-jobs auto \ --max-jobs auto \
"$flake_path#nixosConfigurations.bootstrap.config.system.build.sdImage")" "$flake_path#nixosConfigurations.pi.config.system.build.sdImage")"
image="$(echo "$image_out"/sd-image/*.img*)" image="$(echo "$image_out"/sd-image/*.img*)"
if [ ! -f "$image" ]; then if [ ! -f "$image" ]; then
@ -202,12 +243,30 @@
echo "Flashing $image to $device..." echo "Flashing $image to $device..."
if [ "''${image##*.}" = "zst" ]; then if [ "''${image##*.}" = "zst" ]; then
"$zstd_bin" -d --stdout "$image" | sudo dd of="$device" bs=16M conv=fsync status=progress "$zstd_bin" -d --stdout "$image" | sudo dd of="$device" bs=16M conv=fsync status=progress
elif [ "''${image##*.}" = "xz" ]; then
xz -d -c "$image" | sudo dd of="$device" bs=16M conv=fsync status=progress
else else
sudo dd if="$image" of="$device" bs=16M conv=fsync status=progress sudo dd if="$image" of="$device" bs=16M conv=fsync status=progress
fi fi
sync sync
echo "Done. This is the custom bootstrap NixOS image." 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" "$boot_mount_dir"
(
cd "$secrets_dir"
RULES="$rules_file" agenix -d "$key_name"
) | sudo tee "$boot_mount_dir/noisebell-bootstrap.agekey" >/dev/null
sudo chmod 600 "$boot_mount_dir/noisebell-bootstrap.agekey"
sync
echo "Done. This is the full Pi NixOS image."
''; '';
}; };
@ -288,7 +347,7 @@
inherit inherit
noisebell-cache noisebell-cache
noisebell-discord noisebell-discord
flash-bootstrap-sd flash-pi-sd
pi-serial pi-serial
; ;
default = noisebell-cache; default = noisebell-cache;
@ -296,6 +355,7 @@
packages.aarch64-linux = { packages.aarch64-linux = {
noisebell = noisebell-pi; noisebell = noisebell-pi;
noisebell-static = noisebell-pi-static;
default = noisebell-pi; default = noisebell-pi;
}; };
@ -313,33 +373,26 @@
}; };
}; };
nixosConfigurations.pi = nixpkgs.lib.nixosSystem { nixosConfigurations.pi = nixos-raspberrypi.lib.nixosSystem {
system = "aarch64-linux";
modules = [
agenix.nixosModules.default
nixos-hardware.nixosModules.raspberry-pi-3
(import ./pi/module.nix {
pkg = noisebell-pi;
rev = self.shortRev or "dirty";
})
./pi/configuration.nix
./pi/hardware-configuration.nix
];
};
nixosConfigurations.bootstrap = nixos-raspberrypi.lib.nixosSystem {
specialArgs = { specialArgs = {
inherit nixos-raspberrypi; inherit nixos-raspberrypi;
}; };
modules = [ modules = [
nixos-raspberrypi.nixosModules.sd-image nixos-raspberrypi.nixosModules.sd-image
bootstrapModule agenix.nixosModules.default
piImageBaseModule
(import ./pi/module.nix {
pkg = noisebell-pi;
rev = self.shortRev or "dirty";
})
./pi/configuration.nix
]; ];
}; };
devShells.${system}.default = craneLib.devShell { devShells.${system}.default = craneLib.devShell {
packages = [ packages = [
flash-bootstrap-sd agenix.packages.${system}.default
flash-pi-sd
pi-serial pi-serial
pkgs.nix pkgs.nix
pkgs.parted pkgs.parted
@ -350,9 +403,9 @@
}; };
apps.${system} = { apps.${system} = {
flash-bootstrap-sd = { flash-pi-sd = {
type = "app"; type = "app";
program = "${flash-bootstrap-sd}/bin/flash-bootstrap-sd"; program = "${flash-pi-sd}/bin/flash-pi-sd";
}; };
pi-serial = { pi-serial = {

View file

@ -1,145 +1,181 @@
# Pi # Pi
Rust service and NixOS config for the Raspberry Pi at Noisebridge. Reads a magnetic door sensor via GPIO, serves the current state over HTTP, and pushes changes to the cache service. Rust service and deployment workflow for the Raspberry Pi at Noisebridge.
Runs NixOS with Tailscale for remote access and agenix for secrets. The current recommended setup is:
## How it works 1. run Raspberry Pi OS Lite on the Pi
2. keep the Pi itself free of Nix
3. build a static `aarch64` Noisebell binary on your laptop with Nix
4. copy the binary, secrets, and systemd service to the Pi over SSH
The service watches a GPIO pin for rising/falling edges with a configurable debounce. When the door state changes, it: This avoids the Raspberry Pi Zero 2 W NixOS boot issues while still keeping the application build reproducible.
1. Updates in-memory state (atomics) ## What stays on Raspberry Pi OS
2. POSTs `{"status": "open", "timestamp": ...}` to the cache service with a Bearer token
3. Retries with exponential backoff on failure
On startup it also syncs the initial state. - bootloader
- kernel
- firmware
- Wi-Fi and local networking
- SSH base access
- Tailscale package/runtime
- Avahi package/runtime
## Setup ## What Nix manages
### Prerequisites - building a static `noisebell` binary for `aarch64-linux`
- the exact app binary you deploy
- encrypted secrets in the repo
- repeatable deployment from your laptop
If you're building on an x86_64 machine, you need binfmt emulation for aarch64. On NixOS, add this to your system config and rebuild: ## Initial Pi OS setup
```nix ### 1. Flash Raspberry Pi OS Lite
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 ```sh
nix run .#flash-pi-sd -- /dev/sdX curl -L "https://downloads.raspberrypi.org/raspios_lite_arm64_latest" | xz -d -c | sudo dd of=/dev/sdb bs=16M conv=fsync status=progress && sync
``` ```
This bootstrap image already includes the normal Noisebell service, Tailscale, and the Pi config. ### 2. Configure the flashed SD card
Manual build if you need it: Configure it for:
- Wi-Fi on `Noisebridge`
- SSH enabled
- serial enabled if you want a recovery console
The helper script is:
```sh ```sh
nix build .#nixosConfigurations.bootstrap.config.system.build.sdImage sudo scripts/configure-pios-sd.sh /run/media/jet/bootfs /run/media/jet/rootfs
dd if=result/sd-image/*.img of=/dev/sdX bs=4M status=progress
``` ```
Boot the Pi. It connects to the Noisebridge WiFi automatically. This setup expects SSH key login for user `pi`; it does not configure a password.
### 2. SSH host key ### 3. Boot the Pi and verify SSH
Grab the key and add it to `secrets/secrets.nix`: After boot, verify SSH works:
```sh
ssh pi@noisebridge-pi.local
```
## Add the Pi host key to age recipients
The deploy flow decrypts secrets locally on your laptop, but the Pi host key should still be a recipient for the Pi-facing secrets so the repo stays accurate.
Grab the Pi host key:
```sh ```sh
ssh-keyscan noisebridge-pi.local 2>/dev/null | grep ed25519 ssh-keyscan noisebridge-pi.local 2>/dev/null | grep ed25519
``` ```
```nix Add that key to `secrets/secrets.nix` for:
# secrets/secrets.nix
let
pi = "ssh-ed25519 AAAA...";
in
{
"api-key.age".publicKeys = [ pi ];
"inbound-api-key.age".publicKeys = [ pi ];
"tailscale-auth-key.age".publicKeys = [ pi ];
}
```
### 3. Create secrets - `pi-to-cache-key.age`
- `cache-to-pi-key.age`
- `tailscale-auth-key.age`
```sh Then refresh recipients if needed:
cd secrets
agenix -e api-key.age # key for POSTing to the cache
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 ```sh
cd secrets cd secrets
agenix -r agenix -r
``` ```
If you use `nix run .#flash-pi-sd -- /dev/sdX`, this file is installed automatically. ## Edit secrets
To install the bootstrap identity manually onto a flashed card before first boot:
```sh ```sh
cd secrets cd secrets
agenix -d bootstrap-identity.age > /boot/noisebell-bootstrap.agekey agenix -e pi-to-cache-key.age
chmod 600 /boot/noisebell-bootstrap.agekey agenix -e cache-to-pi-key.age
agenix -e tailscale-auth-key.age
``` ```
### 5. SSH access These stay encrypted in git. The deploy script decrypts them locally on your laptop and copies the plaintext files to the Pi as root-only files.
Add your public key to `configuration.nix`: ## Deploy to Raspberry Pi OS
```nix From your laptop:
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAA..."
];
```
### 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 scripts/deploy-pios-pi.sh pi@noisebridge-pi.local
``` ```
## Configuration If you only know the IP:
Options under `services.noisebell` in `flake.nix`: ```sh
scripts/deploy-pios-pi.sh pi@10.21.x.x
```
| Option | Default | Description | That script:
1. builds `.#packages.aarch64-linux.noisebell-static` locally
2. decrypts the Pi-facing secrets locally with `agenix`
3. uploads the binary and secrets to the Pi
4. installs Tailscale and Avahi if needed
5. writes `/etc/noisebell/noisebell.env`
6. installs `noisebell.service`
7. enables and starts the service
8. runs `tailscale up` with the decrypted auth key
## Files written on the Pi
The deploy script creates:
- `/opt/noisebell/releases/<timestamp>/noisebell`
- `/opt/noisebell/current` -> current release symlink
- `/etc/noisebell/pi-to-cache-key`
- `/etc/noisebell/cache-to-pi-key`
- `/etc/noisebell/tailscale-auth-key`
- `/etc/noisebell/noisebell.env`
- `/etc/systemd/system/noisebell.service`
All secret files are root-only.
## Tailscale
Tailscale is kept on Raspberry Pi OS rather than NixOS.
The deploy script:
- installs the Tailscale package if missing
- enables `tailscaled`
- runs `tailscale up --auth-key=... --hostname=noisebridge-pi`
So Tailscale stays part of the base OS, while its auth key is still managed as an encrypted `age` secret in this repo.
## Later updates
Normal iteration is just rerunning the deploy script:
```sh
scripts/deploy-pios-pi.sh pi@noisebridge-pi.local
```
That rebuilds the binary locally, uploads a new release, refreshes secrets, and restarts the service.
## Service configuration
The deployed service uses these environment variables:
| Variable | Default | Description |
|---|---|---| |---|---|---|
| `endpointUrl` | required | Webhook URL to POST state changes to | | `NOISEBELL_GPIO_PIN` | `17` | GPIO pin number |
| `apiKeyFile` | required | Outbound API key file (agenix secret) | | `NOISEBELL_DEBOUNCE_SECS` | `5` | Debounce delay in seconds |
| `inboundApiKeyFile` | required | Inbound API key file for GET auth | | `NOISEBELL_PORT` | `80` | HTTP server port |
| `gpioPin` | `17` | GPIO pin number | | `NOISEBELL_ENDPOINT_URL` | required | Webhook URL to POST state changes to |
| `debounceSecs` | `5` | Debounce delay in seconds | | `NOISEBELL_RETRY_ATTEMPTS` | `3` | Webhook retry count |
| `port` | `8080` | HTTP server port | | `NOISEBELL_RETRY_BASE_DELAY_SECS` | `1` | Exponential backoff base delay |
| `retryAttempts` | `3` | Webhook retry count | | `NOISEBELL_HTTP_TIMEOUT_SECS` | `10` | Outbound request timeout |
| `retryBaseDelaySecs` | `1` | Exponential backoff base delay | | `NOISEBELL_BIND_ADDRESS` | `0.0.0.0` | HTTP bind address |
| `httpTimeoutSecs` | `10` | Outbound request timeout | | `NOISEBELL_ACTIVE_LOW` | `true` | Low GPIO = door open |
| `bindAddress` | `0.0.0.0` | HTTP bind address |
| `activeLow` | `true` | Low GPIO = door open (depends on wiring) |
| `restartDelaySecs` | `5` | systemd restart delay on failure |
| `watchdogSecs` | `30` | systemd watchdog timeout |
## API ## API
All endpoints require `Authorization: Bearer <token>`. All endpoints require `Authorization: Bearer <token>`.
**`GET /`** — door state **`GET /`**
```json ```json
{"status": "open", "timestamp": 1710000000} {"status": "open", "timestamp": 1710000000}

View file

@ -7,7 +7,6 @@ HOSTNAME=noisebridge-pi
WIFI_SSID=Noisebridge WIFI_SSID=Noisebridge
WIFI_PASSWORD=noisebridge WIFI_PASSWORD=noisebridge
PI_USERNAME=pi PI_USERNAME=pi
PI_PASSWORD=noisebridge
SSH_KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu' SSH_KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu'
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
@ -15,14 +14,26 @@ if [[ $EUID -ne 0 ]]; then
exit 1 exit 1
fi fi
if [[ ! -d "$BOOTFS" || ! -d "$ROOTFS" ]]; then if [[ ! -d "$BOOTFS" ]]; then
echo "Expected mounted boot and root partitions." >&2 echo "Expected mounted boot partition." >&2
echo "Boot: $BOOTFS" >&2 echo "Boot: $BOOTFS" >&2
echo "Root: $ROOTFS" >&2
exit 1 exit 1
fi fi
PASSWORD_HASH=$(mkpasswd -m sha-512 "$PI_PASSWORD") CONFIG_TXT="$BOOTFS/config.txt"
if [[ ! -f "$CONFIG_TXT" && -f "$BOOTFS/firmware/config.txt" ]]; then
CONFIG_TXT="$BOOTFS/firmware/config.txt"
fi
if [[ ! -f "$CONFIG_TXT" ]]; then
echo "Could not find config.txt in $BOOTFS" >&2
exit 1
fi
ROOTFS_READY=0
if [[ -d "$ROOTFS" && -f "$ROOTFS/etc/shadow" ]]; then
ROOTFS_READY=1
fi
cat > "$BOOTFS/network-config" <<EOF cat > "$BOOTFS/network-config" <<EOF
network: network:
@ -59,17 +70,15 @@ dsmode: local
instance_id: ${HOSTNAME}-bootstrap-1 instance_id: ${HOSTNAME}-bootstrap-1
EOF EOF
grep -q '^enable_uart=1$' "$BOOTFS/config.txt" || printf '\nenable_uart=1\n' >> "$BOOTFS/config.txt" grep -q '^enable_uart=1$' "$CONFIG_TXT" || printf '\nenable_uart=1\n' >> "$CONFIG_TXT"
: > "$BOOTFS/ssh" : > "$BOOTFS/ssh"
cat > "$BOOTFS/userconf.txt" <<EOF
${PI_USERNAME}:${PASSWORD_HASH}
EOF
cat > "$ROOTFS/etc/hostname" <<EOF if [[ "$ROOTFS_READY" -eq 1 ]]; then
cat > "$ROOTFS/etc/hostname" <<EOF
${HOSTNAME} ${HOSTNAME}
EOF EOF
cat > "$ROOTFS/etc/hosts" <<EOF cat > "$ROOTFS/etc/hosts" <<EOF
127.0.0.1 localhost 127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback ::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes ff02::1 ip6-allnodes
@ -78,13 +87,14 @@ ff02::2 ip6-allrouters
127.0.1.1 ${HOSTNAME} 127.0.1.1 ${HOSTNAME}
EOF EOF
mkdir -p "$ROOTFS/home/pi/.ssh" mkdir -p "$ROOTFS/home/pi/.ssh"
cat > "$ROOTFS/home/pi/.ssh/authorized_keys" <<EOF cat > "$ROOTFS/home/pi/.ssh/authorized_keys" <<EOF
${SSH_KEY} ${SSH_KEY}
EOF EOF
chown -R 1000:1000 "$ROOTFS/home/pi/.ssh" chown -R 1000:1000 "$ROOTFS/home/pi/.ssh"
chmod 700 "$ROOTFS/home/pi/.ssh" chmod 700 "$ROOTFS/home/pi/.ssh"
chmod 600 "$ROOTFS/home/pi/.ssh/authorized_keys" chmod 600 "$ROOTFS/home/pi/.ssh/authorized_keys"
fi
sync sync
@ -94,5 +104,7 @@ echo "- Wi-Fi: ${WIFI_SSID}"
echo "- SSH enabled on first boot" echo "- SSH enabled on first boot"
echo "- Serial UART enabled" echo "- Serial UART enabled"
echo "- Username: ${PI_USERNAME}" echo "- Username: ${PI_USERNAME}"
echo "- Password: ${PI_PASSWORD}"
echo "- Pi user authorized key installed" echo "- Pi user authorized key installed"
if [[ "$ROOTFS_READY" -ne 1 ]]; then
echo "- Note: rootfs was not mounted; only boot partition config was updated"
fi

6
scripts/deploy-full-pi.sh Executable file
View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
TARGET_HOST=${1:-root@noisebridge-pi.local}
exec nixos-rebuild switch --flake ".#pi" --target-host "$TARGET_HOST"

117
scripts/deploy-pios-pi.sh Executable file
View file

@ -0,0 +1,117 @@
#!/usr/bin/env bash
set -euo pipefail
TARGET_HOST=${1:-pi@noisebridge-pi.local}
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd -- "$SCRIPT_DIR/.." && pwd)
RELEASE_ID=${RELEASE_ID:-$(date +%Y%m%d-%H%M%S)}
REMOTE_RELEASE_DIR=${REMOTE_RELEASE_DIR:-/opt/noisebell/releases/$RELEASE_ID}
REMOTE_CURRENT_LINK=${REMOTE_CURRENT_LINK:-/opt/noisebell/current}
REMOTE_TMP_DIR=${REMOTE_TMP_DIR:-/home/pi/noisebell-deploy-tmp}
TMP_DIR=$(mktemp -d)
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
SSH_OPTS=(
-o StrictHostKeyChecking=accept-new
)
echo "Building static aarch64 Noisebell binary locally..."
PACKAGE_PATH=$(nix build .#packages.aarch64-linux.noisebell-static --print-out-paths --no-link)
BIN_PATH="$PACKAGE_PATH/bin/noisebell"
if [[ ! -x "$BIN_PATH" ]]; then
echo "built binary not found: $BIN_PATH" >&2
exit 1
fi
if ! command -v agenix >/dev/null 2>&1; then
echo "agenix is required in your shell to decrypt secrets locally" >&2
exit 1
fi
echo "Decrypting Pi secrets locally..."
(
cd "$REPO_ROOT/secrets"
RULES="$REPO_ROOT/secrets/secrets.nix" agenix -d pi-to-cache-key.age > "$TMP_DIR/pi-to-cache-key"
RULES="$REPO_ROOT/secrets/secrets.nix" agenix -d cache-to-pi-key.age > "$TMP_DIR/cache-to-pi-key"
RULES="$REPO_ROOT/secrets/secrets.nix" agenix -d tailscale-auth-key.age > "$TMP_DIR/tailscale-auth-key"
)
chmod 600 "$TMP_DIR"/*
echo "Preparing remote directories on $TARGET_HOST..."
ssh "${SSH_OPTS[@]}" "$TARGET_HOST" "mkdir -p '$REMOTE_TMP_DIR' && rm -f '$REMOTE_TMP_DIR/noisebell' '$REMOTE_TMP_DIR/pi-to-cache-key' '$REMOTE_TMP_DIR/cache-to-pi-key' '$REMOTE_TMP_DIR/tailscale-auth-key' && sudo mkdir -p '$REMOTE_RELEASE_DIR' /etc/noisebell /opt/noisebell/releases /var/lib/noisebell"
echo "Uploading binary and secret files..."
scp "${SSH_OPTS[@]}" "$BIN_PATH" "$TARGET_HOST:$REMOTE_TMP_DIR/noisebell"
scp "${SSH_OPTS[@]}" "$TMP_DIR/pi-to-cache-key" "$TARGET_HOST:$REMOTE_TMP_DIR/pi-to-cache-key"
scp "${SSH_OPTS[@]}" "$TMP_DIR/cache-to-pi-key" "$TARGET_HOST:$REMOTE_TMP_DIR/cache-to-pi-key"
scp "${SSH_OPTS[@]}" "$TMP_DIR/tailscale-auth-key" "$TARGET_HOST:$REMOTE_TMP_DIR/tailscale-auth-key"
echo "Installing service and Tailscale on $TARGET_HOST..."
ssh "${SSH_OPTS[@]}" "$TARGET_HOST" "REMOTE_RELEASE_DIR='$REMOTE_RELEASE_DIR' REMOTE_CURRENT_LINK='$REMOTE_CURRENT_LINK' REMOTE_TMP_DIR='$REMOTE_TMP_DIR' bash -s" <<'EOF'
set -euo pipefail
sudo apt-get update
sudo apt-get install -y curl rsync avahi-daemon
if ! command -v tailscale >/dev/null 2>&1; then
curl -fsSL https://tailscale.com/install.sh | sh
fi
sudo systemctl enable --now ssh avahi-daemon tailscaled
sudo install -m 755 "$REMOTE_TMP_DIR/noisebell" "$REMOTE_RELEASE_DIR/noisebell"
sudo mv "$REMOTE_TMP_DIR/pi-to-cache-key" /etc/noisebell/pi-to-cache-key
sudo mv "$REMOTE_TMP_DIR/cache-to-pi-key" /etc/noisebell/cache-to-pi-key
sudo mv "$REMOTE_TMP_DIR/tailscale-auth-key" /etc/noisebell/tailscale-auth-key
sudo chown root:root /etc/noisebell/pi-to-cache-key /etc/noisebell/cache-to-pi-key /etc/noisebell/tailscale-auth-key
sudo chmod 600 /etc/noisebell/pi-to-cache-key /etc/noisebell/cache-to-pi-key /etc/noisebell/tailscale-auth-key
sudo tee /etc/noisebell/noisebell.env >/dev/null <<'ENVEOF'
NOISEBELL_GPIO_PIN=17
NOISEBELL_DEBOUNCE_SECS=5
NOISEBELL_PORT=80
NOISEBELL_RETRY_ATTEMPTS=3
NOISEBELL_RETRY_BASE_DELAY_SECS=1
NOISEBELL_HTTP_TIMEOUT_SECS=10
NOISEBELL_ENDPOINT_URL=https://noisebell.extremist.software/webhook
NOISEBELL_BIND_ADDRESS=0.0.0.0
NOISEBELL_ACTIVE_LOW=true
RUST_LOG=info
ENVEOF
sudo chmod 600 /etc/noisebell/noisebell.env
sudo tee /etc/systemd/system/noisebell.service >/dev/null <<UNITEOF
[Unit]
Description=Noisebell GPIO door monitor
After=network-online.target tailscaled.service
Wants=network-online.target
[Service]
Type=notify
NotifyAccess=all
EnvironmentFile=/etc/noisebell/noisebell.env
ExecStart=/bin/bash -lc 'export NOISEBELL_API_KEY="$$(cat /etc/noisebell/pi-to-cache-key)"; export NOISEBELL_INBOUND_API_KEY="$$(cat /etc/noisebell/cache-to-pi-key)"; exec ${REMOTE_CURRENT_LINK}/noisebell'
Restart=on-failure
RestartSec=5
WatchdogSec=30
[Install]
WantedBy=multi-user.target
UNITEOF
sudo ln -sfn "$REMOTE_RELEASE_DIR" "$REMOTE_CURRENT_LINK"
sudo systemctl daemon-reload
sudo systemctl enable --now noisebell.service
sudo tailscale up --auth-key="$(sudo cat /etc/noisebell/tailscale-auth-key)" --hostname=noisebridge-pi || true
rmdir "$REMOTE_TMP_DIR" 2>/dev/null || true
echo "Noisebell deployed on Raspberry Pi OS."
EOF
echo "Done."

Binary file not shown.

Binary file not shown.

View file

@ -1,8 +1,7 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 Ziw7aw W4nd317o0ON6n006hwUNIops3L7VngNqDqJ1bG7tCyg -> ssh-ed25519 Ziw7aw 756HU1sPe5g3sa0YYfzMnXiToT5K+nfAPhfABEetgRI
xyjvZOOzA0u3e+Cba99LR8J5JAkl6muHuspJ+b8dEog E8dqhn7hN77qM0PhHMEAsZySd1hfk0w1tlsiWj4aEOQ
-> ssh-ed25519 uKftJg bX0GHUPdD4hR2yxLNx8ho689or7FNHXPA8iV9Af1q2s -> ssh-ed25519 uKftJg eGfmzvHseauAFPOR1QXfdmaQy5TjpNsoBWq27mbO50w
B91XIdsnHfAKdKfu8jmHKKptA5OomQ5sHvXfLLUeNbs KRuGUW65uQ5+IdREyg6X1oj0P5IkuuxFEl1WylGpAHc
--- 8zoH1w5ywsbDN5Xt50sE4BUgXoq12mr5b4rSVYMLEB8 --- 2Ya08payqNiMCEqBXrbKEA53ETupxwgUNRcMNu9IP6k
CŽb;*œã‚´*;í5œ#BL^ñƒT¯úE¢<45>%ÚÎçáZ8†r‰wݧ#r-îG¢éÖKÔ—t…|Ê &°‰¢ÏvÄ<76>tBšL˜.¬HéÚ“Y P®Sç3Tó¹•4䱟D¬ s,9Ñeã4TpåÌ,Gë?KtJmwF' ®0³ÉXy»<á÷<E28099>î>½\Áç^þ°m»ÜrÊÏùµŸÏü
³ÿÑòâÙ_ßÄÊ»\s»N´CUG |º‡*i(6ÇÔA•k/µy

Binary file not shown.

View file

@ -1,11 +1,12 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 Ziw7aw TMz4xBHmy5btMk5ETWyvjZcjj0QwF3F6Iqt2QmkwVwk -> ssh-ed25519 Ziw7aw ZaLRvgj6V9ukim0lfxHftVUvCXi7tIXPn5O/2nzQqCE
KvcY409ZakEUO7OrmDrwseMQv/w7i3B7uDiXjls/5qE cem5AxKMkYOs8iifYP80hkbr5km7bFOdjCt7Ym6lQcs
-> ssh-ed25519 jcT/MQ Y5TqVlHy3fM+CmQIBu1x18C3wjHtRZCHJ9dhFyVAR0I -> ssh-ed25519 NFB4qA ssMeOzGjehzTeppIGHpzPViIKObSwnXw6OZ1DfXs6Ew
WPqxEZrpAm0wLYT4s9e1uxSuDjhwUwIOL4BvDtvqytU Y813udN4YGDMszEC8FVZz7Na6XQigVNFTdusLomMusg
-> X25519 ZqSHgIStt6Ru3osVvMA1M5sydoY+CeZ56temQyCFIVY -> X25519 qmoLWSdRljn6daPlUyqk9TOOvBaUx42CvqcpXe/xUCE
4tPA6VITlAzJxCFGVreKK1B6rrHm+ka4ELwnzYrMKbQ 7xMN5RbYnpgw3+/pHyCiEyEhyUmQOwa1zSlAbuVwlQo
-> ssh-ed25519 uKftJg OR8VgPUuEvS/0Gc5c92IlAp4DKKYcRzBbSh1tX9ddzg -> ssh-ed25519 uKftJg Fv8M0RogkcYWd46bJY3OJCoCFAW8QMjzLueDZowylSA
DZYYx9ngwEUTmj/JaP2XnCQHjpPY8WYgOEDlOfZPLeA R3w6E2RvDmgaKKhxqWHjEeIQxNSCHzX7+nLb3Ls+iHs
--- 4iDfaqdSLiW0doVijoZC5ckxiCmsmVWJi5Kvaxic2Ng --- 13dp1N6I6pPdDx+FrxsT+ZS5rsFfrK3x0F7Rs6vN6/I
è÷VV;2†h<>&e@sÏx#d=Y?iðw½´ÉÍF<15>OÝc×/ûš(™¨àœ›¼føkØíƒ—¿}[¡¤'=B@ô‡¨Â!wÔ ÖBI¬8Ż
9”<39>úŠ/ěšÂÚqXžŕÄÇ[𮠼™Í ;ž§Ž†*Ć·¨»ńJ2|ŃS(đ‚$Ó©ŘÎ<>SÁ*m

View file

@ -1,6 +1,6 @@
let let
jet = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu"; jet = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu";
pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKmJFZAVJUjDziQ7XytVhQIv+Pm9hnbVU0flxZK17K5E"; pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEfZfAQEFy8QU5P7deC2vWPN76YpUKcBF8fiWwuANumG";
server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB"; server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB";
piBootstrap = "age1sfqn46dgztr35dtyhpzyzzam5m6kcqu495qs7fcsdxtac56pc4dsj3t862"; piBootstrap = "age1sfqn46dgztr35dtyhpzyzzam5m6kcqu495qs7fcsdxtac56pc4dsj3t862";
in in

Binary file not shown.