Playbook: MongoDB backup script dies silently — UTF-8 BOM in the deployed shell script
Status: ACTIVE (first written 2026-10-10, #7717 / partially implements #7609)
Host: bms-3 (51.68.155.224, rs0 SECONDARY) — canonical SSH user root (per
dev_r_server_capacity.ssh_user, the single source of truth; the ubuntu entry in some older
tables/worker-prompt copies is stale).
Script: /root/mongodb-backup.sh (repo source of truth: scripts/mongodb-backup.sh).
Symptom
s3://p24-infra/mongodb/full/ stays empty and /var/log/mongodb-backup.log repeats exactly:
/root/mongodb-backup.sh: 1: <BOM>#!/usr/bin/env: not found
/root/mongodb-backup.sh: 17: set: Illegal option -o pipefail
The cron fires daily but the script dies on line 1, so its own Discord/GH error-notification never runs → silent failure (117 days undetected in #7717). RPO silently frozen at the last good backup.
Root cause
The deployed file begins with a UTF-8 BOM (EF BB BF) before the shebang. The kernel does not
honour a shebang that isn’t at byte 0, so the script runs under /bin/sh (dash), which fails on
set -o pipefail (and prints the BOM as a mojibake prefix). Cause of the BOM: the file was edited/
saved on a Windows editor. Confirm with head -c 16 /root/mongodb-backup.sh | xxd → leading efbbbf.
Fix (live, surgical — do ONLY this on the live file)
F=/root/mongodb-backup.sh
cp -p "$F" "${F}.bak-$(date -u +%F)" # back up first
sed -i '1s/^\xEF\xBB\xBF//' "$F" # strip ONLY the leading BOM on line 1
# verify:
head -c 3 "$F" | xxd # expect 23 21 2f (#!/)
head -1 "$F" # expect #!/usr/bin/env bash
bash -n "$F" && echo ok # syntax clean
cmp <(tail -c +4 "${F}.bak-"*) "$F" # expect identical — nothing but 3 BOM bytes changedBefore RE-RUNNING the backup — pre-flight gates (bms-3)
- bms-3 must be SECONDARY —
mongosh --quiet --eval 'db.hello()'(unauthenticated handshake):isWritablePrimary=false. If it is PRIMARY, abort. - Other rs0 members
health=1and secondary replication lag < 10 s —rs.status()(needs auth, see credential note below). /tmpfilesystem ≥ 80 GB free; nomongodumpalready running; load1 < 6.- Run low-priority so it can’t starve the replica:
nice -n 19 ionice -c3 /root/mongodb-backup.sh full(thefullarg is ignored by the script). Monitor every ~2 min: lag < 60 s, load < 12,/tmp> 40 GB; on breach SIGTERM the dump, clean/tmp/mongodb-full-*+/tmp/full-*.tar.gz, report, do not blind-retry.
⚠️ Gotcha: /root/.backup-env can be STALE (separate failure from the BOM)
/root/.backup-env is a server-local plaintext file (NOT managed by secrets-sync). The script
authenticates mongodump with its MONGODB_ADMIN_USER/MONGODB_ADMIN_PASSWORD and uploads with its
WASABI_ACCESS_KEY/WASABI_SECRET_KEY. When the rs0 admin password (or a Wasabi key) is rotated,
.backup-env is not updated automatically → mongodump fails Authentication failed even after
the BOM is fixed. This is exactly what blocked #7717 (.backup-env admin pass 18 chars vs current
SOPS MONGODB_RS0_ADMIN_PASSWORD 32 chars).
Diagnose WITHOUT printing values (lengths/fingerprints only):
# length in .backup-env (run on bms-3):
ssh root@51.68.155.224 '( set -a; . /root/.backup-env; set +a; echo "pass_len=${#MONGODB_ADMIN_PASSWORD}" )'
# current length in SOPS (on a host with the age key):
sops -d --input-type dotenv --output-type dotenv secrets/mongodb-bms.env.sops \
| grep '^MONGODB_RS0_ADMIN_PASSWORD=' | cut -d= -f2- | wc -cRefreshing .backup-env is a credential-handling action — route it through secret-manager /
explicit human approval (it is NOT a SOPS write and NOT a new rotation; the current value already
lives in secrets/mongodb-bms.env.sops → it is a one-line sync of an existing secret). An
infra-task worker whose issue says “do not touch /root/.backup-env” must escalate (human-action),
not override.
Permanent prevention (tracked on #7609)
- Deploy the backup script from the repo copy with a BOM check at deploy time (or Ansible-managed),
not hand-copied.
scripts/tests/test_no_bom_in_shell_scripts.pyalready guards the repo copy — the drift is purely deploy-side. - Reconcile cron vs repo: live cron is daily 01:30 with an ignored
fullarg; the repo header says weekly Sunday 01:00. Make repo + cron + Ansible + retention agree. - Add a Prometheus alert on the age of the newest
s3://p24-infra/mongodb/full/object so a silent failure can’t exceed ~25 h, and fix whybackup-exportermissed 117 days. - Add
P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URLto.backup-env(currently absent → the script’s ownnotify_discord_erroris a no-op even when the script runs).
Related
docs/playbooks/sops-windows-crlf.md— BOM/CRLF corruption in SOPS files (same byte-level class).docs/playbooks/mongodb-restore-drill-troubleshooting.md,docs/playbooks/mongodb-rs0-full-restore.md.docs/playbooks/server-ssh-connectivity.md— bms-3 canonical user =root(dev_r_server_capacity).