refactor(beszel): switch to universal token via secrets/common.yaml
Check NixOS configurations / eval-hosts (pull_request) Successful in 10m40s

Replace per-host host-token.nix imports with a single beszel-token secret
in secrets/common.yaml, wired once in enable-agent.nix. Host files now only
need services.beszel.agent.environment.KEY — no imports block required.

Delete modules/beszel/host-token.nix (no longer referenced anywhere).

Action needed: run `sops secrets/common.yaml` and add `beszel-token: <value>`
from the beszel hub UI before deploying.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 10:58:15 +10:00
co-authored by Claude Sonnet 4.6
parent 99ba0ed52b
commit 24c6469f10
10 changed files with 74 additions and 154 deletions
+8 -10
View File
@@ -21,9 +21,8 @@ machines when deployed.
`modules/installer/common.nix` (the auto-installer's own root/nixos login — `modules/installer/common.nix` (the auto-installer's own root/nixos login —
a deliberate, documented choice, see `docs/auto-installer.md`, not a deliberate, documented choice, see `docs/auto-installer.md`, not
accidental tech debt) and **SSH public keys** in `variables.nix` accidental tech debt) and **SSH public keys** in `variables.nix`
(`vars.adminSshKey`, `vars.remoteBuilderAuthorizedKeys`) plus a couple of (`vars.adminSshKey`, `vars.remoteBuilderAuthorizedKeys`) plus per-host
per-host `KEY` values for beszel-agent auth (`hosts/server/host.nix`, `KEY` values for beszel-agent auth (see `docs/beszel.md`). Don't use the installer's hardcoded hash as a
`hosts/nix-cache/host.nix`). Don't use the installer's hardcoded hash as a
template for a *real* host — every other host uses sops-nix template for a *real* host — every other host uses sops-nix
(`hashedPasswordFile`, see "Security Notes" in `README.md`). Flag any *new* (`hashedPasswordFile`, see "Security Notes" in `README.md`). Flag any *new*
secret-like string you encounter instead of committing it. secret-like string you encounter instead of committing it.
@@ -375,9 +374,8 @@ removing a host.
- `hosts/<name>/host.nix` — per-machine identity **only**: hostname, hostId, - `hosts/<name>/host.nix` — per-machine identity **only**: hostname, hostId,
per-machine secrets, `system.stateVersion`. These files carry no `imports` per-machine secrets, `system.stateVersion`. These files carry no `imports`
of their own beyond narrow parameterized helpers (see of their own — all shared behavior comes from the platform/build-type modules
`modules/beszel/host-token.nix` below) — all shared behavior comes from the composed in `flake.nix`, not from the host file.
platform/build-type modules composed in `flake.nix`, not from the host file.
- `modules/platforms/{linode,proxmox,lxc,baremetal}.nix` — platform-specific - `modules/platforms/{linode,proxmox,lxc,baremetal}.nix` — platform-specific
config: boot method, guest tooling, and the hardware config, imported config: boot method, guest tooling, and the hardware config, imported
directly by the platform module itself — **not** wired in from directly by the platform module itself — **not** wired in from
@@ -434,10 +432,10 @@ removing a host.
substituter + SSH remote-builder wiring; see `docs/nix-cache.md` for the substituter + SSH remote-builder wiring; see `docs/nix-cache.md` for the
full design (per-host local stores, no shared `/nix/store`, and how the full design (per-host local stores, no shared `/nix/store`, and how the
`nixremote` signing/SSH keys fit together). `nixremote` signing/SSH keys fit together).
- `modules/beszel/host-token.nix` — parameterized helper module - `modules/beszel/enable-agent.nix` — enables beszel-agent, sets `HUB_URL`,
(`{ name, sopsFile }`) that wires a host's beszel-agent sops secret/template fixes the upstream `StateDirectory` bug, and wires the universal
and `environmentFile`; used by `hosts/server/host.nix` and `beszel-token` sops secret (from `secrets/common.yaml`) into the agent's
`hosts/nix-cache/host.nix` to avoid duplicating that boilerplate. `environmentFile`; see `docs/beszel.md` for the full setup guide.
- `modules/tailscale/`, `modules/docker/`, `modules/networking/`, - `modules/tailscale/`, `modules/docker/`, `modules/networking/`,
`modules/traefik/`, `modules/tor/`, `modules/services/*` — single-purpose, `modules/traefik/`, `modules/tor/`, `modules/services/*` — single-purpose,
single-host single-host
+47 -84
View File
@@ -5,30 +5,29 @@ in this LAN. The hub runs as a Docker container on `docker.sweet.home` (port
`vars.ports.beszelHub`, 8090). Each monitored NixOS host runs a `vars.ports.beszelHub`, 8090). Each monitored NixOS host runs a
`beszel-agent` that connects back to the hub. `beszel-agent` that connects back to the hub.
This document explains how to wire beszel-agent into any build in the flake.
--- ---
## How it is split across modules ## How it works
There are two modules and they have separate jobs: Everything is handled by a single module:
| Module | What it does | Where it goes | **`modules/beszel/enable-agent.nix`** — imported by a build type. It:
|---|---|---| - Enables `beszel-agent`
| `modules/beszel/enable-agent.nix` | Enables the systemd service, sets `HUB_URL`, fixes upstream StateDirectory bug | **Build type** (`modules/build-types/*.nix`) | - Sets `HUB_URL` to `docker.sweet.home:8090`
| `modules/beszel/host-token.nix` | Declares the sops secret + env-file template that holds `TOKEN` | **Host file** (`hosts/<name>/host.nix`) | - Reads the universal `beszel-token` from `secrets/common.yaml` via sops and
passes it to the agent as `TOKEN` in an env file
- Fixes an upstream bug where the agent couldn't persist its hub-pairing
fingerprint across restarts (adds a real `StateDirectory`)
`KEY` (the hub's public key for this agent slot) is a plain string set The only thing a host file ever needs to add is `KEY` the hub's public key
directly on the host under `services.beszel.agent.environment.KEY` — it comes for that agent slot, which comes from the beszel hub UI after first pairing.
from the hub UI, not from sops, so it belongs in the host file rather than a
secret.
--- ---
## Adding beszel to a new build type ## Adding beszel to a new build type
Open `modules/build-types/<type>.nix` and add `../beszel/enable-agent.nix` to Add `../beszel/enable-agent.nix` to the `imports` list in
the `imports` list: `modules/build-types/<type>.nix`:
```nix ```nix
imports = [ imports = [
@@ -37,34 +36,14 @@ imports = [
]; ];
``` ```
That is the only change needed at the build-type level. The module sets: That's the only build-type change required.
- `services.beszel.agent.enable = true`
- `HUB_URL` pointing at `docker.sweet.home:8090`
- a persistent `StateDirectory` for the hub-pairing fingerprint
--- ---
## Wiring the host file ## Wiring the host file
Every host whose build type includes `enable-agent.nix` needs two things in No `imports` are needed in the host file. Just set `KEY` once you've paired
its `hosts/<name>/host.nix`: the agent with the hub:
### 1 — Import `host-token.nix`
```nix
imports = [
(import ../../modules/beszel/host-token.nix {
name = "<hostname>"; # must match the sops secret file name below
sopsFile = ../../secrets/<hostname>.yaml;
})
];
```
This creates a sops-managed env file at runtime containing `TOKEN=<value>` and
passes it to the agent via `environmentFile`. The `name` argument also becomes
the sops template name (`<name>-beszel.env`), so it must be unique per host.
### 2 — Set `KEY` once the hub has paired the agent
```nix ```nix
services.beszel.agent.environment = { services.beszel.agent.environment = {
@@ -72,53 +51,47 @@ services.beszel.agent.environment = {
}; };
``` ```
`KEY` is the hub's SSH public key for this agent slot — copy it from the hub Leave `KEY` commented out until after the first pairing (see "Pairing with
UI after the agent first connects (see "Pairing with the hub" below). the hub" below).
Until the hub has been paired, leave `KEY` commented out or omit it. The agent
will still start and appear in the hub as an unpaired entry; fill in the key
after that first connection.
--- ---
## Adding the sops secret ## One-time setup: add the token to `secrets/common.yaml`
The `beszel-token` value in `secrets/<hostname>.yaml` is the token the agent The universal token is stored once in the common secrets file, shared by all
uses to authenticate to the hub. To add it for a new host: agents. You only need to do this once, not per-host:
1. Decrypt and edit the host's secrets file: ```sh
```sh sops secrets/common.yaml
sops secrets/<hostname>.yaml ```
```
2. Add the token:
```yaml
beszel-token: <token from the beszel hub UI>
```
3. Save and close — sops re-encrypts the file on exit.
If the host does not yet have a `secrets/<hostname>.yaml`, create one using Add:
the same sops recipients as the other secrets files (check `.sops.yaml`), or ```yaml
run `scripts/secrets/sync-host-keys.sh <target>` first so the host's own beszel-token: <token from the beszel hub UI>
SSH key is already a valid recipient. ```
The token is found in the beszel hub under **Settings → Keys** or when you The token is found in the beszel hub under **Settings → Keys** or in the
add a new system via the hub UI. "Add system" flow.
`secrets/common.yaml` is already a sops recipient for every host via their
SSH host keys, so no additional sops recipient setup is needed for hosts that
are already provisioned.
--- ---
## Optional: monitoring extra filesystems ## Optional: monitoring extra filesystems
To have the agent report disk usage for a mount point that is not the root To report disk usage for a mount beyond the root filesystem, add
filesystem, add `EXTRA_FILESYSTEMS` in the host file alongside `KEY`: `EXTRA_FILESYSTEMS` alongside `KEY` in the host file:
```nix ```nix
services.beszel.agent.environment = { services.beszel.agent.environment = {
KEY = "ssh-ed25519 AAAA..."; KEY = "ssh-ed25519 AAAA...";
EXTRA_FILESYSTEMS = "/mnt/data"; # colon-separated if more than one EXTRA_FILESYSTEMS = "/mnt/data"; # colon-separated for multiple paths
}; };
``` ```
The `server` host uses this to expose the ZFS data pool: The `server` host uses this to expose its ZFS data pool:
```nix ```nix
EXTRA_FILESYSTEMS = "${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}"; EXTRA_FILESYSTEMS = "${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
@@ -128,48 +101,39 @@ EXTRA_FILESYSTEMS = "${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}
## Optional: monitoring Docker containers ## Optional: monitoring Docker containers
The `enable-agent.nix` module has a commented-out line for Docker monitoring: `enable-agent.nix` has a commented-out line for Docker monitoring:
```nix ```nix
#DOCKER_HOST = "tcp://docker-socket-proxy:2375"; #DOCKER_HOST = "tcp://docker-socket-proxy:2375";
``` ```
Uncomment it if the host runs docker-socket-proxy and you want per-container Uncomment it if the host runs docker-socket-proxy and you want per-container
stats in the hub. Hosts that do not run Docker should leave this commented out. stats. Hosts without Docker should leave it commented out.
--- ---
## Pairing with the hub ## Pairing with the hub
1. Deploy the host with `host-token.nix` imported and `enable-agent.nix` 1. Deploy the host with its build type importing `enable-agent.nix`. Leave
present in the build type. Leave `KEY` unset for now. `KEY` unset (commented out) for now.
2. Open the beszel hub (`http://docker.sweet.home:8090`). 2. Open the beszel hub (`http://docker.sweet.home:8090`).
3. Go to **Systems → Add system**. The new host should appear as an 3. Go to **Systems → Add system**. The new host should appear as an unpaired
unpaired entry — copy the `KEY` value shown there. entry — copy the `KEY` value shown there.
4. In `hosts/<name>/host.nix`, set: 4. In `hosts/<name>/host.nix`, set:
```nix ```nix
services.beszel.agent.environment.KEY = "<copied key>"; services.beszel.agent.environment.KEY = "<copied key>";
``` ```
5. Rebuild and deploy the host. The agent will now pair permanently with the 5. Rebuild and deploy the host. The agent will now pair permanently.
hub.
--- ---
## Example: complete host file ## Example: complete host file
For reference, `hosts/tor-relay/host.nix` is the minimal case — one Minimal case (`hosts/tor-relay/host.nix` — one filesystem, no Docker, LXC):
filesystem, no Docker, LXC platform:
```nix ```nix
{ vars, ... }: { vars, ... }:
{ {
imports = [
(import ../../modules/beszel/host-token.nix {
name = "tor-relay";
sopsFile = ../../secrets/tor-relay.yaml;
})
];
networking = { ... }; networking = { ... };
services.beszel.agent.environment = { services.beszel.agent.environment = {
@@ -180,8 +144,7 @@ filesystem, no Docker, LXC platform:
} }
``` ```
`hosts/server/host.nix` is the fuller case — extra filesystem path, debug Fuller case (`hosts/server/host.nix` — extra filesystem, debug logging):
logging:
```nix ```nix
services.beszel.agent.environment = { services.beszel.agent.environment = {
+1 -8
View File
@@ -1,12 +1,5 @@
{ vars, ... }: { vars, ... }:
{ {
imports = [
(import ../../modules/beszel/host-token.nix {
name = "ha-server-1";
sopsFile = ../../secrets/ha-server-1.yaml;
})
];
networking = { networking = {
hostName = vars.haServer1Host; hostName = vars.haServer1Host;
hostId = "3a4b5c6d"; hostId = "3a4b5c6d";
@@ -23,7 +16,7 @@
nameservers = [ vars.domainControllerIp ]; nameservers = [ vars.domainControllerIp ];
}; };
# Set KEY after pairing this host with the beszel hub; the token is sops-managed. # Set KEY after pairing this host with the beszel hub (see docs/beszel.md).
# services.beszel.agent.environment.KEY = ""; # services.beszel.agent.environment.KEY = "";
system.stateVersion = "26.05"; system.stateVersion = "26.05";
+1 -8
View File
@@ -1,12 +1,5 @@
{ vars, ... }: { vars, ... }:
{ {
imports = [
(import ../../modules/beszel/host-token.nix {
name = "ha-server-2";
sopsFile = ../../secrets/ha-server-2.yaml;
})
];
networking = { networking = {
hostName = vars.haServer2Host; hostName = vars.haServer2Host;
hostId = "7e8f9a0b"; hostId = "7e8f9a0b";
@@ -23,7 +16,7 @@
nameservers = [ vars.domainControllerIp ]; nameservers = [ vars.domainControllerIp ];
}; };
# Set KEY after pairing this host with the beszel hub; the token is sops-managed. # Set KEY after pairing this host with the beszel hub (see docs/beszel.md).
# services.beszel.agent.environment.KEY = ""; # services.beszel.agent.environment.KEY = "";
system.stateVersion = "26.05"; system.stateVersion = "26.05";
-7
View File
@@ -1,13 +1,6 @@
{ vars, ... }: { vars, ... }:
{ {
imports = [
(import ../../modules/beszel/host-token.nix {
name = "nix-cache";
sopsFile = ../../secrets/nix-cache.yaml;
})
];
networking = { networking = {
hostName = vars.nixCacheHost; hostName = vars.nixCacheHost;
useDHCP = false; useDHCP = false;
-7
View File
@@ -1,13 +1,6 @@
{ vars, ... }: { vars, ... }:
{ {
imports = [
(import ../../modules/beszel/host-token.nix {
name = "server";
sopsFile = ../../secrets/server.yaml;
})
];
networking = { networking = {
hostName = vars.nfsServerHost; hostName = vars.nfsServerHost;
hostId = "6689f93e"; hostId = "6689f93e";
-7
View File
@@ -1,13 +1,6 @@
{ vars, ... }: { vars, ... }:
{ {
imports = [
(import ../../modules/beszel/host-token.nix {
name = "tailscale-router";
sopsFile = ../../secrets/tailscale-router.yaml;
})
];
networking = { networking = {
hostName = "tailscale-router"; hostName = "tailscale-router";
useDHCP = false; useDHCP = false;
-7
View File
@@ -1,13 +1,6 @@
{ vars, ... }: { vars, ... }:
{ {
imports = [
(import ../../modules/beszel/host-token.nix {
name = "tor-relay";
sopsFile = ../../secrets/tor-relay.yaml;
})
];
networking = { networking = {
hostName = "tor-relay"; hostName = "tor-relay";
useDHCP = false; useDHCP = false;
+17 -5
View File
@@ -1,10 +1,22 @@
{ vars, ... }: { config, vars, ... }:
{ {
services.beszel.agent.enable = true; # Universal token shared by all beszel agents. Add to secrets/common.yaml:
services.beszel.agent.environment = { # sops secrets/common.yaml
#DOCKER_HOST = "tcp://docker-socket-proxy:2375"; # beszel-token: <value from the beszel hub UI>
HUB_URL = "http://${vars.dockerHost}.${vars.homeDomain}:${toString vars.ports.beszelHub}"; sops.secrets."beszel-token" = { };
sops.templates."beszel.env".content = ''
TOKEN=${config.sops.placeholder."beszel-token"}
'';
services.beszel.agent = {
enable = true;
environmentFile = config.sops.templates."beszel.env".path;
environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
HUB_URL = "http://${vars.dockerHost}.${vars.homeDomain}:${toString vars.ports.beszelHub}";
};
}; };
# The upstream module runs beszel-agent under DynamicUser with # The upstream module runs beszel-agent under DynamicUser with
-11
View File
@@ -1,11 +0,0 @@
{ name, sopsFile }:
{ config, ... }:
{
sops.secrets."beszel-token".sopsFile = sopsFile;
sops.templates."${name}-beszel.env".content = ''
TOKEN=${config.sops.placeholder."beszel-token"}
'';
services.beszel.agent.environmentFile = config.sops.templates."${name}-beszel.env".path;
}