Files
bdm dd26e7c751 Fix the vaultwarden backup failing silently for nine days
Every nightly run had been failing since 20 Sep, writing the reason only to
a log file nobody reads. The last backup on the share was from 19 Sep.

Root cause: a sudoers rule that names arguments matches only that exact
argument list. The rule allowed

    sudo /usr/bin/sqlite3 /var/.../db.sqlite3

but the script ran

    sudo sqlite3 /var/.../db.sqlite3 ".backup '/tmp/.../db'"

The second argument was not in the rule, so it did not match, sudo fell
through to the password-required bdm ALL=(ALL) ALL, and cron has no
terminal to prompt on. Reproduced with -n, so neither call could prompt:

    sudo -n sqlite3 <db>            # 1 arg, matches  -> exit 0
    sudo -n sqlite3 <db> "SELECT 1" # 2 args, no match -> password required

Two further problems were hidden behind that early exit:

The cleanup phase called sudo mount/umount/find inline, but sudoers only
whitelisted helper scripts that existed and were never invoked. The 60-day
retention was dead too. The old cleanup also could not distinguish "share
not mounted" from "nothing to delete": find matched nothing and the script
exited 0 reporting a healthy run.

The helpers that *were* whitelisted lived under /home/bdm and are owned by
bdm, so the grant was equivalent to unrestricted root.

Changes:

Move each privileged step into a root-owned helper in /usr/local/sbin,
deployed from the repo as source of truth. This avoids depending on fragile
argument globs and lets the rule constrain the one argument that varies.
vw-backup-db re-validates the destination path itself rather than trusting
the sudoers pattern.

Call the helpers from the script and drop the inline sudo. Verify the
snapshot with PRAGMA integrity_check before uploading, and abort rather
than shipping a copy that does not verify. Check smbclient's exit status
directly instead of piping into grep, which would have let SIGPIPE mask a
failed upload.

Add a systemd staleness watchdog so this cannot hide again. It mounts the
share read-only and separates STALE (nothing recent) from CANNOT VERIFY
(could not mount), which mean different things.

Also chmod 0440 /etc/sudoers.d/bdm, which visudo was warning about.

Tested with a cold sudo cache, since timestamp_type=global means an
interactive sudo -v otherwise masks the failure. A real cron invocation
succeeded end to end: snapshot, integrity check, upload, and prune.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-28 11:11:42 -07:00

64 lines
1.9 KiB
Bash
Executable File

#!/bin/bash
# Create a consistent SQLite backup of the vaultwarden database.
#
# Run as root via a NOPASSWD sudoers rule, because the database lives in a
# root-owned Docker volume. The destination is constrained to the backup
# directory so this grant cannot be used to write a file anywhere else.
#
# Install to /usr/local/sbin/vw-backup-db owned by root, mode 755. The copy
# under scripts/ is the source of truth; do not sudo this one directly, or the
# grant becomes a way to run arbitrary code as root.
set -euo pipefail
DB="/var/lib/docker/volumes/caddy_vaultwarden_data/_data/db.sqlite3"
BACKUP_DIR="/tmp/vaultwarden-backup"
OUT="${1:-}"
if [ -z "$OUT" ]; then
echo "vw-backup-db: no destination given" >&2
exit 1
fi
# Only ever write a backup file into the backup directory. This is the
# constraint that makes the sudoers grant safe, so it is checked here too and
# not just in sudoers.
case "$OUT" in
"${BACKUP_DIR}"/vaultwarden-*.db) ;;
*)
echo "vw-backup-db: refusing to write outside ${BACKUP_DIR}: ${OUT}" >&2
exit 1
;;
esac
if [ ! -f "$DB" ]; then
echo "vw-backup-db: database not found at ${DB}" >&2
exit 1
fi
mkdir -p "$BACKUP_DIR"
# Remove a stale file so a failed backup cannot be mistaken for a good one.
rm -f "$OUT"
# .backup takes a read lock and produces a consistent snapshot. The database
# runs in WAL mode with an uncheckpointed WAL, so copying the file with cp
# would capture stale data; .backup is required.
if ! /usr/bin/sqlite3 "$DB" ".backup '${OUT}'"; then
echo "vw-backup-db: sqlite3 .backup failed" >&2
rm -f "$OUT"
exit 1
fi
if [ ! -s "$OUT" ]; then
echo "vw-backup-db: backup file is missing or empty" >&2
rm -f "$OUT"
exit 1
fi
# Hand the file back to the calling user so it can be uploaded without sudo.
if [ -n "${SUDO_UID:-}" ] && [ -n "${SUDO_GID:-}" ]; then
chown "${SUDO_UID}:${SUDO_GID}" "$OUT"
fi
echo "vw-backup-db: wrote ${OUT} ($(stat -c %s "$OUT") bytes)"