Compare commits

..

No commits in common. "16ad3c61813ede8308921ac37fce15c761979794" and "cf3c5ef1f54820c8d713391f3f3eb1197f345b77" have entirely different histories.

22 changed files with 173 additions and 795 deletions

1
.envrc
View file

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

2
.gitignore vendored
View file

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

View file

@ -14,10 +14,7 @@ Pi (door sensor) ──webhook──> Cache ──webhook──> Discord
| Directory | What it is | | Directory | What it is |
|-----------|------------| |-----------|------------|
| [`pi/`](pi/) | Raspberry Pi OS base with laptop-built Noisebell deploy | | [`pi/`](pi/) | NixOS config + Rust service for the Pi |
| [`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,22 +23,6 @@
"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,
@ -76,21 +60,6 @@
"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": [
@ -112,79 +81,13 @@
"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": 1773821835, "lastModified": 1773646010,
"narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", "narHash": "sha256-iYrs97hS7p5u4lQzuNWzuALGIOdkPXvjz7bviiBjUu8=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", "rev": "5b2c2d84341b2afb5647081c1386a80d7a8d8605",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -198,8 +101,6 @@
"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"
} }
@ -211,11 +112,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1774149071, "lastModified": 1773803479,
"narHash": "sha256-SYp8NyzwfCO3Guqmu9hPRHR1hwESlQia5nNz3lYo2qA=", "narHash": "sha256-GD6i1F2vrSxbsmbS92+8+x3DbHOJ+yrS78Pm4xigW4M=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "6a031966eab3bfaa19be9e261eed5b8a79c04b18", "rev": "f17186f52e82ec5cf40920b58eac63b78692ac7c",
"type": "github" "type": "github"
}, },
"original": { "original": {

243
flake.nix
View file

@ -3,7 +3,6 @@
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";
@ -13,18 +12,12 @@
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,
@ -74,17 +67,8 @@
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 = [ targets = [ "aarch64-unknown-linux-gnu" ];
"aarch64-unknown-linux-gnu"
"aarch64-unknown-linux-musl"
];
}; };
piCraneLib = (crane.mkLib pkgs).overrideToolchain piRustToolchain; piCraneLib = (crane.mkLib pkgs).overrideToolchain piRustToolchain;
@ -115,61 +99,6 @@
} }
); );
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 = [
@ -179,7 +108,6 @@
pkgs.parted pkgs.parted
pkgs.systemd pkgs.systemd
pkgs.util-linux pkgs.util-linux
pkgs.xz
pkgs.zstd pkgs.zstd
]; ];
text = '' text = ''
@ -191,6 +119,7 @@
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
@ -199,17 +128,18 @@
device="$1" device="$1"
flake_path=${builtins.toString ./.} flake_path=${builtins.toString ./.}
zstd_bin=${pkgs.zstd}/bin/zstd image_link="$(mktemp -u /tmp/noisebell-sd-image.XXXXXX)"
secrets_dir=${builtins.toString ./secrets} mount_dir="$(mktemp -d)"
rules_file=${builtins.toString ./secrets/secrets.nix} secrets_dir="${builtins.toString ./secrets}"
key_name="bootstrap-identity.age" key_name="bootstrap-identity.age"
boot_mount_dir="$(mktemp -d)" rules_file="${builtins.toString ./secrets/secrets.nix}"
cleanup() { cleanup() {
if "$MOUNTPOINT" -q "$boot_mount_dir"; then if "$MOUNTPOINT" -q "$mount_dir"; then
sudo "$UMOUNT" "$boot_mount_dir" sudo "$UMOUNT" "$mount_dir"
fi fi
rm -rf "$boot_mount_dir" rm -rf "$mount_dir"
rm -f "$image_link"
} }
trap cleanup EXIT trap cleanup EXIT
@ -223,30 +153,20 @@
*[0-9]) boot_part="''${device}p1" ;; *[0-9]) boot_part="''${device}p1" ;;
esac esac
echo "Requesting sudo access before build and flash..." echo "Building bootstrap SD image..."
sudo -v nix build "$flake_path#nixosConfigurations.bootstrap.config.system.build.sdImage" -o "$image_link"
echo "Sudo authentication successful."
echo "Building full Pi NixOS image..." image="$(echo "$image_link"/sd-image/*.img*)"
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_out/sd-image" >&2 echo "failed to locate SD image under $image_link/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_bin" -d --stdout "$image" | sudo dd of="$device" bs=16M conv=fsync status=progress "$ZSTD" -d --stdout "$image" | sudo dd of="$device" bs=4M 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=4M conv=fsync status=progress
fi fi
sync sync
@ -258,104 +178,26 @@
fi fi
echo "Installing bootstrap age identity onto $boot_part..." echo "Installing bootstrap age identity onto $boot_part..."
sudo "$MOUNT" "$boot_part" "$boot_mount_dir" sudo "$MOUNT" "$boot_part" "$mount_dir"
( (
cd "$secrets_dir" cd "$secrets_dir"
RULES="$rules_file" agenix -d "$key_name" RULES="$rules_file" agenix -d "$key_name"
) | sudo tee "$boot_mount_dir/noisebell-bootstrap.agekey" >/dev/null ) | sudo tee "$mount_dir/noisebell-bootstrap.agekey" >/dev/null
sudo chmod 600 "$boot_mount_dir/noisebell-bootstrap.agekey" sudo chmod 600 "$mount_dir/noisebell-bootstrap.agekey"
sync sync
echo "Done. This is the full Pi NixOS image." echo "Done. You can now move the card to the Pi and boot it."
'';
};
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 inherit noisebell-cache noisebell-discord flash-pi-sd;
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;
}; };
@ -366,52 +208,51 @@
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 = nixos-raspberrypi.lib.nixosSystem { nixosConfigurations.pi = nixpkgs.lib.nixosSystem {
specialArgs = { system = "aarch64-linux";
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.tio pkgs.systemd
pkgs.util-linux
pkgs.zstd pkgs.zstd
agenix.packages.${system}.default
]; ];
}; };
apps.${system} = { apps.${system}.flash-pi-sd = {
flash-pi-sd = { type = "app";
type = "app"; program = "${flash-pi-sd}/bin/flash-pi-sd";
program = "${flash-pi-sd}/bin/flash-pi-sd";
};
pi-serial = {
type = "app";
program = "${pi-serial}/bin/pi-serial";
};
}; };
}; };
} }

View file

@ -1,181 +1,145 @@
# Pi # Pi
Rust service and deployment workflow for the Raspberry Pi at Noisebridge. 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.
The current recommended setup is: Runs NixOS with Tailscale for remote access and agenix for secrets.
1. run Raspberry Pi OS Lite on the Pi ## How it works
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
This avoids the Raspberry Pi Zero 2 W NixOS boot issues while still keeping the application build reproducible. The service watches a GPIO pin for rising/falling edges with a configurable debounce. When the door state changes, it:
## What stays on Raspberry Pi OS 1. Updates in-memory state (atomics)
2. POSTs `{"status": "open", "timestamp": ...}` to the cache service with a Bearer token
3. Retries with exponential backoff on failure
- bootloader On startup it also syncs the initial state.
- kernel
- firmware
- Wi-Fi and local networking
- SSH base access
- Tailscale package/runtime
- Avahi package/runtime
## What Nix manages ## Setup
- building a static `noisebell` binary for `aarch64-linux` ### Prerequisites
- the exact app binary you deploy
- encrypted secrets in the repo
- repeatable deployment from your laptop
## Initial Pi OS setup 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:
### 1. Flash Raspberry Pi OS Lite ```nix
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
```sh
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
``` ```
### 2. Configure the flashed SD card ### 1. Flash the SD card
Configure it for: 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:
- Wi-Fi on `Noisebridge`
- SSH enabled
- serial enabled if you want a recovery console
The helper script is:
```sh ```sh
sudo scripts/configure-pios-sd.sh /run/media/jet/bootfs /run/media/jet/rootfs nix run .#flash-pi-sd -- /dev/sdX
``` ```
This setup expects SSH key login for user `pi`; it does not configure a password. This bootstrap image already includes the normal Noisebell service, Tailscale, and the Pi config.
### 3. Boot the Pi and verify SSH Manual build if you need it:
After boot, verify SSH works:
```sh ```sh
ssh pi@noisebridge-pi.local nix build .#nixosConfigurations.bootstrap.config.system.build.sdImage
dd if=result/sd-image/*.img of=/dev/sdX bs=4M status=progress
``` ```
## Add the Pi host key to age recipients Boot the Pi. It connects to the Noisebridge WiFi automatically.
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. ### 2. SSH host key
Grab the Pi host key: Grab the key and add it to `secrets/secrets.nix`:
```sh ```sh
ssh-keyscan noisebridge-pi.local 2>/dev/null | grep ed25519 ssh-keyscan noisebridge-pi.local 2>/dev/null | grep ed25519
``` ```
Add that key to `secrets/secrets.nix` for: ```nix
# 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 ];
}
```
- `pi-to-cache-key.age` ### 3. Create secrets
- `cache-to-pi-key.age`
- `tailscale-auth-key.age`
Then refresh recipients if needed: ```sh
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
``` ```
## Edit secrets 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 ```sh
cd secrets cd secrets
agenix -e pi-to-cache-key.age agenix -d bootstrap-identity.age > /boot/noisebell-bootstrap.agekey
agenix -e cache-to-pi-key.age chmod 600 /boot/noisebell-bootstrap.agekey
agenix -e tailscale-auth-key.age
``` ```
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. ### 5. SSH access
## Deploy to Raspberry Pi OS Add your public key to `configuration.nix`:
From your laptop: ```nix
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
scripts/deploy-pios-pi.sh pi@noisebridge-pi.local nixos-rebuild switch --flake .#pi --target-host root@noisebell
``` ```
If you only know the IP: ## Configuration
```sh Options under `services.noisebell` in `flake.nix`:
scripts/deploy-pios-pi.sh pi@10.21.x.x
```
That script: | Option | Default | Description |
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 |
|---|---|---| |---|---|---|
| `NOISEBELL_GPIO_PIN` | `17` | GPIO pin number | | `endpointUrl` | required | Webhook URL to POST state changes to |
| `NOISEBELL_DEBOUNCE_SECS` | `5` | Debounce delay in seconds | | `apiKeyFile` | required | Outbound API key file (agenix secret) |
| `NOISEBELL_PORT` | `80` | HTTP server port | | `inboundApiKeyFile` | required | Inbound API key file for GET auth |
| `NOISEBELL_ENDPOINT_URL` | required | Webhook URL to POST state changes to | | `gpioPin` | `17` | GPIO pin number |
| `NOISEBELL_RETRY_ATTEMPTS` | `3` | Webhook retry count | | `debounceSecs` | `5` | Debounce delay in seconds |
| `NOISEBELL_RETRY_BASE_DELAY_SECS` | `1` | Exponential backoff base delay | | `port` | `8080` | HTTP server port |
| `NOISEBELL_HTTP_TIMEOUT_SECS` | `10` | Outbound request timeout | | `retryAttempts` | `3` | Webhook retry count |
| `NOISEBELL_BIND_ADDRESS` | `0.0.0.0` | HTTP bind address | | `retryBaseDelaySecs` | `1` | Exponential backoff base delay |
| `NOISEBELL_ACTIVE_LOW` | `true` | Low GPIO = door open | | `httpTimeoutSecs` | `10` | Outbound request timeout |
| `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 /`** **`GET /`** — door state
```json ```json
{"status": "open", "timestamp": 1710000000} {"status": "open", "timestamp": 1710000000}

8
pi/bootstrap.nix Normal file
View file

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

View file

@ -10,11 +10,7 @@
networks."Noisebridge".psk = "noisebridge"; networks."Noisebridge".psk = "noisebridge";
}; };
services.avahi = { services.avahi.enable = false;
enable = true;
nssmdns4 = true;
openFirewall = true;
};
# Decrypted at runtime by agenix # Decrypted at runtime by agenix
age.identityPaths = [ age.identityPaths = [
@ -34,23 +30,11 @@
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;
@ -58,18 +42,6 @@
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,10 +1,4 @@
{ { config, lib, pkgs, modulesPath, ... }:
config,
lib,
pkgs,
modulesPath,
...
}:
{ {
imports = [ ]; imports = [ ];

1
pi/result Symbolic link
View file

@ -0,0 +1 @@
/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 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. The flake exports NixOS modules. Each service runs as a hardened systemd unit behind Caddy.
```nix ```nix
{ {
inputs.noisebell.url = "git+https://git.extremist.software/jet/noisebell"; inputs.noisebell.url = "git+https://git.extremist.software/jet/noisebell?dir=remote";
outputs = { self, nixpkgs, noisebell, ... }: { outputs = { self, nixpkgs, noisebell, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
@ -41,11 +41,19 @@ The flake exports a NixOS module for the hosted remote machine. It imports `agen
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";
}; };
}) })
]; ];
@ -53,14 +61,3 @@ The flake exports a NixOS module for the hosted remote machine. It imports `agen
}; };
} }
``` ```
`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.

