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 <host>/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.
96 lines
4.1 KiB
YAML
96 lines
4.1 KiB
YAML
# 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@<ip> --playbook playbooks/restic-repository-file.yaml \
|
|
# --var cfg=<configs/restic dir> --var expect_sha=<first 12 hex of the LIVE profile's sha256>
|
|
#
|
|
# 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"
|