110 lines
3.9 KiB
Bash
Executable File
110 lines
3.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# Copyright 2026 Umbra Tools (umbratools.com)
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# Vaultwarden daily backup to the NetBackup SMB share.
|
|
#
|
|
# Run daily from bdm's crontab at 03:00. Logging goes to
|
|
# /var/log/vaultwarden-backup.log, and a failure is also reported to the
|
|
# journal so it is visible without reading the log file.
|
|
#
|
|
# The privileged steps are four root-owned helpers in /usr/local/sbin, each
|
|
# granted to bdm NOPASSWD in /etc/sudoers.d/vaultwarden-backup. Do not replace
|
|
# those calls with inline sudo: a sudoers rule that names arguments matches only
|
|
# that exact argument list, so anything that varies the arguments falls through
|
|
# to a password-required rule and fails from cron, where there is no terminal.
|
|
#
|
|
# Source of truth for the helpers is scripts/vaultwarden-backup-helpers/.
|
|
|
|
set -uo pipefail
|
|
|
|
BACKUP_DIR="/tmp/vaultwarden-backup"
|
|
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
|
|
BACKUP_FILE="${BACKUP_DIR}/vaultwarden-${TIMESTAMP}.db"
|
|
SMB_HOST="192.168.123.2"
|
|
SMB_SHARE="NetBackup"
|
|
SMB_DIR="Vault"
|
|
BACKUP_NAME="vaultwarden-${TIMESTAMP}.db"
|
|
|
|
# SMB credentials live in an external file (mode 600) rather than inline here,
|
|
# so the password never lands in version control or in the process list.
|
|
SMB_CRED_FILE="${SMB_CRED_FILE:-$HOME/.smbcredentials}"
|
|
|
|
# Report to the journal as well as stdout, so a failure is not invisible.
|
|
log() {
|
|
echo "$(date): $*"
|
|
logger -t vaultwarden-backup -- "$*"
|
|
}
|
|
|
|
die() {
|
|
log "FATAL - $*"
|
|
exit 1
|
|
}
|
|
|
|
if [ ! -r "$SMB_CRED_FILE" ]; then
|
|
die "SMB credentials file not readable: ${SMB_CRED_FILE}"
|
|
fi
|
|
|
|
mkdir -p "$BACKUP_DIR" || die "could not create ${BACKUP_DIR}"
|
|
|
|
log "Starting vaultwarden backup..."
|
|
|
|
# Consistent snapshot. The database runs in WAL mode with an uncheckpointed
|
|
# WAL, so this must be sqlite3 .backup and not a file copy. The helper also
|
|
# chowns the result back to this user.
|
|
if ! sudo -n /usr/local/sbin/vw-backup-db "$BACKUP_FILE"; then
|
|
die "backup step failed"
|
|
fi
|
|
|
|
if [ ! -s "$BACKUP_FILE" ]; then
|
|
die "backup file missing or empty: ${BACKUP_FILE}"
|
|
fi
|
|
|
|
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
|
log "Backup created (${SIZE}), uploading to SMB..."
|
|
|
|
# The backup is worthless if it cannot be read back, so verify it before
|
|
# spending bandwidth and before deleting anything.
|
|
INTEGRITY=$(sqlite3 "$BACKUP_FILE" "PRAGMA integrity_check;" 2>&1)
|
|
if [ "$INTEGRITY" != "ok" ]; then
|
|
log "ERROR - backup failed integrity check: ${INTEGRITY}"
|
|
rm -f "$BACKUP_FILE"
|
|
die "refusing to upload a backup that does not verify"
|
|
fi
|
|
log "Backup passed integrity check."
|
|
|
|
# Capture the output and the exit status separately. Piping smbclient into
|
|
# "grep -q" would not work: grep exits at the first match, smbclient takes
|
|
# SIGPIPE, and the pipeline status would describe grep rather than the upload.
|
|
upload_out=$(smbclient "//${SMB_HOST}/${SMB_SHARE}" -A "$SMB_CRED_FILE" \
|
|
-c "cd ${SMB_DIR}; put ${BACKUP_FILE} ${BACKUP_NAME}" 2>&1)
|
|
upload_rc=$?
|
|
printf '%s\n' "$upload_out"
|
|
|
|
if [ "$upload_rc" -ne 0 ] || ! printf '%s' "$upload_out" | grep -q 'putting file'; then
|
|
rm -f "$BACKUP_FILE"
|
|
die "SMB upload failed (smbclient exit ${upload_rc})"
|
|
fi
|
|
|
|
rm -f "$BACKUP_FILE"
|
|
log "Backup uploaded successfully to //${SMB_HOST}/${SMB_SHARE}/${SMB_DIR}/${BACKUP_NAME}"
|
|
|
|
# Retention pruning. This is best-effort: a failure here must not be reported
|
|
# as a failed backup, because the backup itself already succeeded. The helpers
|
|
# fail loudly if the share is not mounted, which is the case worth knowing
|
|
# about, so the warning is logged rather than swallowed.
|
|
log "Pruning backups older than 60 days..."
|
|
if sudo -n /usr/local/sbin/vw-mount-smb; then
|
|
if sudo -n /usr/local/sbin/vw-cleanup-old; then
|
|
log "Retention prune complete."
|
|
else
|
|
log "WARNING - pruning failed; retention may not be enforced."
|
|
fi
|
|
sudo -n /usr/local/sbin/vw-umount-smb || log "WARNING - could not unmount ${BACKUP_DIR}"
|
|
else
|
|
log "WARNING - could not mount SMB share, skipping retention prune."
|
|
fi
|
|
|
|
log "Backup finished successfully."
|
|
exit 0
|