fix: enrollment FK race + log-when-rejected; runbook fixes from dry-run

The smoke runbook caught a real bug: ConsumeEnrollmentToken was
inserting into host_credentials (FK -> hosts) inside the same tx as
the token burn, but the host row didn't exist yet — CreateHost
runs in the *next* statement. The agent saw a generic 401 with no
clue why.

Fix: drop the host_credentials insert from ConsumeEnrollmentToken;
the HTTP handler now does Consume -> CreateHost ->
SetHostCredentials. SetHostCredentials failure is logged loudly
but doesn't fail the enrol — operator recovers via PUT
/api/hosts/{id}/repo-credentials.

Adds slog.Warn lines on both 401 paths in handleAgentEnroll so the
underlying cause is visible in server logs (the wire response stays
generic to avoid leaking which step failed).

Test: TestEnrollmentTransfersRepoCreds rewritten to mirror the new
order (consume -> create host -> SetHostCredentials).

Runbook (docs/e2e-smoke.md): rest-server moved off 8000 (commonly
in use); URLs use trailing slash on the rest path; clarified that
secrets_key is minted on first agent start, not at enrol time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 14:01:59 +01:00
parent 6cfbdfc7ab
commit 44feb708bc
5 changed files with 47 additions and 44 deletions
+13 -8
View File
@@ -66,7 +66,9 @@ services:
environment:
- OPTIONS=--no-auth # smoke-test only; real deploys use --append-only + htpasswd
ports:
- "127.0.0.1:8000:8000"
# Mapped to 8100 because most dev boxes already have something
# on 8000. Use any free port; just keep the URLs below in sync.
- "127.0.0.1:8100:8000"
volumes:
- ./rest:/data
@@ -118,7 +120,7 @@ ENROLL=$(curl -s -b /tmp/rm-smoke/cookies -X POST http://127.0.0.1:8080/api/enro
-H 'content-type: application/json' \
-d '{
"hostname":"smoke-host",
"repo_url":"rest:http://127.0.0.1:8000/smoke",
"repo_url":"rest:http://127.0.0.1:8100/smoke/",
"repo_username":"",
"repo_password":"smoke-pw"
}')
@@ -136,7 +138,7 @@ restic itself wants the repo initialised:
```sh
RESTIC_PASSWORD=smoke-pw \
restic -r rest:http://127.0.0.1:8000/smoke init
restic -r rest:http://127.0.0.1:8100/smoke/ init
```
## 6. Pretend to be a fresh endpoint
@@ -169,9 +171,11 @@ HOST_ID=$(grep host_id "$CONFIG" | awk '{print $2}' | tr -d '"')
echo "host id: $HOST_ID"
```
After enrolment, `agent.yaml` should contain `secrets_key:` (a
base64 32-byte key) and `host_id:` (a ULID). It should **not**
contain `repo_url:` or `repo_password:`.
After enrolment, `agent.yaml` should contain `host_id:` (a ULID),
`agent_token:`, and `server_url:`. It will **not** contain
`secrets_key:` yet — that's minted on the first non-enroll start
of the agent (next step). It should **not** contain `repo_url:`
or `repo_password:` (those never appear in plaintext on disk).
```sh
cat "$CONFIG"
@@ -196,7 +200,8 @@ ws agent: repo credentials updated via config.update
That last line confirms slice 1 + 2 of P1-32/33: the server pushed
the encrypted creds, the agent decrypted, persisted to `secrets.enc`,
and is now ready to back up. `secrets.enc` should now exist and be
0600.
0600. `agent.yaml` should now also contain a freshly-minted
`secrets_key:` (base64-encoded 32 bytes).
```sh
ls -l /tmp/rm-smoke/agent/var-lib/secrets.enc
@@ -235,7 +240,7 @@ curl -s -b /tmp/rm-smoke/cookies \
"http://127.0.0.1:8080/api/hosts/$HOST_ID/repo-credentials" | jq
```
Expect `{"repo_url":"rest:http://127.0.0.1:8000/smoke","has_password":true}`.
Expect `{"repo_url":"rest:http://127.0.0.1:8100/smoke/","has_password":true}`.
The password is never returned over this endpoint.
## 11. Edit creds + verify push-on-update