From e58917106dbb4f9a7214e8d0595510041f028952 Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Fri, 1 May 2026 12:32:53 +0100 Subject: [PATCH] spec/tasks: pull repo-credential plumbing into Phase 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- spec.md | 36 ++++++++++++++++++++++++++++++++---- tasks.md | 19 +++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/spec.md b/spec.md index f1f3609..e71270f 100644 --- a/spec.md +++ b/spec.md @@ -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 diff --git a/tasks.md b/tasks.md index a54e87f..5a0be28 100644 --- a/tasks.md +++ b/tasks.md @@ -67,11 +67,30 @@ Sizes: **S** = under a day, **M** = 1–3 days, **L** = 3–7 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`. ---