From 6e203dcb999b084b383b93ba42a46425eebf6534 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Sun, 27 Sep 2026 01:51:05 -0700 Subject: [PATCH] fix(restic): stop publishing rest-server passwords in systemd units resticprofile schedule copies env-file values into the generated units, which are 0644, so RESTIC_REPOSITORY (the rest-server basic-auth password included) was readable by every local user on every restic host. New playbooks/restic-repository-file.yaml: - derives /etc/restic/repository (root 0400) from restic.env; - uploads the profile switched to repository-file, but only when the live profile's sha matches the repo copy it was edited from (drift guard); - checks the repository is reachable through the new profile (cat config); - regenerates the units and verifies they exist and contain no rest:http. Applied to ana-docker, fv-ml1 (configs/restic/ana-ml2), esh-docker-vm, esh-vm-db, irv-ml1, nh3-dev and nh3-docker. An independent check across all eight restic hosts (these seven plus esh-ml1) found 0 leaking units. nh3-docker's scheduled unit ran a real backup afterwards (snapshot a29b889d). Each host's URL and passphrase are vaulted as /etc/restic/{repository,password}. restic.env is kept (root 0600) because the per-host READMEs and the freshness probe source it. A rotation must update the vault, restic.env and repository. vm-esh-nas has no infra-ops account. Its in-place migration script is staged for Prime to run with sudo, and its repo profile is pre-edited to match. Also mirrors augaman-dev's 401df2d (compose header only). The config hash on esh-ml1 is unchanged. --- configs/restic/README.md | 12 ++- configs/restic/ana-docker/profiles.yaml | 8 +- configs/restic/ana-ml2/profiles.yaml | 6 +- configs/restic/esh-docker-vm/profiles.yaml | 4 +- configs/restic/esh-ml1/README.md | 10 +- configs/restic/esh-ml1/profiles.yaml | 12 +-- configs/restic/esh-vm-db/profiles.yaml | 4 +- configs/restic/irv-ml1/profiles.yaml | 4 +- configs/restic/nh3-dev/profiles.yaml | 4 +- configs/restic/nh3-docker/profiles.yaml | 6 +- .../vm-esh-nas/migrate-repository-file.sh | 30 ++++++ configs/restic/vm-esh-nas/profiles.yaml | 2 +- docs/runbooks/backups.md | 25 +++-- persistent-memory.md | 10 ++ playbooks/restic-repository-file.yaml | 95 +++++++++++++++++++ stacks/augaman/compose.yaml | 5 +- 16 files changed, 202 insertions(+), 35 deletions(-) create mode 100755 configs/restic/vm-esh-nas/migrate-repository-file.sh create mode 100644 playbooks/restic-repository-file.yaml diff --git a/configs/restic/README.md b/configs/restic/README.md index f902b0d..caeffe0 100644 --- a/configs/restic/README.md +++ b/configs/restic/README.md @@ -22,7 +22,8 @@ On each server, deployed to `/etc/restic/`: ├── profiles.yaml # scp'd from configs/restic//profiles.yaml ├── pre-backup.sh # scp'd, 0755, root:root ├── password # 0400 root:root — client-side encryption passphrase -└── restic.env # 0600 root:root — RESTIC_REPOSITORY=rest:http://user:pw@host:port/path/ +├── repository # 0400 root:root — rest:http://user:pw@host:port/path/ (read via repository-file) +└── restic.env # 0600 root:root — RESTIC_REPOSITORY=… (same URL; kept for manual `. restic.env` snippets) /var/lib/restic/ ├── stage/ # temp staging for DB dumps; owned by root, 0700 └── last-success # unix timestamp of the last successful run @@ -30,8 +31,9 @@ On each server, deployed to `/etc/restic/`: ## Why this shape -- **No secrets in committed config.** `profiles.yaml` references `RESTIC_PASSWORD_FILE=/etc/restic/password` and loads `RESTIC_REPOSITORY` from `restic.env`. Both files live only on the host, 0400/0600 root-owned. -- **Creds go in the URL, not netrc.** restic's rest backend doesn't consult `~/.netrc` — HTTP basic-auth has to be embedded in the repository URL. Keeping that URL in an env-file (not the committed YAML) means the secret stays on the host. +- **No secrets in committed config.** `profiles.yaml` references `RESTIC_PASSWORD_FILE=/etc/restic/password` and reads the repository URL through `repository-file: /etc/restic/repository`. Both files live only on the host, root 0400. Both are also vaulted as `/etc/restic/{password,repository}` (2026-09-27). +- **Creds go in the URL, not netrc.** restic's rest backend doesn't consult `~/.netrc` — HTTP basic-auth has to be embedded in the repository URL. +- ⚠ **Never `env-file` for the URL.** Until 2026-09-27 the profiles loaded it with `env-file: /etc/restic/restic.env`. But `resticprofile schedule` copies env-file values into the generated systemd units, which are 0644, so every local user could read the rest-server password. All hosts except vm-esh-nas were moved with `playbooks/restic-repository-file.yaml`. vm-esh-nas has no infra-ops account; its script is `configs/restic/vm-esh-nas/migrate-repository-file.sh`, and Prime runs it with sudo. `restic.env` is kept (root 0600, not a leak) because the per-host READMEs and the freshness probe source it. **On a password rotation, update the vault, `restic.env` AND `repository`.** - **Pre-hook runs DB dumps into a staging dir**, then `restic backup` includes that dir alongside the regular paths. One snapshot = one point-in-time. - **`restic forget` is scheduled; `restic prune` is not.** Rest-server's `--append-only` blocks prune from the client side by design. Prune is a manual ceremony (flip the flag, run prune, flip back). @@ -66,12 +68,16 @@ ssh -t "$HOST" 'sudo install -d -o root -g root -m 0700 /var/lib/restic/stage' # 2. Seed the two secret files on the host (never in this repo): # - the client-side encryption passphrase (the one used at `restic init`) # - an env-file with the full RESTIC_REPOSITORY URL including HTTP creds +# - the same URL alone in /etc/restic/repository (what the profile reads) +# Vault them first: secret put /etc/restic/password, …/repository ssh -t "$HOST" 'sudo install -o root -g root -m 0400 /dev/null /etc/restic/password' ssh -t "$HOST" 'sudo install -o root -g root -m 0600 /dev/null /etc/restic/restic.env' +ssh -t "$HOST" 'sudo install -o root -g root -m 0400 /dev/null /etc/restic/repository' # Seed content (replace <…> with real values from your vault): ssh -t "$HOST" "echo '' | sudo tee /etc/restic/password >/dev/null" ssh -t "$HOST" "echo 'RESTIC_REPOSITORY=rest:http://:@:8000//' | sudo tee /etc/restic/restic.env >/dev/null" +ssh -t "$HOST" "echo 'rest:http://:@:8000//' | sudo tee /etc/restic/repository >/dev/null" # 3. Push the committed config + pre-hook scp "configs/restic/$HOST/profiles.yaml" "$HOST:/tmp/profiles.yaml" diff --git a/configs/restic/ana-docker/profiles.yaml b/configs/restic/ana-docker/profiles.yaml index 8cef08d..b8cb2aa 100644 --- a/configs/restic/ana-docker/profiles.yaml +++ b/configs/restic/ana-docker/profiles.yaml @@ -2,8 +2,8 @@ # # Writes to the Anaheim-side rest-server at 10.250.50.70 as user # `ana-docker`. The full REST URL (including HTTP basic-auth creds) -# lives in /etc/restic/restic.env — loaded via env-file so this YAML -# carries zero secrets and is safe to version-control. +# lives in /etc/restic/repository (root 0400), read via repository-file, so +# this YAML carries zero secrets and is safe to version-control. # # The client-side encryption passphrase lives in /etc/restic/password. @@ -17,7 +17,9 @@ global: min-memory: 100 default: - env-file: /etc/restic/restic.env # provides RESTIC_REPOSITORY=rest:http://user:pw@… + repository-file: /etc/restic/repository # URL incl. rest-server creds (root 0400). + # NOT env-file: `resticprofile schedule` copies env-file values into the + # world-readable systemd units (docs/runbooks/backups.md, Known gaps). env: RESTIC_PASSWORD_FILE: /etc/restic/password initialize: false # repo was created by `restic init` diff --git a/configs/restic/ana-ml2/profiles.yaml b/configs/restic/ana-ml2/profiles.yaml index 9c52378..ee973ff 100644 --- a/configs/restic/ana-ml2/profiles.yaml +++ b/configs/restic/ana-ml2/profiles.yaml @@ -4,7 +4,7 @@ # backup for this host's config/state, so it's the highest-stakes repo in # the fleet. Writes to the Anaheim rest-server at 10.250.50.70 as user # `ana-ml2`. The full REST URL (with HTTP basic auth creds) lives in -# /etc/restic/restic.env. The client-side encryption passphrase lives in +# /etc/restic/repository (root 0400). The client-side encryption passphrase lives in # /etc/restic/password. # # Notable exclusions: @@ -29,7 +29,9 @@ global: min-memory: 100 default: - env-file: /etc/restic/restic.env # RESTIC_REPOSITORY=rest:http://user:pw@… + repository-file: /etc/restic/repository # URL incl. rest-server creds (root 0400). + # NOT env-file: `resticprofile schedule` copies env-file values into the + # world-readable systemd units (docs/runbooks/backups.md, Known gaps). env: RESTIC_PASSWORD_FILE: /etc/restic/password initialize: false # repo created manually by `restic init` diff --git a/configs/restic/esh-docker-vm/profiles.yaml b/configs/restic/esh-docker-vm/profiles.yaml index 01f145a..19757b1 100644 --- a/configs/restic/esh-docker-vm/profiles.yaml +++ b/configs/restic/esh-docker-vm/profiles.yaml @@ -22,7 +22,9 @@ global: min-memory: 100 default: - env-file: /etc/restic/restic.env + repository-file: /etc/restic/repository # URL incl. rest-server creds (root 0400). + # NOT env-file: `resticprofile schedule` copies env-file values into the + # world-readable systemd units (docs/runbooks/backups.md, Known gaps). env: RESTIC_PASSWORD_FILE: /etc/restic/password initialize: false diff --git a/configs/restic/esh-ml1/README.md b/configs/restic/esh-ml1/README.md index 535355e..5ea8ac0 100644 --- a/configs/restic/esh-ml1/README.md +++ b/configs/restic/esh-ml1/README.md @@ -29,13 +29,13 @@ Tested by making it fail (2026-09-27): with a stub `docker` that exits 1 on PATH `resticprofile backup` returned 1, the snapshot count stayed 1 → 1, and `last-success` did not move. -## ⚠ Deviation from the other hosts: `repository-file`, not `restic.env` +## `repository-file`, not `restic.env` (now the fleet standard) `resticprofile schedule` copies `env-file` values into the generated systemd -units, which are world-readable. On the env-file hosts that publishes the -rest-server password to every local user (see `docs/runbooks/backups.md` → -Known gaps). Here the URL lives in `/etc/restic/repository` (root 0400) and the -unit carries only the path. The playbook's verify step fails if a unit ever +units, which are world-readable. That published the rest-server password on +every other host until 2026-09-27, when the rest of the fleet was moved to this +pattern (see `docs/runbooks/backups.md` → Known gaps). Here the URL lives in +`/etc/restic/repository` (root 0400), and the unit carries only the path. The playbook's verify step fails if a unit ever contains `rest:http`. ## Secrets (seeded by hand; never in this repo) diff --git a/configs/restic/esh-ml1/profiles.yaml b/configs/restic/esh-ml1/profiles.yaml index 30d3fcc..5daaba0 100644 --- a/configs/restic/esh-ml1/profiles.yaml +++ b/configs/restic/esh-ml1/profiles.yaml @@ -8,13 +8,13 @@ # Writes cross-site to rest-server-ana (10.250.50.70:8000/esh-ml1/) because # the ESH site has no local rest-server (same as esh-docker-vm / esh-vm-db). # -# ⚠ DELIBERATE DEVIATION from the other hosts: the repository URL (which -# embeds the rest-server basic-auth password) is read from -# /etc/restic/repository via `repository-file`, NOT from an env-file. +# The repository URL (which embeds the rest-server basic-auth password) is +# read from /etc/restic/repository via `repository-file`, NOT from an env-file. # `resticprofile schedule` copies env-file values into the generated systemd -# unit, and units under /etc/systemd/system are world-readable (0644), so -# the env-file pattern publishes the credential to every local user. With -# repository-file the unit carries only the path. See README.md. +# unit, and units under /etc/systemd/system are world-readable (0644), so the +# env-file pattern publishes the credential to every local user. With +# repository-file the unit carries only the path. See README.md. (esh-ml1 was +# first; the rest of the fleet moved to repository-file on 2026-09-27.) # # ⚠ The CT runs in UTC; the schedules carry an explicit zone so they fire at # the same wall-clock time as the rest of the fleet. diff --git a/configs/restic/esh-vm-db/profiles.yaml b/configs/restic/esh-vm-db/profiles.yaml index 4d88f15..30038f4 100644 --- a/configs/restic/esh-vm-db/profiles.yaml +++ b/configs/restic/esh-vm-db/profiles.yaml @@ -30,7 +30,9 @@ global: min-memory: 100 default: - env-file: /etc/restic/restic.env + repository-file: /etc/restic/repository # URL incl. rest-server creds (root 0400). + # NOT env-file: `resticprofile schedule` copies env-file values into the + # world-readable systemd units (docs/runbooks/backups.md, Known gaps). env: RESTIC_PASSWORD_FILE: /etc/restic/password initialize: false diff --git a/configs/restic/irv-ml1/profiles.yaml b/configs/restic/irv-ml1/profiles.yaml index de94561..0da0eac 100644 --- a/configs/restic/irv-ml1/profiles.yaml +++ b/configs/restic/irv-ml1/profiles.yaml @@ -33,7 +33,9 @@ global: min-memory: 100 default: - env-file: /etc/restic/restic.env # RESTIC_REPOSITORY=rest:http://user:pw@10.100.50.50:8000/irv-ml1/ + repository-file: /etc/restic/repository # URL incl. rest-server creds (root 0400). + # NOT env-file: `resticprofile schedule` copies env-file values into the + # world-readable systemd units (docs/runbooks/backups.md, Known gaps). env: RESTIC_PASSWORD_FILE: /etc/restic/password initialize: false # repo created manually by `restic init` diff --git a/configs/restic/nh3-dev/profiles.yaml b/configs/restic/nh3-dev/profiles.yaml index ced843a..c0039a5 100644 --- a/configs/restic/nh3-dev/profiles.yaml +++ b/configs/restic/nh3-dev/profiles.yaml @@ -26,7 +26,9 @@ global: min-memory: 200 # workstation has more RAM than agents; give restic a bit more default: - env-file: /etc/restic/restic.env + repository-file: /etc/restic/repository # URL incl. rest-server creds (root 0400). + # NOT env-file: `resticprofile schedule` copies env-file values into the + # world-readable systemd units (docs/runbooks/backups.md, Known gaps). env: RESTIC_PASSWORD_FILE: /etc/restic/password initialize: false diff --git a/configs/restic/nh3-docker/profiles.yaml b/configs/restic/nh3-docker/profiles.yaml index 198349c..83176ca 100644 --- a/configs/restic/nh3-docker/profiles.yaml +++ b/configs/restic/nh3-docker/profiles.yaml @@ -7,7 +7,7 @@ # # Writes to the Synology rest-server at 10.100.50.50:8000 as user # `nh3-docker`. The full REST URL (with HTTP basic-auth creds) lives in -# /etc/restic/restic.env. The client-side encryption passphrase lives in +# /etc/restic/repository (root 0400). The client-side encryption passphrase lives in # /etc/restic/password. # # No DB dumps needed. None of the stacks on this host (adguard, dockge, @@ -25,7 +25,9 @@ global: min-memory: 100 default: - env-file: /etc/restic/restic.env # RESTIC_REPOSITORY=rest:http://user:pw@… + repository-file: /etc/restic/repository # URL incl. rest-server creds (root 0400). + # NOT env-file: `resticprofile schedule` copies env-file values into the + # world-readable systemd units (docs/runbooks/backups.md, Known gaps). env: RESTIC_PASSWORD_FILE: /etc/restic/password initialize: false # repo created manually by `restic init` diff --git a/configs/restic/vm-esh-nas/migrate-repository-file.sh b/configs/restic/vm-esh-nas/migrate-repository-file.sh new file mode 100755 index 0000000..29bc806 --- /dev/null +++ b/configs/restic/vm-esh-nas/migrate-repository-file.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# vm-esh-nas: move restic from env-file to repository-file (2026-09-27). +# The same change playbooks/restic-repository-file.yaml made on the seven hosts +# where infra-ops has sudo. vm-esh-nas has no infra-ops account, so run it as: +# ssh -t vm-esh-nas 'sudo bash ~/restic-repofile-migrate.sh' +# It edits the env-file line IN PLACE (the live profile cannot be diffed without +# root, so any drift is preserved), and restores the old profile if a check fails. +set -eu +cd /etc/restic +grep -q '^ *env-file: /etc/restic/restic.env' profiles.yaml \ + || { echo "no env-file line: already migrated, or the profile differs; nothing done"; exit 1; } +val=$(sh -c 'set -a; . /etc/restic/restic.env; printf %s "$RESTIC_REPOSITORY"') +case "$val" in rest:http*) ;; *) echo "RESTIC_REPOSITORY is not a rest: URL; nothing done"; exit 1;; esac +umask 077 +printf '%s\n' "$val" > repository.new +chown root:root repository.new; chmod 0400 repository.new; mv repository.new repository +unset val +cp -p profiles.yaml profiles.yaml.bak-20260927-envfile +trap 'cp -p profiles.yaml.bak-20260927-envfile profiles.yaml; resticprofile --no-ansi --config /etc/restic/profiles.yaml --name default schedule >/dev/null 2>&1 || true; echo "FAILED: old profile restored"' ERR +sed -i 's|^\( *\)env-file: /etc/restic/restic.env.*$|\1repository-file: /etc/restic/repository # not env-file: schedule copies env-file values into world-readable units|' profiles.yaml +resticprofile --no-ansi --config /etc/restic/profiles.yaml --name default cat config >/dev/null +resticprofile --no-ansi --config /etc/restic/profiles.yaml --name default schedule >/dev/null +for u in backup check; do + f=/etc/systemd/system/resticprofile-$u@profile-default.service + test -f "$f" + ! grep -q 'rest:http' "$f" +done +systemctl is-active --quiet resticprofile-backup@profile-default.timer +trap - ERR +echo "vm-esh-nas migrated: units carry no repository URL, backup timer active" diff --git a/configs/restic/vm-esh-nas/profiles.yaml b/configs/restic/vm-esh-nas/profiles.yaml index 7e90555..83c7cb7 100644 --- a/configs/restic/vm-esh-nas/profiles.yaml +++ b/configs/restic/vm-esh-nas/profiles.yaml @@ -27,7 +27,7 @@ global: min-memory: 100 default: - env-file: /etc/restic/restic.env + repository-file: /etc/restic/repository # not env-file: schedule copies env-file values into world-readable units env: RESTIC_PASSWORD_FILE: /etc/restic/password initialize: false diff --git a/docs/runbooks/backups.md b/docs/runbooks/backups.md index ea6e3e6..eafa9ae 100644 --- a/docs/runbooks/backups.md +++ b/docs/runbooks/backups.md @@ -178,7 +178,7 @@ the stop→umount→rm-ghost→remount→start variant. ## Known gaps / TODO -### ⚠ `resticprofile schedule` publishes the repo credential (found 2026-09-27) +### ✅ `resticprofile schedule` published the repo credential (found and fixed 2026-09-27, except vm-esh-nas) On a host whose profile loads `RESTIC_REPOSITORY` from `env-file: /etc/restic/restic.env`, `resticprofile schedule` copies that value, **including @@ -191,11 +191,24 @@ a caller read the encrypted blobs and append to that one repo (rest-server is `--append-only`, and the encryption passphrase is a separate file). It does not decrypt anything. -**esh-ml1 does not have this problem:** its profile uses `repository-file: -/etc/restic/repository` (root 0400), so the unit carries only the path, and -`playbooks/esh-ml1-restic.yaml` verifies that no unit contains `rest:http`. -Moving the other hosts to `repository-file` is not done; it belongs with the -(operator-held) rest-server password rotation below. +**Fixed 2026-09-27 (Prime):** ana-docker, fv-ml1 (`configs/restic/ana-ml2`), +esh-docker-vm, esh-vm-db, irv-ml1, nh3-dev and nh3-docker were moved to +`repository-file: /etc/restic/repository` (root 0400) by +`playbooks/restic-repository-file.yaml`. esh-ml1 was built that way. The playbook's +verify step, plus an independent check across all eight hosts, found no unit +containing `rest:http`. nh3-docker's scheduled unit then ran a real backup (snapshot +`a29b889d`). All seven hosts' URLs and passphrases are now vaulted as +`/etc/restic/{repository,password}`. + +**Still open: vm-esh-nas.** infra-ops has no account there, so its unit still +leaks. The migration script is staged at `~lkraven/restic-repofile-migrate.sh` +(repo copy `configs/restic/vm-esh-nas/migrate-repository-file.sh`), and Prime runs +it with `ssh -t vm-esh-nas 'sudo bash ~/restic-repofile-migrate.sh'`. Its secrets are +not vaulted yet, because that needs root there. + +The passwords themselves were readable until the move, so the **rotation below is +still the real fix**. On rotation, update the vault, `restic.env` and `repository` +on each host. ### ✅ restic content assertion (2026-09-22, CLOSED) diff --git a/persistent-memory.md b/persistent-memory.md index cd2d506..dadd33c 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -142,6 +142,16 @@ _As of 2026-09-26 ~1620 PT._ commissioning misbehaves, the UniFi mDNS reflector on esh-iot is the first knob (a Prime/infra-ops change). → `servers/esh-matter/README.md` +### restic: credential leak fixed (2026-09-27, Prime) + +- Seven hosts were moved from `env-file` to `repository-file` (`playbooks/restic-repository-file.yaml`), + so the systemd units no longer carry the rest-server password. Secrets are vaulted as + `/etc/restic/{repository,password}`. `restic.env` is KEPT for manual snippets, so + **a rotation must update the vault, `restic.env` and `repository`.** +- **vm-esh-nas is still leaking**, because infra-ops has no account there. Prime runs + `ssh -t vm-esh-nas 'sudo bash ~/restic-repofile-migrate.sh'`. Its secrets are not vaulted. +- The passwords were readable until today, so the rotation (Prime's) remains the real fix. + ### augaman: face recognition for Cicada - **v0.1.2 LIVE on esh-ml1:8040** (2026-09-27; v0.1.1 first deployed 2026-09-26 2347 PT), diff --git a/playbooks/restic-repository-file.yaml b/playbooks/restic-repository-file.yaml new file mode 100644 index 0000000..0a13bc1 --- /dev/null +++ b/playbooks/restic-repository-file.yaml @@ -0,0 +1,95 @@ +# Move a restic client from `env-file: /etc/restic/restic.env` to +# `repository-file: /etc/restic/repository`. +# +# Why: `resticprofile schedule` copies env-file values into the generated +# systemd units, and those are world-readable (0644). So the RESTIC_REPOSITORY +# URL, rest-server password included, was readable by every local user on every +# env-file host (docs/runbooks/backups.md, Known gaps). With repository-file the +# unit carries only a path. +# +# Run, one host at a time: +# scripts/elway infra-ops@ --playbook playbooks/restic-repository-file.yaml \ +# --var cfg= --var expect_sha= +# +# expect_sha guards against clobbering drift: the upload only proceeds when the live +# profile is exactly the one the repo edit was made from, or already the new one. +# +# /etc/restic/restic.env is deliberately KEPT. The per-host READMEs and the freshness +# probe source it for manual restic commands. It is root 0600, so it is not the leak. +# On a password rotation, update BOTH files (restic.env and repository). + +vars: + cfg: "" + expect_sha: "" + +steps: + - name: Guard — live profile is the expected pre-change version (or already migrated) + sudo: true + shell: | + set -eu + test -n "{{ cfg }}" && test -n "{{ expect_sha }}" + live=$(sha256sum /etc/restic/profiles.yaml | cut -c1-12) + if [ "$live" = "{{ expect_sha }}" ]; then echo "live profile = expected pre-change $live"; exit 0; fi + if grep -q '^ *repository-file: /etc/restic/repository' /etc/restic/profiles.yaml; then echo "already migrated ($live)"; exit 0; fi + echo "DRIFT: live profile sha $live != expected {{ expect_sha }}; reconcile before migrating"; exit 1 + changed_when: "false" + + - name: Create /etc/restic/repository from restic.env (root 0400, value never printed) + sudo: true + shell: | + set -eu + val=$(sh -c 'set -a; . /etc/restic/restic.env; printf %s "$RESTIC_REPOSITORY"') + case "$val" in rest:http*) ;; *) echo "RESTIC_REPOSITORY in restic.env is not a rest: URL"; exit 1;; esac + umask 077 + printf '%s\n' "$val" > /etc/restic/repository.new + chown root:root /etc/restic/repository.new + chmod 0400 /etc/restic/repository.new + mv /etc/restic/repository.new /etc/restic/repository + creates: /etc/restic/repository + + - name: repository file matches restic.env (compared, not printed) + sudo: true + shell: | + set -eu + a=$(sh -c 'set -a; . /etc/restic/restic.env; printf %s "$RESTIC_REPOSITORY"') + b=$(head -n1 /etc/restic/repository) + [ "$a" = "$b" ] || { echo "repository file differs from restic.env"; exit 1; } + test "$(stat -c %U:%a /etc/restic/repository)" = root:400 + changed_when: "false" + + - name: Keep the pre-change profile beside the new one + sudo: true + shell: cp -p /etc/restic/profiles.yaml /etc/restic/profiles.yaml.bak-20260927-envfile + creates: /etc/restic/profiles.yaml.bak-20260927-envfile + + - name: Upload the repository-file profile + sudo: true + upload: + src: configs/restic/{{ cfg }}/profiles.yaml + dest: /etc/restic/profiles.yaml + mode: "0644" + + - name: The new profile reaches the repository (read-only `cat config`) + sudo: true + shell: resticprofile --no-ansi --config /etc/restic/profiles.yaml --name default cat config >/dev/null + changed_when: "false" + + - name: Regenerate the systemd units + sudo: true + shell: resticprofile --no-ansi --config /etc/restic/profiles.yaml --name default schedule + +verify: + # The units must EXIST before "no match" means anything. + - name: Units exist and carry no repository URL + shell: | + set -eu + d=/etc/systemd/system + for u in resticprofile-backup@profile-default.service resticprofile-check@profile-default.service; do + test -f "$d/$u" || { echo "missing unit $u"; exit 1; } + if grep -q 'rest:http' "$d/$u"; then echo "$u still embeds the repository URL"; exit 1; fi + done + changed_when: "false" + + - name: Backup and check timers are active + shell: systemctl is-active --quiet resticprofile-backup@profile-default.timer && systemctl is-active --quiet resticprofile-check@profile-default.timer + changed_when: "false" diff --git a/stacks/augaman/compose.yaml b/stacks/augaman/compose.yaml index 66ddc54..4cab61c 100644 --- a/stacks/augaman/compose.yaml +++ b/stacks/augaman/compose.yaml @@ -1,6 +1,5 @@ -# augaman: the fleet's face-recognition service for Cicada (gitea pfi/augaman). The primary -# instance is on esh-ml1 (CT 110 on esh-pve, RTX 2000E Ada 16 GB), with the gallery and the -# backup. A second, fixtures-only instance runs on fv-ml1 (GPU_ID=1, no backup). Enroll, +# augaman: the fleet's face-recognition service for Cicada (gitea pfi/augaman). It runs on +# esh-ml1 (CT 110 on esh-pve, RTX 2000E Ada 16 GB), with the gallery and the backup. Enroll, # recognize, verify; buffalo_l (SCRFD + ArcFace w600k_r50) on ONNX Runtime CUDA. Canonical copy: # this file in pfi/augaman; the eshpfi stack mirrors it as stacks/augaman. #