drop restic repo encryption; rely on TLS + append-only + LUKS
User credentials now serve HTTP basic auth only. Repos init with --insecure-no-password. Removes: - RESTIC_PASSWORD env in client subprocess - Per-repo password coordination story - Multi-key restic setup (user key + operator-master key) - Two-password recovery edge cases Operator-side prune now runs over the filesystem path (-r /srv/.../<user>/) which bypasses rest-server's HTTP-layer append-only enforcement. No password needed at all. Protection model stays: - TLS in transit (reverse proxy) - HTTP basic per-user (htpasswd) for read/write authorization - --private-repos for per-user URL isolation - --append-only for client-side delete protection - LUKS / disk-level for at-rest encryption (operator's responsibility) Verified end-to-end on john: pull → push → restore round-trip works, DELETE on bogus snapshot still returns 403 (append-only intact), operator can read repo via filesystem path (prune-mode access works). 33 pytest still green.
This commit is contained in:
@@ -17,7 +17,7 @@ Per-Discord-user state sync for Minecraft. Pulls on launch, pushes on exit. Sing
|
||||
| Retention policy | `restic forget --keep-last/daily/weekly/monthly` |
|
||||
| Append-only enforcement | `restic-rest-server --append-only`: even with a valid password, clients can't delete |
|
||||
| Per-user isolation | `--private-repos`: URL path must contain the authenticated username |
|
||||
| Encryption at rest | Per-repo password, built in |
|
||||
| Encryption at rest | Disabled (`--insecure-no-password`); delegated to LUKS on host disk |
|
||||
| Multi-machine support | Restic tags + hostname; if we ever want it, free |
|
||||
|
||||
cloud-svc as originally designed was a worse re-implementation of all the above. Pivoting before it ships; cloud-svc gets reshaped into the control plane described below.
|
||||
@@ -72,15 +72,15 @@ htpasswd"/]:::pvc
|
||||
POST /admin/users"| cs_int
|
||||
cs_int -.->|"htpasswd add
|
||||
restic init
|
||||
key add"| htp
|
||||
--insecure-no-password"| htp
|
||||
cs_int -.->|"mints repo"| store
|
||||
bot -.->|"DM password"| pl
|
||||
|
||||
op -.->|"SSH then
|
||||
automc-setup cloud ..."| cs_admin
|
||||
cs_admin -.->|"list / prune / revoke"| htp
|
||||
cs_admin -.->|"list / revoke"| htp
|
||||
cs_admin -.->|"prune via
|
||||
operator master key"| store
|
||||
filesystem path"| store
|
||||
|
||||
classDef deploy fill:#d5e8d4,stroke:#82b366,color:#000
|
||||
classDef pvc fill:#f5f5f5,stroke:#666,color:#000
|
||||
@@ -105,11 +105,11 @@ Auth model:
|
||||
| Element | Value |
|
||||
|---|---|
|
||||
| User identity | Discord ID (immutable, from discord-bot's existing account-card flow) |
|
||||
| User credential | restic repo password = bcrypt'd in `/etc/restic-users` htpasswd file |
|
||||
| User credential | One password per user. HTTP basic auth ONLY — bcrypt'd in `/etc/restic-users` htpasswd file. Restic repos use `--insecure-no-password`, so this password does NOT also encrypt blobs. |
|
||||
| URL pattern | `rest:https://cloud.tm.center/<discord_id>/` |
|
||||
| Server isolation | `--private-repos` enforces URL path matches authenticated user |
|
||||
|
||||
discord-bot's `/register` flow extends to call `POST cloud-svc:9091/admin/users` with the player's Discord ID. cloud-svc mints a random password, `htpasswd -B`-adds it to the file, runs `restic init` + `restic key add operator-master`, and returns the password. discord-bot DMs it to the player. discord-bot itself never touches restic or htpasswd directly.
|
||||
discord-bot's `/register` flow extends to call `POST cloud-svc:9091/admin/users` with the player's Discord ID. cloud-svc mints a random password, `htpasswd -B`-adds it to the file, runs `restic init --insecure-no-password`, and returns the password. discord-bot DMs it to the player. discord-bot itself never touches restic or htpasswd directly.
|
||||
|
||||
Revocation = operator runs `automc-setup cloud revoke <discord_id>` which hits the loopback admin port. No token store, no scope checks, no auth-service involvement.
|
||||
|
||||
@@ -184,26 +184,26 @@ Server-side cron (e.g., daily at 04:00 UTC) walks all per-user repos:
|
||||
```bash
|
||||
for repo in /srv/cloud-data/*/; do
|
||||
user=$(basename "$repo")
|
||||
restic -r "$repo" --password-file /etc/restic-master-pass \
|
||||
restic -r "$repo" --insecure-no-password \
|
||||
forget --keep-last=20 --keep-daily=7 --keep-weekly=4 --keep-monthly=6 --prune
|
||||
done
|
||||
```
|
||||
|
||||
This requires the operator to have a "master password" that opens any user's repo — restic doesn't have that natively. **Options:**
|
||||
Operator-side prune talks to restic **directly on the filesystem** (`-r /srv/cloud-data/<user>/`), bypassing the rest-server's `--append-only` enforcement. No HTTP, no password. Works because:
|
||||
|
||||
1. **Init each user's repo with TWO keys** — one for the user, one for the operator-side pruner. restic supports multi-key per repo.
|
||||
2. **Run the cron with each user's own password** — requires storing all user passwords server-side; defeats the encryption.
|
||||
3. **Don't auto-prune** — let users push forever, trust quota at the rest-server level.
|
||||
- Repos use `--insecure-no-password` (no encryption key to coordinate)
|
||||
- The operator owns the on-disk files anyway
|
||||
- `--append-only` is a rest-server HTTP-layer policy; the filesystem doesn't care
|
||||
|
||||
Recommendation: **option 1** (multi-key per repo). On `/register`, the bot calls `restic -r <repo> --password-file <operator> key add` to add the player's password as a SECOND key. The pruner cron uses the operator master password.
|
||||
Previous drafts of this doc proposed a multi-key restic setup (one key per user + one operator-master key) to enable HTTP-mode prune. That's no longer needed.
|
||||
|
||||
## What's in v1
|
||||
|
||||
- restic-rest-server with `--private-repos --append-only --htpasswd-file`
|
||||
- discord-bot `/register` extension: mint password, htpasswd add, `restic init` repo, `restic key add` player key
|
||||
- cloud-sync.pyz that subprocesses restic for pull/push
|
||||
- discord-bot `/register` extension: mint password, htpasswd add, `restic init --insecure-no-password`
|
||||
- cloud-sync.pyz that subprocesses restic for pull/push (`--insecure-no-password` on every call)
|
||||
- Auto-download restic binary on first run from upstream GitHub release
|
||||
- Server-side nightly prune cron with operator-side master password key
|
||||
- Server-side nightly prune via separate systemd timer on john, running `restic forget --prune` directly on filesystem paths (bypassing the rest-server's HTTP-layer append-only enforcement)
|
||||
|
||||
## What's deferred
|
||||
|
||||
@@ -271,7 +271,8 @@ All locked 2026-06-02:
|
||||
|
||||
- [x] cloud-svc reshapes to control plane, not archived
|
||||
- [x] Two-port split — automc-net for provisioning, loopback for operator
|
||||
- [x] Server-side prune via operator master password key on each repo. On `provision`, cloud-svc runs `restic init` then `restic key add` with the operator-master password as a SECOND key. The nightly pruner uses the operator key to open any repo.
|
||||
- [x] **Repo encryption disabled** (`--insecure-no-password`). Per-user password covers HTTP basic auth ONLY. Defense-in-depth via repo encryption was dropped for the homelab scope; protection delegated to LUKS on disk + TLS at proxy + append-only at rest-server. Cuts provisioning from 3 restic ops to 1, removes the two-password coordination problem.
|
||||
- [x] Server-side prune over filesystem path (`-r /srv/cloud-data/<user>/`). Bypasses rest-server's HTTP-layer `--append-only`. No multi-key dance needed.
|
||||
- [x] cloud-sync.pyz auto-downloads restic binary. Matches `packwiz-installer-bootstrap` pattern. First run hits `https://github.com/restic/restic/releases` for the matching platform binary, caches under `<pack-folder>/.cloud-sync/restic-<version>`. SHA256 verified against the release's `SHA256SUMS` file. `--no-download` flag for air-gapped operators.
|
||||
- [x] Nightly prune at 04:00 UTC. `time.Ticker` inside cloud-svc; no external cron. `--prune-time HH:MM` flag in case operators want a different window.
|
||||
- [x] Nightly prune at 04:00 UTC via **separate systemd timer on john** (not embedded in cloud-svc), for fault isolation — prune crash doesn't take down provisioning. The timer also runs `restic copy` to the homelab primary before pruning (per-user repos, mirroring john's layout).
|
||||
- [x] Per-caller tokens, NOT shared. cloud-svc reads `CLOUD_PROVISIONING_TOKENS_BOT`, `CLOUD_PROVISIONING_TOKENS_<OTHER>` env vars — one per known caller. Logs include the matched caller name so audit trails show which service made each call. Adding a future caller (e.g., a portal) means a new env var, not a token rotation.
|
||||
|
||||
Reference in New Issue
Block a user