# 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"