The agent host's vault and SilverBullet (M5.4)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m50s
CI / e2e (pull_request) Successful in 4m45s

deploy/agent-host sets up the vault on nisse-host (infra/tofu#77) and
SilverBullet over it: the operator writes the vault, the agents' memory/
is read-only to it, and only the operator's Caddy reaches it, which
serves vault.dongar.net to the LAN. scripts/agent-host.sh installs and
checks it from here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-18 20:55:21 +00:00
1 parent da9097a4dd
commit eafe1b9f4a
8 files changed
+342 -7

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
# The agent host
`nisse-host.garrett-hall.com`, made by infra/tofu#77 (architecture X1). For now
it holds the knowledge vault and SilverBullet, the operator's view of it (L10,
M5.4). The agents' containers come later (M6.1).
## What's here
- `silverbullet/compose.yaml`: SilverBullet, its version pinned. It mounts the
vault, with the agents' `memory/` read-only, and its own data folder.
Nothing else of the host.
- `setup.sh`: makes the vault's folders, installs the stack under
`/opt/stacks/silverbullet` and the firewall, and starts it. Safe to run again.
- `nisse-firewall`: only the operator's Caddy may reach SilverBullet's port
(Docker's `DOCKER-USER` chain, re-applied whenever Docker starts).
- `caddy/vault.dongar.net.caddy`: the site for the operator's Caddy on
`docker.garrett-hall.com`. HTTPS from Caddy's usual certificate, LAN only.
## The vault
`/srv/nisse/vault`, one copy on the host's disk, nothing synced:
- **The operator's:** the root and everything in it but `memory/`, written
through SilverBullet. `infrastructure/` holds material for `nisse-ops`, which
`nisse-dev` will never be given.
- **The agents':** `memory/<agent>/`, one writer each, when the agents move in.
SilverBullet sees them read-only.
Backed up nightly with the VM (Proxmox; `backup: true` in infra/tofu).
## Installing and checking
From the development VM, with the bring-up key the operator added for `ubuntu`:
```sh
scripts/agent-host.sh install # copy this folder over and run setup.sh
scripts/agent-host.sh check # running, mounts, firewall
```
Then:
1. **The operator adds the Caddy site** from `caddy/vault.dongar.net.caddy`
and reloads Caddy.
2. **The operator runs SilverBullet's setup wizard at once,** at
`https://vault.dongar.net`. Until it's done, whoever opens the page first
makes the administrator account.
- Account: the operator's, with a strong password.
- Space: folder `/vault`, address `vault.dongar.net`, path `/`.
@@ -0,0 +1,10 @@
# The vault's SilverBullet, for the operator's Caddy on docker.garrett-hall.com
# (/opt/stacks). HTTPS from Caddy's usual automatic certificate; LAN only.
vault.dongar.net {
# Anyone outside the LAN gets nothing. Caddy still answers Let's Encrypt's
# checks, which it handles before the site's own routes.
@outside not remote_ip private_ranges
respond @outside 403
reverse_proxy nisse-host.garrett-hall.com:3000
}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
# nisse-firewall ADDRESS PORT: only ADDRESS may reach a port Docker publishes
# as PORT. Docker publishes ports past the host's own firewall rules, so the
# limit goes in its DOCKER-USER chain. Installed by setup.sh and run by
# nisse-firewall.service whenever Docker starts. Its own rules (tagged) are
# replaced each run, so a changed address leaves no old rule behind.
set -eu
address=$1 port=$2
iptables -S DOCKER-USER | grep -- '--comment nisse-firewall' | sed 's/^-A /-D /' |
while read -r old; do eval "iptables $old"; done
iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport "$port" \
--ctdir ORIGINAL ! -s "$address" -m comment --comment nisse-firewall -j DROP
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Set up the agent host's vault and SilverBullet (M5.4). Run as root on the
# host, from this folder's copy (scripts/agent-host.sh install does that).
# Safe to run again: it changes only what differs.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
# The only address allowed to reach SilverBullet: the operator's Caddy
# (docker.garrett-hall.com), which serves vault.dongar.net to the LAN.
CADDY_ADDR=192.168.6.13
SB_PORT=3000
SB_UID=1000 # the image's PUID/PGID: the operator's side of the vault
# The vault. Its root and everything in it belong to the operator, except
# memory/, where each agent gets a folder of its own when it moves in. memory/
# is root's, and SilverBullet mounts it read-only besides.
install -d -m 0755 /srv/nisse
install -d -o "$SB_UID" -g "$SB_UID" -m 0750 /srv/nisse/silverbullet
install -d -o "$SB_UID" -g "$SB_UID" -m 0755 /srv/nisse/vault
install -d -o "$SB_UID" -g "$SB_UID" -m 0755 /srv/nisse/vault/infrastructure
install -d -o root -g root -m 0755 /srv/nisse/vault/memory
# SilverBullet's stack, where the docker role keeps stacks.
install -d -m 0755 /opt/stacks/silverbullet
install -m 0644 "$here/silverbullet/compose.yaml" /opt/stacks/silverbullet/compose.yaml
# Only Caddy may reach SilverBullet's port, re-applied whenever Docker starts.
install -m 0755 "$here/nisse-firewall" /usr/local/sbin/nisse-firewall
cat > /etc/systemd/system/nisse-firewall.service <<EOF
[Unit]
Description=Only the operator's Caddy reaches SilverBullet (deploy/agent-host)
After=docker.service
BindsTo=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/nisse-firewall $CADDY_ADDR $SB_PORT
[Install]
WantedBy=docker.service
EOF
systemctl daemon-reload
systemctl enable --now nisse-firewall.service
systemctl restart nisse-firewall.service # picks up a changed rule
docker compose -f /opt/stacks/silverbullet/compose.yaml up -d
echo "SilverBullet is up on port $SB_PORT, reachable from $CADDY_ADDR only"
@@ -0,0 +1,26 @@
# SilverBullet on the agent host: the operator's view of the vault (L10, M5.4).
# Installed to /opt/stacks/silverbullet by ../setup.sh. No secrets here: its
# accounts are made in its setup wizard and kept in its data folder.
services:
silverbullet:
# Pinned: the docker role updates stacks' images nightly, and a pinned
# version only changes in a PR here.
image: ghcr.io/silverbulletmd/silverbullet:2.11.1-slim
restart: unless-stopped
environment:
PUID: "1000"
PGID: "1000"
SB_NAME: Nisse vault
ports:
# IPv4 only; setup.sh's firewall lets only the operator's Caddy in.
- "0.0.0.0:3000:3000"
volumes:
# Its own accounts and settings.
- /srv/nisse/silverbullet:/data
# The vault, the space's folder: the operator writes everything in it...
- /srv/nisse/vault:/vault
# ...except the agents' memory, which only the agents write (one writer
# per folder, enforced by the kernel).
- /srv/nisse/vault/memory:/vault/memory:ro
logging:
driver: journald # logs go to the host's journal (L12)