Files
command-center/agent/managed-skills/rotate-fleet-service-host-secret/SKILL.md
T
Frank 8af40c402b chore(agent): commit the machine tier and the doctrine text the checkout was already running
Landing blocker for #61 and #63: the deployment checkout is 12 commits behind and
`merge --ff-only` refuses while tracked paths are dirty. Discard is not available here
(workspace policy: no `checkout`/`stash` on shared state), so the dirt is committed.

- `agent/APPEND_SYSTEM.md`: the point-of-use / one-work-log / PR-link doctrine, already
  byte-identical to `origin/main` (`cmp` against the trunk blob: no difference). No new
  prose — the commit exists so the trunk merge can proceed.
- `agent/config.yml` and `agent/managed-skills/**`: the machine-live tier the heartbeat
  owns (AGENTS.md § Live-sync tiers).

`ALLOW_SECRETS=1` was used, deliberately and after auditing the staged diff by hand: the
only credential-shaped strings in it are two copies of `AKIA0000000000000EXAMPLE` — the
inert all-zero AWS example id — and they appear inside the two skill pages that teach
that exact placeholder shape. This checkout is still running the pre-#61 hook; the merged
gate (value-scoped placeholder filter, `71601f7`) exempts this value, so the refusal
dies with this landing. No `--no-verify` was used: the gate ran and its verdict is
recorded here.

Refs #61
2026-10-21 01:44:51 -07:00

42 lines
3.8 KiB
Markdown

---
name: rotate-fleet-service-host-secret
description: "Rotate a Dank-fleet rootless-quadlet service's host-side secret (e.g. open-notebook OPEN_NOTEBOOK_PASSWORD) safely: confirm the deployer's secret seeder is idempotent first so a future deploy won't clobber the new value, change ONLY the target line while holding encryption keys byte-identical, then restart the user unit and prove the rotation (new token 200, old token 401, no token 401). Not for git/Gitea tokens or LLM endpoint secrets."
---
# Rotate a fleet service's host-side secret safely
Host secrets live OUTSIDE the repo, in `/home/dank/Containers/<svc>/env` (mode 600). The repo only holds the knobs (`scripts/<svc>.env`, secrets declared EMPTY). Changing a secret = edit the host file, never the repo.
## 1. First: prove the deployer won't clobber it (MANDATORY)
`deploy-remote.sh` seeds some secrets from `dankclaw/.env`. Confirm the seed fn is **idempotent** before editing, or a later `deploy.sh` overwrites your value. In `scripts/deploy-remote.sh`, `seed_<svc>_env` must look like open-notebook's:
```bash
for var in A B C D; do grep -q "^${var}=." "$dest" 2>/dev/null || missing=1; done
[[ "$missing" == 0 ]] && return 0 # all present → leaves the file untouched
if [[ ! -e "$dest" ]]; then ...seed from dankclaw/.env...; fi
```
If it returns-early on all-present, editing `$dest` directly is deploy-safe. If it seeds unconditionally, edit BOTH the host file and `dankclaw/.env` instead.
## 2. Read, then rewrite only the target line
`read ssh://<host>/home/dank/Containers/<svc>/env` (never print secrets to chat — they leak into the transcript). Then `write ssh://...` overwriting with **all** lines, changing only the one secret. **Hold encryption keys byte-identical** (e.g. `OPEN_NOTEBOOK_ENCRYPTION_KEY`): a DB-encrypted credential store is unreadable if that key changes. Then `chmod 600`.
- ssh:// `read` emits NO edit tag, so the `edit` tool can't target it — use `write` (full overwrite) with the exact values from the read.
## 3. Restart the right unit
Rootless quadlets are USER units: `ssh <host> systemctl --user restart <svc>.service`. (systemctl --user works over plain ssh here — linger + bus are set up.) Restart only the app unit; DB password unchanged means the DB unit needs no restart.
## 4. Prove the rotation behaviorally (this is the smoke test)
Hit a protected API route three ways, expecting **new=200 / old=401 / none=401**. Pass the bearer via a mode-600 curl config + `-K` (never `-H "Bearer …"` — argv leak):
```bash
ssh <host> 'cfg=$(mktemp); chmod 600 "$cfg"
printf "header = \"Authorization: Bearer %s\"\n" "<new-or-old>" > "$cfg"
curl -s -o /dev/null -w "%{http_code}\n" -K "$cfg" http://127.0.0.1:5056/api/<protected>'
```
## Gotchas that bit
- **Health is on the API loopback port, not the web port.** open-notebook: `/health` answers on **127.0.0.1:5056** (the FastAPI), NOT `:8502` (Next.js, which 404s `/health`). Probing the wrong port reads as "service failed" when it's up and `healthy`. Excluded/auth-free paths (`/`, `/health`, `/docs`, `/openapi.json`, `/redoc`) — everything else needs the bearer.
- **`OPEN_NOTEBOOK_PASSWORD` unset = auth OFF** for anything that can reach the port (other containers dial it by name on `dankclaw_default`), so it's mandatory, not cosmetic; deployer refuses to start on blank.
- Browser/clients keep the old secret in localStorage → they 401 on next call and re-prompt. Expected.
- **Harness command-parser gotcha:** adjacent/interleaved single+double quotes in one bash line (`'...'"$x"'...'`) crash `pi-natives:command: unterminated quote`. Build a curl-config `header=` line with the proven single-quoted printf (`printf 'header = "Authorization: token %s"\n' "$tok"`) and put long bodies in a file via the `write` tool + `jq --rawfile`, not a giant inline `$(printf …)`.