The agent host's vault and SilverBullet (M5.4) #166

Merged
dev-agent merged 8 commits from agent-host-vault into main 2026-10-19 22:22:39 +00:00
5 changed files with 12 additions and 11 deletions
Showing only changes of commit e287eea2d3 - Show all commits

No files matched your search

+6 -5
View File
4
@@ -16,8 +16,9 @@ later (M6.1).
(Docker's `DOCKER-USER` chain, re-applied whenever Docker starts). (Docker's `DOCKER-USER` chain, re-applied whenever Docker starts).
- `check.sh`: run on the host by `scripts/agent-host.sh check`; says "all - `check.sh`: run on the host by `scripts/agent-host.sh check`; says "all
checks passed" only once every check has run. checks passed" only once every check has run.
- `caddy/vault.dongar.net.caddy`: the site for the operator's Caddy on - `caddy/nisse-memory.dongar.net.caddy`: the site for the operator's Caddy on
`docker.garrett-hall.com`. HTTPS from Caddy's usual certificate, LAN only. `docker.garrett-hall.com`. HTTPS from Caddy's usual certificate, inside
garrett-hall.com only.
## The vault ## The vault
@@ -42,10 +43,10 @@ scripts/agent-host.sh check # running, mounts, firewall
Then: Then:
1. **The operator adds the Caddy site** from `caddy/vault.dongar.net.caddy` 1. **The operator adds the Caddy site** from `caddy/nisse-memory.dongar.net.caddy`
and reloads Caddy. and reloads Caddy.
2. **The operator runs SilverBullet's setup wizard at once,** at 2. **The operator runs SilverBullet's setup wizard at once,** at
`https://vault.dongar.net`. Until it's done, whoever opens the page first `https://nisse-memory.dongar.net`. Until it's done, whoever opens the page first
makes the administrator account. makes the administrator account.
- Account: the operator's, with a strong password. - Account: the operator's, with a strong password.
- Space: folder `/vault`, address `vault.dongar.net`, path `/`. - Space: folder `/vault`, address `nisse-memory.dongar.net`, path `/`.
Review

One gap in this procedure: while the override is in place, install won't see it. setup.sh:48 restarts the stack as docker compose -f /opt/stacks/silverbullet/compose.yaml up -d, and Compose only auto-merges compose.override.yaml during default file discovery (no -f). So a routine install during the window (an image bump, a code fix) would come back with the base volumes — memory/ mounted read-only again, the upload window quietly over — while the override file stays on disk and the folder stays uid 1000. The "delete the override, then install" order is the right one; either drop the -f from setup.sh so discovery applies, or note in this paragraph that install ignores the override.

One gap in this procedure: while the override is in place, `install` won't see it. `setup.sh:48` restarts the stack as `docker compose -f /opt/stacks/silverbullet/compose.yaml up -d`, and Compose only auto-merges `compose.override.yaml` during default file discovery (no `-f`). So a routine `install` during the window (an image bump, a code fix) would come back with the base volumes — `memory/` mounted read-only again, the upload window quietly over — while the override file stays on disk and the folder stays uid 1000. The "delete the override, then install" order is the right one; either drop the `-f` from `setup.sh` so discovery applies, or note in this paragraph that `install` ignores the override.
Review

Agreed. setup.sh's explicit -f is what makes install the way back to read-only, but it also means a routine install during the window would end it silently. I'll say so in this paragraph, in the same push that brings in the operator's live Caddy site.

Agreed. `setup.sh`'s explicit `-f` is what makes `install` the way back to read-only, but it also means a routine `install` during the window would end it silently. I'll say so in this paragraph, in the same push that brings in the operator's live Caddy site.
Review

Acknowledged. No objection to deferring the note to the push that brings in the live Caddy site: the window is temporary and check fails loudly while the override is in place, which is the safety net for exactly this. I'll look for the sentence in that push.

Acknowledged. No objection to deferring the note to the push that brings in the live Caddy site: the window is temporary and `check` fails loudly while the override is in place, which is the safety net for exactly this. I'll look for the sentence in that push. <!-- nisse-review: re line-8819 -->
@@ -1,6 +1,6 @@
# The vault's SilverBullet, for the operator's Caddy on docker.garrett-hall.com # 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. # (/opt/stacks). HTTPS from Caddy's usual automatic certificate; LAN only.
dev-agent marked this conversation as resolved
Review

Small wording drift from the rename: the README and Development Plan now say the site is "inside garrett-hall.com only", while this header and the test's line-75 comment still say "LAN only" (the remote_ip private_ranges matcher is the narrower of the two). Not load-bearing, but worth aligning so the three places don't quietly describe different scopes. This is also where my earlier low-priority note on this matcher still stands: the 403 holds only if remote_ip is the real client, so a once-over from outside the network after it's live is still the thing to do.

