spec/tasks: pull repo-credential plumbing into Phase 1

Adds P1-32/33/34: encrypted repo creds carried on the enrollment token,
agent-side AEAD secrets file, end-to-end smoke. spec.md §4.2 and §7.3
rewritten to describe the full flow (server-issued at token time,
pushed via config.update on hello, persisted encrypted on the agent)
and to make the encrypted-file-now / OS-keyring-Phase-2 split
explicit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 12:32:53 +01:00
parent 51bbb555d4
commit 8d8150ee6e
2 changed files with 51 additions and 4 deletions
+32 -4
View File
@@ -123,7 +123,14 @@ It is built for small-to-medium fleets (initial target: ~12 endpoints) and is in
- **Service integration:** systemd unit (Linux). Windows service via
`golang.org/x/sys/windows/svc` — Phase 2.
- **Footprint goal:** ≤ 15 MB binary, ≤ 50 MB RSS idle
- **Persistence:** local config file + small state DB (BoltDB or JSON) for queued reports if server is unreachable
- **Persistence:** `agent.yaml` (server URL, host ID, bearer, secrets
key) + an AEAD-encrypted secrets blob (`secrets.enc`) holding the
restic repo URL + password. Both files are mode 0600 owned by the
agent service user. Phase 1 ships the encrypted-file form on
Linux; Phase 2 swaps that for OS-keyring storage (DPAPI on Windows,
Secret Service / `pass` on Linux where a session bus is
available — see §7.3). A small state DB (BoltDB or JSON) for
queued reports lands when offline-resilience work does.
- **Restic invocation:** spawns `restic` with `--json`, parses streamed output, forwards to server in real time
- **Updates:** distributed via OS package manager — apt repo (Linux) and
Chocolatey package (Windows), both pointing at gitea releases. No
@@ -341,9 +348,30 @@ offline.
- **viewer:** read-only
### 7.3 Secret handling
- Restic repo passwords and REST-server credentials encrypted at rest in SQLite using a server-side key (loaded from env or file at startup)
- Pushed to agents only over the authenticated WS, only when needed for a job
- Agent stores them in OS keyring where available (Windows DPAPI, Linux Secret Service / fallback to encrypted file with restricted perms)
- Restic repo passwords and REST-server credentials encrypted at rest
in SQLite using a server-side key (loaded from env or file at
startup, AEAD via `internal/crypto`).
- Operator supplies repo URL + username + password when minting an
enrollment token. The token row holds them as a single encrypted
blob; on `ConsumeEnrollmentToken` the blob is moved to a
`host_credentials` row keyed by `host_id` (same tx).
- Pushed to agents over the authenticated WS as a `config.update`
message — sent immediately after the agent's `hello` on every
connect, and again whenever the operator edits the credential.
Agents that connect before credentials exist proceed normally
but refuse to start backup jobs until the push arrives.
- Agent persistence:
- **Phase 1, Linux:** AEAD-encrypted file at
`/var/lib/restic-manager/secrets.enc`, key stored in
`agent.yaml` alongside the bearer (same 0600 trust boundary).
Atomic writes (tmp+fsync+rename).
- **Phase 2:** OS keyring where available — Windows DPAPI; Linux
Secret Service via `pass` / `gnome-keyring` / `kwallet` when a
session bus is present. The encrypted-file path stays as the
fallback for headless boxes.
- Plaintext repo passwords never appear in `agent.yaml`, server logs,
audit-log payloads, or job-log streams. The audit log records
*that* a credential was set/changed and by whom, never the value.
### 7.4 Repo protection
- Restic REST server runs with `--append-only` for routine backups
+19
View File
@@ -67,11 +67,30 @@ Sizes: **S** = under a day, **M** = 13 days, **L** = 37 days.
- [x] **P1-29** (M) `install.sh` (Linux): detects arch, downloads agent, installs systemd unit, enrolls. Detects existing restic timers / `/etc/cron.{d,daily,hourly,weekly}/*` / root crontab and prints them with the exact disable commands — does **not** auto-disable
- [~] **P1-31** (S) Server endpoint to serve agent binaries + install scripts ✓ (`/agent/binary` + `/install/*`); **signature verification** deferred to Phase 5 OSS readiness
### Repo credentials (pulled forward from Phase 2)
- [ ] **P1-32** (M) Server-side encrypted repo creds carried on the enrollment token:
- `POST /api/enrollment-tokens` body grows `repo_url`, `repo_username`, `repo_password` (all required).
- Token row stores them as one AEAD-encrypted blob (existing `crypto.AEAD`); `ConsumeEnrollmentToken` moves the blob to a new `host_credentials` row keyed by `host_id` in the same tx.
- `PUT /api/hosts/{id}/repo-credentials` (admin/operator) re-encrypts and replaces the row, emits an in-memory event to the WS hub.
- On WS `hello`, server pushes a `config.update` with decrypted creds **before** returning the connection to idle. Same path on edit-while-connected.
- Audit-logged on create / consume / edit; payload omits the secret material.
- [ ] **P1-33** (M) Agent-side encrypted secrets store:
- New `internal/agent/secrets` package: AEAD blob at `/var/lib/restic-manager/secrets.enc`, atomic write (tmp+fsync+rename, mode 0600).
- Per-host 32-byte secrets key minted at enrollment, persisted in `agent.yaml` (already 0600 root-only — same trust boundary as the bearer; explicit comment in the file).
- Strip `repo_url` / `repo_password` from `agent.config.Config`. Agent loads creds from `secrets.enc` at startup; `config.update` handler writes through to the file.
- Dispatcher reads from the secrets store on every job rather than from in-memory config.
- Migration path: if `agent.yaml` still contains `repo_url`/`repo_password`, copy them into `secrets.enc` on next start, then strip from the YAML on save.
- [ ] **P1-34** (S) End-to-end smoke: enrollment with repo creds → agent receives them via push-on-connect → run-now backup completes against a real `restic/rest-server` in a sibling container → host appears with snapshot count.
### Phase 1 acceptance
- One Linux host can enroll, appear in the dashboard, and a backup can be triggered from the UI with live log streaming. Snapshots list updates after success.
- Windows binary builds cleanly in CI (`.gitea/workflows/ci.yml`) but is not service-tested or installer-shipped in Phase 1 — that lands in Phase 2 (P2-16, P2-17).
- Agent ↔ server `protocol_version` handshake rejects mismatched versions with a clear error rather than failing on JSON parse.
- Repo credentials never appear in plaintext on disk: server stores them AEAD-encrypted, agent stores them AEAD-encrypted, the wire carries them only inside the authenticated WS as `config.update`.
---