Compare commits

...

2 commits

Author SHA1 Message Date
Jet
16ad3c6181
feat: deploy onto the pi and add scripts for boot 2026-03-22 23:24:07 -07:00
Jet
f4d95c595e
feat!: make declarative version 2026-03-22 17:40:48 -07:00
22 changed files with 798 additions and 176 deletions

1
.envrc
View file

@ -1 +1,2 @@
export NIX_CONFIG="eval-cache = false"
use flake use flake

2
.gitignore vendored
View file

@ -2,3 +2,5 @@ target/
result result
result-* result-*
.direnv .direnv
pi-serial-*.log
serial-*.log

View file

@ -14,7 +14,10 @@ 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 |
Each directory has its own README with setup and configuration details. Each directory has its own README with setup and configuration details.
For hosted deployment, another repo such as `../extremist-software` imports `noisebell.nixosModules.default`. That host repo provides deployment-specific values like domains, ports, and the Pi address, while the Noisebell module itself points `agenix` at the encrypted files in `secrets/` and consumes the decrypted runtime files on the target machine.

111
flake.lock generated
View file

@ -23,6 +23,22 @@
"type": "github" "type": "github"
} }
}, },
"argononed": {
"flake": false,
"locked": {
"lastModified": 1729566243,
"narHash": "sha256-DPNI0Dpk5aym3Baf5UbEe5GENDrSmmXVdriRSWE+rgk=",
"owner": "nvmd",
"repo": "argononed",
"rev": "16dbee54d49b66d5654d228d1061246b440ef7cf",
"type": "github"
},
"original": {
"owner": "nvmd",
"repo": "argononed",
"type": "github"
}
},
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1773857772, "lastModified": 1773857772,
@ -60,6 +76,21 @@
"type": "github" "type": "github"
} }
}, },
"flake-compat": {
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"home-manager": { "home-manager": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@ -81,13 +112,79 @@
"type": "github" "type": "github"
} }
}, },
"nixos-hardware": {
"locked": {
"lastModified": 1774018263,
"narHash": "sha256-HHYEwK1A22aSaxv2ibhMMkKvrDGKGlA/qObG4smrSqc=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "2d4b4717b2534fad5c715968c1cece04a172b365",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "master",
"repo": "nixos-hardware",
"type": "github"
}
},
"nixos-images": {
"inputs": {
"nixos-stable": [
"nixos-raspberrypi",
"nixpkgs"
],
"nixos-unstable": [
"nixos-raspberrypi",
"nixpkgs"
]
},
"locked": {
"lastModified": 1747747741,
"narHash": "sha256-LUOH27unNWbGTvZFitHonraNx0JF/55h30r9WxqrznM=",
"owner": "nvmd",
"repo": "nixos-images",
"rev": "cbbd6db325775096680b65e2a32fb6187c09bbb4",
"type": "github"
},
"original": {
"owner": "nvmd",
"ref": "sdimage-installer",
"repo": "nixos-images",
"type": "github"
}
},
"nixos-raspberrypi": {
"inputs": {
"argononed": "argononed",
"flake-compat": "flake-compat",
"nixos-images": "nixos-images",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1773704510,
"narHash": "sha256-Kq0WPitNekYzouyd8ROlZb63cpSg/+Ep2XxkV0YlABU=",
"owner": "nvmd",
"repo": "nixos-raspberrypi",
"rev": "b5c77d506bed55250a4642ce6c8b395dd29ef06b",
"type": "github"
},
"original": {
"owner": "nvmd",
"ref": "main",
"repo": "nixos-raspberrypi",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1773646010, "lastModified": 1773821835,
"narHash": "sha256-iYrs97hS7p5u4lQzuNWzuALGIOdkPXvjz7bviiBjUu8=", "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "5b2c2d84341b2afb5647081c1386a80d7a8d8605", "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -101,6 +198,8 @@
"inputs": { "inputs": {
"agenix": "agenix", "agenix": "agenix",
"crane": "crane", "crane": "crane",
"nixos-hardware": "nixos-hardware",
"nixos-raspberrypi": "nixos-raspberrypi",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay" "rust-overlay": "rust-overlay"
} }
@ -112,11 +211,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1773803479, "lastModified": 1774149071,
"narHash": "sha256-GD6i1F2vrSxbsmbS92+8+x3DbHOJ+yrS78Pm4xigW4M=", "narHash": "sha256-SYp8NyzwfCO3Guqmu9hPRHR1hwESlQia5nNz3lYo2qA=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "f17186f52e82ec5cf40920b58eac63b78692ac7c", "rev": "6a031966eab3bfaa19be9e261eed5b8a79c04b18",
"type": "github" "type": "github"
}, },
"original": { "original": {

243
flake.nix
View file

@ -3,6 +3,7 @@
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
agenix = { agenix = {
url = "github:ryantm/agenix"; url = "github:ryantm/agenix";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
@ -12,12 +13,18 @@
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
nixos-raspberrypi = {
url = "github:nvmd/nixos-raspberrypi/main";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = outputs =
{ {
self, self,
nixpkgs, nixpkgs,
nixos-hardware,
nixos-raspberrypi,
agenix, agenix,
crane, crane,
rust-overlay, rust-overlay,
@ -67,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;
@ -99,6 +115,61 @@
} }
); );
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,
nixos-raspberrypi,
pkgs,
...
}:
{
imports = with nixos-raspberrypi.nixosModules; [
default
usb-gadget-ethernet
];
system.stateVersion = "24.11";
boot.loader.raspberry-pi = {
variant = "02";
firmwarePackage = nixos-raspberrypi.packages.${pkgs.stdenv.hostPlatform.system}.raspberrypifw;
bootloader = "kernel";
};
boot.supportedFilesystems = lib.mkForce [
"ext4"
"vfat"
];
boot.kernelParams = lib.mkAfter [ "cfg80211.ieee80211_regdom=US" ];
networking.networkmanager.enable = lib.mkForce false;
};
flash-pi-sd = pkgs.writeShellApplication { flash-pi-sd = pkgs.writeShellApplication {
name = "flash-pi-sd"; name = "flash-pi-sd";
runtimeInputs = [ runtimeInputs = [
@ -108,6 +179,7 @@
pkgs.parted pkgs.parted
pkgs.systemd pkgs.systemd
pkgs.util-linux pkgs.util-linux
pkgs.xz
pkgs.zstd pkgs.zstd
]; ];
text = '' text = ''
@ -119,7 +191,6 @@
MOUNTPOINT=${pkgs.util-linux}/bin/mountpoint MOUNTPOINT=${pkgs.util-linux}/bin/mountpoint
FINDMNT=${pkgs.util-linux}/bin/findmnt FINDMNT=${pkgs.util-linux}/bin/findmnt
UDEVADM=${pkgs.systemd}/bin/udevadm UDEVADM=${pkgs.systemd}/bin/udevadm
ZSTD=${pkgs.zstd}/bin/zstd
if [ "$#" -ne 1 ]; then if [ "$#" -ne 1 ]; then
echo "usage: flash-pi-sd /dev/sdX" >&2 echo "usage: flash-pi-sd /dev/sdX" >&2
@ -128,18 +199,17 @@
device="$1" device="$1"
flake_path=${builtins.toString ./.} flake_path=${builtins.toString ./.}
image_link="$(mktemp -u /tmp/noisebell-sd-image.XXXXXX)" zstd_bin=${pkgs.zstd}/bin/zstd
mount_dir="$(mktemp -d)" secrets_dir=${builtins.toString ./secrets}
secrets_dir="${builtins.toString ./secrets}" rules_file=${builtins.toString ./secrets/secrets.nix}
key_name="bootstrap-identity.age" key_name="bootstrap-identity.age"
rules_file="${builtins.toString ./secrets/secrets.nix}" boot_mount_dir="$(mktemp -d)"
cleanup() { cleanup() {
if "$MOUNTPOINT" -q "$mount_dir"; then if "$MOUNTPOINT" -q "$boot_mount_dir"; then
sudo "$UMOUNT" "$mount_dir" sudo "$UMOUNT" "$boot_mount_dir"
fi fi
rm -rf "$mount_dir" rm -rf "$boot_mount_dir"
rm -f "$image_link"
} }
trap cleanup EXIT trap cleanup EXIT
@ -153,20 +223,30 @@
*[0-9]) boot_part="''${device}p1" ;; *[0-9]) boot_part="''${device}p1" ;;
esac esac
echo "Building bootstrap SD image..." echo "Requesting sudo access before build and flash..."
nix build "$flake_path#nixosConfigurations.bootstrap.config.system.build.sdImage" -o "$image_link" sudo -v
echo "Sudo authentication successful."
image="$(echo "$image_link"/sd-image/*.img*)" echo "Building full Pi NixOS image..."
image_out="$(nix build \
--print-out-paths \
--cores 0 \
--max-jobs auto \
"$flake_path#nixosConfigurations.pi.config.system.build.sdImage")"
image="$(echo "$image_out"/sd-image/*.img*)"
if [ ! -f "$image" ]; then if [ ! -f "$image" ]; then
echo "failed to locate SD image under $image_link/sd-image" >&2 echo "failed to locate SD image under $image_out/sd-image" >&2
exit 1 exit 1
fi fi
echo "Flashing $image to $device..." echo "Flashing $image to $device..."
if [ "''${image##*.}" = "zst" ]; then if [ "''${image##*.}" = "zst" ]; then
"$ZSTD" -d --stdout "$image" | sudo dd of="$device" bs=4M 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=4M conv=fsync status=progress sudo dd if="$image" of="$device" bs=16M conv=fsync status=progress
fi fi
sync sync
@ -178,26 +258,104 @@
fi fi
echo "Installing bootstrap age identity onto $boot_part..." echo "Installing bootstrap age identity onto $boot_part..."
sudo "$MOUNT" "$boot_part" "$mount_dir" sudo "$MOUNT" "$boot_part" "$boot_mount_dir"
( (
cd "$secrets_dir" cd "$secrets_dir"
RULES="$rules_file" agenix -d "$key_name" RULES="$rules_file" agenix -d "$key_name"
) | sudo tee "$mount_dir/noisebell-bootstrap.agekey" >/dev/null ) | sudo tee "$boot_mount_dir/noisebell-bootstrap.agekey" >/dev/null
sudo chmod 600 "$mount_dir/noisebell-bootstrap.agekey" sudo chmod 600 "$boot_mount_dir/noisebell-bootstrap.agekey"
sync sync
echo "Done. You can now move the card to the Pi and boot it." echo "Done. This is the full Pi NixOS image."
'';
};
pi-serial = pkgs.writeShellApplication {
name = "pi-serial";
runtimeInputs = [
pkgs.coreutils
pkgs.procps
pkgs.tio
pkgs.util-linux
];
text = ''
set -euo pipefail
baud_rate=115200
data_bits=8
stop_bits=1
parity=none
flow_control=none
serial_tools="screen tio minicom picocom"
port=""
if [ "$#" -gt 1 ]; then
echo "usage: pi-serial [device]" >&2
exit 1
fi
if [ "$#" -eq 1 ]; then
port="$1"
else
for candidate in /dev/serial/by-id/* /dev/ttyUSB* /dev/ttyACM*; do
if [ -e "$candidate" ]; then
port="$candidate"
break
fi
done
fi
if [ -z "$port" ]; then
echo "No serial device found." >&2
echo "Check the adapter and run: ls -l /dev/serial/by-id /dev/ttyUSB* /dev/ttyACM* 2>/dev/null" >&2
exit 1
fi
log_file="pi-serial-$(date +%Y%m%d-%H%M%S).log"
echo "Stopping old serial sessions for this user"
for tool in $serial_tools; do
pkill -x -u "$USER" "$tool" 2>/dev/null || true
done
sleep 1
echo "Waiting for port to become free: $port"
while fuser "$port" >/dev/null 2>&1; do
sleep 1
done
echo "Using serial port: $port"
echo "Logging to: $log_file"
echo "Start this before powering the Pi."
exec sudo tio \
-b "$baud_rate" \
-d "$data_bits" \
-s "$stop_bits" \
-p "$parity" \
-f "$flow_control" \
-t \
--log \
--log-file "$log_file" \
"$port"
''; '';
}; };
in in
{ {
packages.${system} = { packages.${system} = {
inherit noisebell-cache noisebell-discord flash-pi-sd; inherit
noisebell-cache
noisebell-discord
flash-pi-sd
pi-serial
;
default = noisebell-cache; default = noisebell-cache;
}; };
packages.aarch64-linux = { packages.aarch64-linux = {
noisebell = noisebell-pi; noisebell = noisebell-pi;
noisebell-static = noisebell-pi-static;
default = noisebell-pi; default = noisebell-pi;
}; };
@ -208,51 +366,52 @@
imports = [ imports = [
(import ./remote/cache-service/module.nix noisebell-cache) (import ./remote/cache-service/module.nix noisebell-cache)
(import ./remote/discord-bot/module.nix noisebell-discord) (import ./remote/discord-bot/module.nix noisebell-discord)
(import ./remote/hosted-module.nix {
inherit self agenix;
})
]; ];
}; };
}; };
nixosConfigurations.pi = nixpkgs.lib.nixosSystem { nixosConfigurations.pi = nixos-raspberrypi.lib.nixosSystem {
system = "aarch64-linux"; specialArgs = {
inherit nixos-raspberrypi;
};
modules = [ modules = [
nixos-raspberrypi.nixosModules.sd-image
agenix.nixosModules.default agenix.nixosModules.default
piImageBaseModule
(import ./pi/module.nix { (import ./pi/module.nix {
pkg = noisebell-pi; pkg = noisebell-pi;
rev = self.shortRev or "dirty"; rev = self.shortRev or "dirty";
}) })
./pi/configuration.nix ./pi/configuration.nix
./pi/hardware-configuration.nix
];
};
nixosConfigurations.bootstrap = nixpkgs.lib.nixosSystem {
system = "aarch64-linux";
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 {
packages = [ packages = [
agenix.packages.${system}.default
flash-pi-sd flash-pi-sd
pi-serial
pkgs.nix pkgs.nix
pkgs.parted pkgs.parted
pkgs.rust-analyzer pkgs.rust-analyzer
pkgs.systemd pkgs.tio
pkgs.util-linux
pkgs.zstd pkgs.zstd
agenix.packages.${system}.default
]; ];
}; };
apps.${system}.flash-pi-sd = { apps.${system} = {
type = "app"; flash-pi-sd = {
program = "${flash-pi-sd}/bin/flash-pi-sd"; type = "app";
program = "${flash-pi-sd}/bin/flash-pi-sd";
};
pi-serial = {
type = "app";
program = "${pi-serial}/bin/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

@ -1,8 +0,0 @@
{ modulesPath, ... }:
{
imports = [
"${modulesPath}/installer/sd-card/sd-image-aarch64.nix"
./configuration.nix
];
}

View file

@ -10,7 +10,11 @@
networks."Noisebridge".psk = "noisebridge"; networks."Noisebridge".psk = "noisebridge";
}; };
services.avahi.enable = false; services.avahi = {
enable = true;
nssmdns4 = true;
openFirewall = true;
};
# Decrypted at runtime by agenix # Decrypted at runtime by agenix
age.identityPaths = [ age.identityPaths = [
@ -30,11 +34,23 @@
inboundApiKeyFile = config.age.secrets.cache-to-pi-key.path; inboundApiKeyFile = config.age.secrets.cache-to-pi-key.path;
}; };
hardware.enableRedistributableFirmware = true;
nix.settings.experimental-features = [ nix.settings.experimental-features = [
"nix-command" "nix-command"
"flakes" "flakes"
]; ];
boot.kernelParams = [
"console=ttyS0,115200n8"
"console=ttyAMA0,115200n8"
"console=tty0"
"boot.shell_on_fail"
"loglevel=7"
"systemd.log_level=debug"
"systemd.log_target=console"
];
services.tailscale = { services.tailscale = {
enable = true; enable = true;
authKeyFile = config.age.secrets.tailscale-auth-key.path; authKeyFile = config.age.secrets.tailscale-auth-key.path;
@ -42,6 +58,18 @@
services.openssh.enable = true; services.openssh.enable = true;
system.activationScripts.pi-zero-2-dtb-compat.text = ''
for dtb_dir in /boot/nixos/*-dtbs/broadcom; do
if [ -d "$dtb_dir" ]; then
if [ -f "$dtb_dir/bcm2837-rpi-zero-2-w.dtb" ] && [ ! -e "$dtb_dir/bcm2837-rpi-zero-2.dtb" ]; then
cp "$dtb_dir/bcm2837-rpi-zero-2-w.dtb" "$dtb_dir/bcm2837-rpi-zero-2.dtb"
elif [ -f "$dtb_dir/bcm2837-rpi-3-b.dtb" ] && [ ! -e "$dtb_dir/bcm2837-rpi-zero-2.dtb" ]; then
cp "$dtb_dir/bcm2837-rpi-3-b.dtb" "$dtb_dir/bcm2837-rpi-zero-2.dtb"
fi
fi
done
'';
networking.firewall = { networking.firewall = {
trustedInterfaces = [ "tailscale0" ]; trustedInterfaces = [ "tailscale0" ];
allowedUDPPorts = [ config.services.tailscale.port ]; allowedUDPPorts = [ config.services.tailscale.port ];

View file

@ -1,4 +1,10 @@
{ config, lib, pkgs, modulesPath, ... }: {
config,
lib,
pkgs,
modulesPath,
...
}:
{ {
imports = [ ]; imports = [ ];

View file

@ -1 +0,0 @@
/nix/store/pmrzmz2b2hsffk62icl3c0ni56gpi3qs-nixos-image-sd-card-26.05.20260308.9dcb002-aarch64-linux.img.zst

View file

@ -25,11 +25,11 @@ nix build .#noisebell-discord
## NixOS deployment ## NixOS deployment
The flake exports NixOS modules. Each service runs as a hardened systemd unit behind Caddy. The flake exports a NixOS module for the hosted remote machine. It imports `agenix`, declares the Noisebell secrets from `secrets/*.age`, and wires the cache and Discord services together with sensible defaults. Each service runs as a hardened systemd unit behind Caddy.
```nix ```nix
{ {
inputs.noisebell.url = "git+https://git.extremist.software/jet/noisebell?dir=remote"; inputs.noisebell.url = "git+https://git.extremist.software/jet/noisebell";
outputs = { self, nixpkgs, noisebell, ... }: { outputs = { self, nixpkgs, noisebell, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
@ -41,19 +41,11 @@ The flake exports NixOS modules. Each service runs as a hardened systemd unit be
enable = true; enable = true;
domain = "cache.noisebell.example.com"; domain = "cache.noisebell.example.com";
piAddress = "http://noisebell-pi:80"; piAddress = "http://noisebell-pi:80";
piApiKeyFile = "/run/secrets/noisebell-pi-api-key";
inboundApiKeyFile = "/run/secrets/noisebell-inbound-api-key";
outboundWebhooks = [{
url = "http://localhost:3001/webhook";
secretFile = "/run/secrets/noisebell-discord-webhook-secret";
}];
}; };
services.noisebell-discord = { services.noisebell-discord = {
enable = true; enable = true;
domain = "discord.noisebell.example.com"; domain = "discord.noisebell.example.com";
discordTokenFile = "/run/secrets/noisebell-discord-token";
channelId = "123456789012345678"; channelId = "123456789012345678";
webhookSecretFile = "/run/secrets/noisebell-discord-webhook-secret";
}; };
}) })
]; ];
@ -61,3 +53,14 @@ The flake exports NixOS modules. Each service runs as a hardened systemd unit be
}; };
} }
``` ```
`nixosModules.default` handles these secrets automatically:
| Secret file | Deployed on | Used for |
|-------------|-------------|----------|
| `secrets/pi-to-cache-key.age` | Pi + remote | Pi authenticates to cache `/webhook` |
| `secrets/cache-to-pi-key.age` | Pi + remote | cache authenticates to Pi GET endpoints |
| `secrets/discord-webhook-secret.age` | remote | cache authenticates to Discord bot `/webhook` |
| `secrets/discord-token.age` | remote | Discord bot login |
When `extremist-software` builds a system using the Noisebell flake input, Nix uses the checked-out flake source for that input. The module points `agenix` at encrypted files inside that Noisebell source tree, such as `${inputs.noisebell}/secrets/discord-token.age`. At activation time `agenix` decrypts them locally on the target host into runtime paths like `/run/agenix/noisebell-discord-token`. The service modules then read those local decrypted files when systemd starts them.

61
remote/hosted-module.nix Normal file
View file

@ -0,0 +1,61 @@
{ self, agenix }:
{ config, lib, ... }:
let
cfgCache = config.services.noisebell-cache;
cfgDiscord = config.services.noisebell-discord;
in
{
imports = [ agenix.nixosModules.default ];
users.groups.noisebell = { };
users.users.noisebell-cache.extraGroups = lib.mkIf cfgCache.enable [ "noisebell" ];
users.users.noisebell-discord.extraGroups = lib.mkIf cfgDiscord.enable [ "noisebell" ];
age.secrets.noisebell-pi-to-cache-key = {
file = "${self}/secrets/pi-to-cache-key.age";
group = "noisebell";
mode = "0440";
};
age.secrets.noisebell-cache-to-pi-key = {
file = "${self}/secrets/cache-to-pi-key.age";
group = "noisebell";
mode = "0440";
};
age.secrets.noisebell-discord-token = {
file = "${self}/secrets/discord-token.age";
group = "noisebell";
mode = "0440";
};
age.secrets.noisebell-discord-webhook-secret = {
file = "${self}/secrets/discord-webhook-secret.age";
group = "noisebell";
mode = "0440";
};
services.noisebell-cache = lib.mkIf cfgCache.enable {
piApiKeyFile = lib.mkDefault config.age.secrets.noisebell-cache-to-pi-key.path;
inboundApiKeyFile = lib.mkDefault config.age.secrets.noisebell-pi-to-cache-key.path;
outboundWebhooks = lib.mkDefault (
lib.optional cfgDiscord.enable {
url = "http://127.0.0.1:${toString cfgDiscord.port}/webhook";
secretFile = config.age.secrets.noisebell-discord-webhook-secret.path;
}
);
};
services.noisebell-discord = lib.mkIf cfgDiscord.enable (
{
discordTokenFile = lib.mkDefault config.age.secrets.noisebell-discord-token.path;
webhookSecretFile = lib.mkDefault config.age.secrets.noisebell-discord-webhook-secret.path;
}
// lib.optionalAttrs cfgCache.enable {
cacheUrl = lib.mkDefault "http://127.0.0.1:${toString cfgCache.port}";
imageBaseUrl = lib.mkDefault "https://${cfgCache.domain}/image";
}
);
}

110
scripts/configure-pios-sd.sh Executable file
View file

@ -0,0 +1,110 @@
#!/usr/bin/env bash
set -euo pipefail
BOOTFS=${1:-/run/media/jet/bootfs}
ROOTFS=${2:-/run/media/jet/rootfs}
HOSTNAME=noisebridge-pi
WIFI_SSID=Noisebridge
WIFI_PASSWORD=noisebridge
PI_USERNAME=pi
SSH_KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu'
if [[ $EUID -ne 0 ]]; then
echo "Run with sudo: sudo $0 [bootfs] [rootfs]" >&2
exit 1
fi
if [[ ! -d "$BOOTFS" ]]; then
echo "Expected mounted boot partition." >&2
echo "Boot: $BOOTFS" >&2
exit 1
fi
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
network:
version: 2
wifis:
wlan0:
dhcp4: true
optional: true
access-points:
${WIFI_SSID}:
password: "${WIFI_PASSWORD}"
regulatory-domain: US
EOF
cat > "$BOOTFS/user-data" <<EOF
#cloud-config
hostname: ${HOSTNAME}
manage_etc_hosts: true
ssh_pwauth: false
package_update: false
packages:
- avahi-daemon
users:
- name: pi
ssh_authorized_keys:
- ${SSH_KEY}
runcmd:
- [ systemctl, enable, --now, avahi-daemon ]
EOF
cat > "$BOOTFS/meta-data" <<EOF
dsmode: local
instance_id: ${HOSTNAME}-bootstrap-1
EOF
grep -q '^enable_uart=1$' "$CONFIG_TXT" || printf '\nenable_uart=1\n' >> "$CONFIG_TXT"
: > "$BOOTFS/ssh"
if [[ "$ROOTFS_READY" -eq 1 ]]; then
cat > "$ROOTFS/etc/hostname" <<EOF
${HOSTNAME}
EOF
cat > "$ROOTFS/etc/hosts" <<EOF
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
127.0.1.1 ${HOSTNAME}
EOF
mkdir -p "$ROOTFS/home/pi/.ssh"
cat > "$ROOTFS/home/pi/.ssh/authorized_keys" <<EOF
${SSH_KEY}
EOF
chown -R 1000:1000 "$ROOTFS/home/pi/.ssh"
chmod 700 "$ROOTFS/home/pi/.ssh"
chmod 600 "$ROOTFS/home/pi/.ssh/authorized_keys"
fi
sync
echo "Configured Raspberry Pi OS SD card."
echo "- Hostname: ${HOSTNAME}"
echo "- Wi-Fi: ${WIFI_SSID}"
echo "- SSH enabled on first boot"
echo "- Serial UART enabled"
echo "- Username: ${PI_USERNAME}"
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.