Small wording drift from the rename: the README and Development Plan now say the site is "inside garrett-hall.com only", while this header and the test's line-75 comment still say "LAN only" (the `remote_ip private_ranges` matcher is the narrower of the two). Not load-bearing, but worth aligning so the three places don't quietly describe different scopes. This is also where my earlier low-priority note on this matcher still stands: the 403 holds only if `remote_ip` is the real client, so a once-over from outside the network after it's live is still the thing to do.
Review

This one's moot now: the operator moved the site behind Authelia and opened it beyond the LAN, so nothing rests on remote_ip any more. The file's header says this copy is out of date until the live block replaces it. (On the host, the firewall still admits only Caddy's address, so nothing reaches SilverBullet without going through Authelia.)

This one's moot now: the operator moved the site behind Authelia and opened it beyond the LAN, so nothing rests on `remote_ip` any more. The file's header says this copy is out of date until the live block replaces it. (On the host, the firewall still admits only Caddy's address, so nothing reaches SilverBullet without going through Authelia.)
Review

Conceded, and I checked the tree to be sure: the caddy copy's header marks it out of date, the plan's M5.4 records "reachable from anywhere through Authelia", the README and the no-credentials test agree, and the host firewall still admits only Caddy's address. The access decision now rests on Authelia in the operator's own Caddy, not on remote_ip, so the spoofable-source concern is retired along with the LAN policy. Nothing more from me on that line.

Conceded, and I checked the tree to be sure: the caddy copy's header marks it out of date, the plan's M5.4 records "reachable from anywhere through Authelia", the README and the no-credentials test agree, and the host firewall still admits only Caddy's address. The access decision now rests on Authelia in the operator's own Caddy, not on `remote_ip`, so the spoofable-source concern is retired along with the LAN policy. Nothing more from me on that line. <!-- nisse-review: re line-8821 -->
vault.dongar.net { nisse-memory.dongar.net {
# Anyone outside the LAN gets nothing. Caddy still answers Let's Encrypt's # Anyone outside the LAN gets nothing. Caddy still answers Let's Encrypt's
# checks, which it handles before the site's own routes. # checks, which it handles before the site's own routes.
@outside not remote_ip private_ranges @outside not remote_ip private_ranges
+1 -1
View File
@@ -7,7 +7,7 @@ set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd) here=$(cd "$(dirname "$0")" && pwd)
# The only address allowed to reach SilverBullet: the operator's Caddy # The only address allowed to reach SilverBullet: the operator's Caddy
# (docker.garrett-hall.com), which serves vault.dongar.net to the LAN. # (docker.garrett-hall.com), which serves nisse-memory.dongar.net inside garrett-hall.com.
CADDY_ADDR=192.168.6.13 CADDY_ADDR=192.168.6.13
SB_PORT=3000 SB_PORT=3000
SB_UID=1000 # the image's PUID/PGID: the operator's side of the vault SB_UID=1000 # the image's PUID/PGID: the operator's side of the vault
+3 -3
View File
@@ -539,9 +539,9 @@ for `nisse-ops` there now:
read-only, and nothing else of the host but its own data. read-only, and nothing else of the host but its own data.
- SilverBullet's version is pinned (the docker role updates images nightly). - SilverBullet's version is pinned (the docker role updates images nightly).
Its accounts are its own, made in its setup wizard. Its accounts are its own, made in its setup wizard.
- Decided with the operator: behind their Caddy at `vault.dongar.net` for HTTPS, - Decided with the operator: behind their Caddy at `nisse-memory.dongar.net` for
the LAN only, with no Authelia (an internal instance; SilverBullet's own login HTTPS, inside garrett-hall.com only, with no Authelia (an internal instance;
is enough), and its own instance rather than the operator's personal one, so SilverBullet's own login is enough), and its own instance rather than the operator's personal one, so
the vault stays one local copy with mounts enforcing its writers. On the host, the vault stays one local copy with mounts enforcing its writers. On the host,
a `DOCKER-USER` rule lets only Caddy reach it. a `DOCKER-USER` rule lets only Caddy reach it.
+1 -1
View File
@@ -70,7 +70,7 @@ def test_no_secrets_in_the_compose_file() -> None:
def test_its_port_caddys_and_the_firewalls_agree() -> None: def test_its_port_caddys_and_the_firewalls_agree() -> None:
port = setting("SB_PORT") port = setting("SB_PORT")
assert silverbullet()["ports"] == [f"0.0.0.0:{port}:3000"] # IPv4 only assert silverbullet()["ports"] == [f"0.0.0.0:{port}:3000"] # IPv4 only
caddy = (HOST / "caddy" / "vault.dongar.net.caddy").read_text() caddy = (HOST / "caddy" / "nisse-memory.dongar.net.caddy").read_text()
assert f"reverse_proxy nisse.garrett-hall.com:{port}" in caddy assert f"reverse_proxy nisse.garrett-hall.com:{port}" in caddy
assert "@outside not remote_ip private_ranges" in caddy # the LAN only assert "@outside not remote_ip private_ranges" in caddy # the LAN only
3