View file

@ -1,61 +0,0 @@
{ 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";
}
);
}

View file

@ -1,110 +0,0 @@
#!/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

View file

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

View file

@ -1,117 +0,0 @@
#!/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,7 +1,8 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 Ziw7aw 756HU1sPe5g3sa0YYfzMnXiToT5K+nfAPhfABEetgRI -> ssh-ed25519 Ziw7aw W4nd317o0ON6n006hwUNIops3L7VngNqDqJ1bG7tCyg
E8dqhn7hN77qM0PhHMEAsZySd1hfk0w1tlsiWj4aEOQ xyjvZOOzA0u3e+Cba99LR8J5JAkl6muHuspJ+b8dEog
-> ssh-ed25519 uKftJg eGfmzvHseauAFPOR1QXfdmaQy5TjpNsoBWq27mbO50w -> ssh-ed25519 uKftJg bX0GHUPdD4hR2yxLNx8ho689or7FNHXPA8iV9Af1q2s
KRuGUW65uQ5+IdREyg6X1oj0P5IkuuxFEl1WylGpAHc B91XIdsnHfAKdKfu8jmHKKptA5OomQ5sHvXfLLUeNbs
--- 2Ya08payqNiMCEqBXrbKEA53ETupxwgUNRcMNu9IP6k --- 8zoH1w5ywsbDN5Xt50sE4BUgXoq12mr5b4rSVYMLEB8
&°‰¢ÏvÄ<76>tBšL˜.¬HéÚ“Y P®Sç3Tó¹•4䱟D¬ s,9Ñeã4TpåÌ,Gë?KtJmwF' ®0³ÉXy»<á÷<E28099>î>½\Áç^þ°m»ÜrÊÏùµŸÏü 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,12 +1,11 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 Ziw7aw ZaLRvgj6V9ukim0lfxHftVUvCXi7tIXPn5O/2nzQqCE -> ssh-ed25519 Ziw7aw TMz4xBHmy5btMk5ETWyvjZcjj0QwF3F6Iqt2QmkwVwk
cem5AxKMkYOs8iifYP80hkbr5km7bFOdjCt7Ym6lQcs KvcY409ZakEUO7OrmDrwseMQv/w7i3B7uDiXjls/5qE
-> ssh-ed25519 NFB4qA ssMeOzGjehzTeppIGHpzPViIKObSwnXw6OZ1DfXs6Ew -> ssh-ed25519 jcT/MQ Y5TqVlHy3fM+CmQIBu1x18C3wjHtRZCHJ9dhFyVAR0I
Y813udN4YGDMszEC8FVZz7Na6XQigVNFTdusLomMusg WPqxEZrpAm0wLYT4s9e1uxSuDjhwUwIOL4BvDtvqytU
-> X25519 qmoLWSdRljn6daPlUyqk9TOOvBaUx42CvqcpXe/xUCE -> X25519 ZqSHgIStt6Ru3osVvMA1M5sydoY+CeZ56temQyCFIVY
7xMN5RbYnpgw3+/pHyCiEyEhyUmQOwa1zSlAbuVwlQo 4tPA6VITlAzJxCFGVreKK1B6rrHm+ka4ELwnzYrMKbQ
-> ssh-ed25519 uKftJg Fv8M0RogkcYWd46bJY3OJCoCFAW8QMjzLueDZowylSA -> ssh-ed25519 uKftJg OR8VgPUuEvS/0Gc5c92IlAp4DKKYcRzBbSh1tX9ddzg
R3w6E2RvDmgaKKhxqWHjEeIQxNSCHzX7+nLb3Ls+iHs DZYYx9ngwEUTmj/JaP2XnCQHjpPY8WYgOEDlOfZPLeA
--- 13dp1N6I6pPdDx+FrxsT+ZS5rsFfrK3x0F7Rs6vN6/I --- 4iDfaqdSLiW0doVijoZC5ckxiCmsmVWJi5Kvaxic2Ng
ÖBI¬8Ż è÷VV;2†h<>&e@sÏx#d=Y?iðw½´ÉÍF<15>OÝc×/ûš(™¨àœ›¼føkØíƒ—¿}[¡¤'=B@ô‡¨Â!wÔ
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 AAAAC3NzaC1lZDI1NTE5AAAAIEfZfAQEFy8QU5P7deC2vWPN76YpUKcBF8fiWwuANumG"; pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKmJFZAVJUjDziQ7XytVhQIv+Pm9hnbVU0flxZK17K5E";
server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB"; server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB";
piBootstrap = "age1sfqn46dgztr35dtyhpzyzzam5m6kcqu495qs7fcsdxtac56pc4dsj3t862"; piBootstrap = "age1sfqn46dgztr35dtyhpzyzzam5m6kcqu495qs7fcsdxtac56pc4dsj3t862";
in in

Binary file not shown.