From d27e93d220f3537ccf996211e535e25f0afadf0e Mon Sep 17 00:00:00 2001 From: bdm Date: Mon, 28 Sep 2026 06:24:25 -0700 Subject: [PATCH] Track vaultwarden backup and restore scripts Daily SQLite backup to the NetBackup SMB share plus restore/import/export helpers. SMB credentials moved out of the scripts into ~/.smbcredentials (mode 600) and are passed to smbclient via -A instead of -U "user%pass". This keeps the password out of version control and out of the process list, where -U exposes it to any local user through ps. --- .gitignore | 21 +++ vaultwarden-backup-helpers/README.md | 145 ++++++++++++++++++ .../cleanup-old-backups.sh | 3 + vaultwarden-backup-helpers/export-to-json.sh | 74 +++++++++ .../import-from-json.sh | 60 ++++++++ vaultwarden-backup-helpers/mount-smb.sh | 7 + vaultwarden-backup-helpers/restore-sqlite.sh | 75 +++++++++ vaultwarden-backup-helpers/umount-smb.sh | 3 + vaultwarden-backup.sh | 65 ++++++++ 9 files changed, 453 insertions(+) create mode 100644 .gitignore create mode 100644 vaultwarden-backup-helpers/README.md create mode 100755 vaultwarden-backup-helpers/cleanup-old-backups.sh create mode 100755 vaultwarden-backup-helpers/export-to-json.sh create mode 100755 vaultwarden-backup-helpers/import-from-json.sh create mode 100755 vaultwarden-backup-helpers/mount-smb.sh create mode 100755 vaultwarden-backup-helpers/restore-sqlite.sh create mode 100755 vaultwarden-backup-helpers/umount-smb.sh create mode 100755 vaultwarden-backup.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec2b4bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# OS / editor cruft +.DS_Store +Thumbs.db +*.swp +*~ + +# SMB credentials - NEVER commit these. +# Real file lives at ~/.smbcredentials (mode 600). This guards against anyone +# dropping a copy inside the repo. +.smbcredentials +*.smbcredentials +smb.credentials +*.env +.env.* + +# Backup output (the script uses /tmp, but guard local drops) +*.db +*.db-journal + +# Logs +*.log diff --git a/vaultwarden-backup-helpers/README.md b/vaultwarden-backup-helpers/README.md new file mode 100644 index 0000000..660010c --- /dev/null +++ b/vaultwarden-backup-helpers/README.md @@ -0,0 +1,145 @@ +# Vaultwarden Backup & Restore System + +This directory contains scripts for backing up, restoring, and converting Vaultwarden data. + +## Overview + +### Primary Backup: SQLite Database (Automated Daily) +The main backup runs daily at 3:00 AM via cron (`/home/bdm/scripts/vaultwarden-backup.sh`). It: +1. Creates a consistent SQLite backup using `sqlite3 .backup` +2. Uploads to SMB share (`//192.168.123.2/NetBackup/Vault/`) +3. Cleans up backups older than 60 days + +**This is the complete backup for disaster recovery.** It contains ALL users, their encrypted vaults, organizations, collections, sends, etc. + +### Restore Options + +#### Option 1: Full Instance Restore (Recommended for Disaster Recovery) +Use `restore-sqlite.sh` to restore the entire Vaultwarden instance: +```bash +./restore-sqlite.sh vaultwarden-2026-09-19_09-03-45.db +``` +This replaces the database file and restarts Vaultwarden. All users log in with their existing master passwords. + +#### Option 2: Individual User Vault Export/Import (JSON) +Use `export-to-json.sh` and `import-from-json.sh` for individual user vaults: +```bash +# Export (requires master password) +./export-to-json.sh user@example.com my-vault.json + +# Import (requires master password) +./import-from-json.sh user@example.com my-vault.json +``` + +**Note:** JSON export requires the master password because the SQLite database stores all data ENCRYPTED. The master password is needed to decrypt the vault for JSON export. + +## Scripts + +| Script | Purpose | Requires Master Password | +|--------|---------|-------------------------| +| `vaultwarden-backup.sh` | Daily automated SQLite backup (run by cron) | No | +| `restore-sqlite.sh` | Restore entire instance from SQLite backup | No | +| `export-to-json.sh` | Export single user vault to JSON | **Yes** | +| `import-from-json.sh` | Import JSON vault to user account | **Yes** | +| `mount-smb.sh` | Mount SMB share for cleanup (internal) | No | +| `umount-smb.sh` | Unmount SMB share (internal) | No | +| `cleanup-old-backups.sh` | Delete backups older than 60 days (internal) | No | + +## Backup Location + +- **SMB Share:** `//192.168.123.2/NetBackup/Vault/` +- **Local Temp:** `/tmp/vaultwarden-backup/` (cleaned up after upload) +- **Log File:** `/var/log/vaultwarden-backup.log` + +## Cron Schedule + +Daily at 3:00 AM (see `crontab -l`): +``` +0 3 * * * /home/bdm/scripts/vaultwarden-backup.sh >> /var/log/vaultwarden-backup.log 2>&1 +``` + +## Manual Backup + +To run a manual backup: +```bash +/home/bdm/scripts/vaultwarden-backup.sh +``` + +## Sudo Configuration + +The backup script uses sudo for specific commands. Sudoers rules are in `/etc/sudoers.d/vaultwarden-backup`: +- `sqlite3` backup command +- `chown` for temp files +- SMB mount/umount/cleanup wrapper scripts + +## Disaster Recovery Procedure + +1. **New server setup:** + - Install Docker, Docker Compose + - Deploy Vaultwarden container with same volume name (`caddy_vaultwarden_data`) + - Start container once to create empty database + +2. **Restore database:** + ```bash + # Download backup from SMB (credentials read from the file below) + smbclient //192.168.123.2/NetBackup -A "$HOME/.smbcredentials" -c "cd Vault; get vaultwarden-YYYY-MM-DD_HH-MM-SS.db" + + # Restore + ./restore-sqlite.sh vaultwarden-YYYY-MM-DD_HH-MM-SS.db + ``` + +3. **Verify:** Users can log in with their existing credentials + +## JSON Import/Export Notes + +- JSON files contain **DECRYPTED** vault data - store securely! +- Use `BW_MASTER_PASSWORD` env var for automation (less secure) +- Interactive prompt is more secure +- JSON format is Bitwarden-compatible (select "Bitwarden (json)" on import) +- Import does not overwrite existing items - it adds/updates + +## Troubleshooting + +### Backup fails with "sqlite3 backup failed" +- Check if container is running: `docker ps | grep vaultwarden` +- Check volume path: `ls -la /var/lib/docker/volumes/caddy_vaultwarden_data/_data/` +- Run manually to see errors: `/home/bdm/scripts/vaultwarden-backup.sh` + +### SMB upload fails +- Check network connectivity: `ping 192.168.123.2` +- Check the SMB credentials file is present and readable: `ls -l ~/.smbcredentials` +- Test manually: `smbclient //192.168.123.2/NetBackup -A "$HOME/.smbcredentials" -c "ls"` + +### Cron doesn't run +- Check cron logs: `journalctl -u cron -f` +- Verify crontab: `crontab -l` +- Check log file: `cat /var/log/vaultwarden-backup.log` + +## SMB Credentials + +The scripts do **not** contain the SMB password. It lives in an external file so +that it stays out of version control and out of the process list: + +- **Location:** `~/.smbcredentials` (override with the `SMB_CRED_FILE` env var) +- **Permissions:** `600`, owned by the user running the backup (`bdm`) + +Create it once, with mode 600: + +```bash +umask 077 +printf 'username = %s\npassword = %s\n' 'bdm' 'YOUR_PASSWORD' > ~/.smbcredentials +``` + +The scripts pass it to `smbclient` with `-A` (authentication file), which is +preferable to `-U "user%pass"` because a password passed via `-U` is visible to +any local user through `ps`. + +Note that `/root/.smbcredentials` is a *separate* file used only by the `sudo +mount -t cifs` calls, which run as root. The two are independent. + +## Security Notes + +- SMB credentials are kept in `~/.smbcredentials` (mode 600), not in the scripts +- JSON exports contain plaintext passwords - encrypt them if storing long-term +- SQLite backups contain encrypted data - safe to store but treat as sensitive +- Master passwords are never stored - only used interactively or via env var \ No newline at end of file diff --git a/vaultwarden-backup-helpers/cleanup-old-backups.sh b/vaultwarden-backup-helpers/cleanup-old-backups.sh new file mode 100755 index 0000000..e23c729 --- /dev/null +++ b/vaultwarden-backup-helpers/cleanup-old-backups.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Clean up old vaultwarden backups from SMB share +find "/mnt/netbackup/Vault" -name "vaultwarden-*.db" -mtime +60 -type f -print -delete 2>/dev/null | wc -l \ No newline at end of file diff --git a/vaultwarden-backup-helpers/export-to-json.sh b/vaultwarden-backup-helpers/export-to-json.sh new file mode 100755 index 0000000..f9618f6 --- /dev/null +++ b/vaultwarden-backup-helpers/export-to-json.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Vaultwarden JSON Export Script +# This script exports a user's vault to JSON format using the Bitwarden CLI. +# +# IMPORTANT: This requires the user's MASTER PASSWORD to decrypt the vault data. +# The SQLite database stores data ENCRYPTED - it cannot be converted to JSON without +# the master password. +# +# The JSON export is useful for: +# - Migrating to a different password manager +# - Importing into a different Vaultwarden instance as a different user +# - Human-readable backup (store securely!) +# +# For full instance disaster recovery, use restore-sqlite.sh instead. +# +# Usage: ./export-to-json.sh [output-file.json] +# Example: ./export-to-json.sh user@example.com my-vault-export.json +# +# The master password can be provided via: +# - BW_MASTER_PASSWORD environment variable +# - Interactive prompt (recommended for security) + +set -euo pipefail + +EMAIL="${1:-}" +OUTPUT_FILE="${2:-vaultwarden-export-$(date +%Y-%m-%d).json}" +VAULTWARDEN_URL="https://vault.bdmurphy.me" + +if [ -z "$EMAIL" ]; then + echo "Usage: $0 [output-file.json]" + exit 1 +fi + +# Check if bw CLI is available +if ! command -v bw &> /dev/null; then + echo "ERROR: Bitwarden CLI (bw) not found. Install with: npm install -g @bitwarden/cli" + exit 1 +fi + +# Configure server URL +bw config server "$VAULTWARDEN_URL" >/dev/null + +# Login +echo "Logging in to Vaultwarden as $EMAIL..." + +if [ -n "${BW_MASTER_PASSWORD:-}" ]; then + # Use environment variable + export BW_SESSION=$(bw login "$EMAIL" "$BW_MASTER_PASSWORD" --raw) +else + # Interactive login + export BW_SESSION=$(bw login "$EMAIL" --raw) +fi + +if [ -z "$BW_SESSION" ] || [ "$BW_SESSION" = "null" ]; then + echo "ERROR: Login failed. Check credentials." + exit 1 +fi + +echo "Login successful. Exporting vault..." + +# Export vault to JSON +bw export --format json --output "$OUTPUT_FILE" --session "$BW_SESSION" + +# Lock the vault +bw lock --session "$BW_SESSION" >/dev/null + +echo "Export complete: $OUTPUT_FILE" +echo "" +echo "IMPORTANT: This file contains DECRYPTED vault data. Store it securely!" +echo "To import into another Vaultwarden instance:" +echo " 1. Log in to the target Vaultwarden web vault" +echo " 2. Go to Tools > Import Data" +echo " 3. Select 'Bitwarden (json)' as format" +echo " 4. Upload this file" \ No newline at end of file diff --git a/vaultwarden-backup-helpers/import-from-json.sh b/vaultwarden-backup-helpers/import-from-json.sh new file mode 100755 index 0000000..3adc6f7 --- /dev/null +++ b/vaultwarden-backup-helpers/import-from-json.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Vaultwarden JSON Import Script +# This script imports a JSON export file into Vaultwarden using the Bitwarden CLI. +# +# Usage: ./import-from-json.sh +# Example: ./import-from-json.sh user@example.com vaultwarden-export-2026-09-19.json +# +# The master password can be provided via: +# - BW_MASTER_PASSWORD environment variable +# - Interactive prompt + +set -euo pipefail + +EMAIL="${1:-}" +INPUT_FILE="${2:-}" +VAULTWARDEN_URL="https://vault.bdmurphy.me" + +if [ -z "$EMAIL" ] || [ -z "$INPUT_FILE" ]; then + echo "Usage: $0 " + exit 1 +fi + +if [ ! -f "$INPUT_FILE" ]; then + echo "ERROR: Input file not found: $INPUT_FILE" + exit 1 +fi + +# Check if bw CLI is available +if ! command -v bw &> /dev/null; then + echo "ERROR: Bitwarden CLI (bw) not found. Install with: npm install -g @bitwarden/cli" + exit 1 +fi + +# Configure server URL +bw config server "$VAULTWARDEN_URL" >/dev/null + +# Login +echo "Logging in to Vaultwarden as $EMAIL..." + +if [ -n "${BW_MASTER_PASSWORD:-}" ]; then + export BW_SESSION=$(bw login "$EMAIL" "$BW_MASTER_PASSWORD" --raw) +else + export BW_SESSION=$(bw login "$EMAIL" --raw) +fi + +if [ -z "$BW_SESSION" ] || [ "$BW_SESSION" = "null" ]; then + echo "ERROR: Login failed. Check credentials." + exit 1 +fi + +echo "Login successful. Importing vault from $INPUT_FILE..." + +# Import vault from JSON +bw import --format json --input "$INPUT_FILE" --session "$BW_SESSION" + +# Lock the vault +bw lock --session "$BW_SESSION" >/dev/null + +echo "Import complete!" +echo "Note: You may need to sync the vault in the web/browser extension." \ No newline at end of file diff --git a/vaultwarden-backup-helpers/mount-smb.sh b/vaultwarden-backup-helpers/mount-smb.sh new file mode 100755 index 0000000..594b4fc --- /dev/null +++ b/vaultwarden-backup-helpers/mount-smb.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Mount SMB share for vaultwarden backup cleanup +mkdir -p /mnt/netbackup +if ! mountpoint -q /mnt/netbackup; then + mount -t cifs "//192.168.123.2/NetBackup" /mnt/netbackup \ + -o credentials=/root/.smbcredentials,iocharset=utf8,vers=3.0,uid=1000,gid=1000,file_mode=0666,dir_mode=0777 2>/dev/null +fi \ No newline at end of file diff --git a/vaultwarden-backup-helpers/restore-sqlite.sh b/vaultwarden-backup-helpers/restore-sqlite.sh new file mode 100755 index 0000000..a95d829 --- /dev/null +++ b/vaultwarden-backup-helpers/restore-sqlite.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# Vaultwarden SQLite Database Restore Script +# This script restores a vaultwarden SQLite database backup to the running container. +# +# IMPORTANT: The SQLite backup is the PRIMARY backup for full instance recovery. +# It contains ALL users, their encrypted vault data, organizations, collections, etc. +# Simply replacing the database file and restarting Vaultwarden will restore everything. +# +# Usage: ./restore-sqlite.sh +# Example: ./restore-sqlite.sh vaultwarden-2026-09-19_09-03-45.db + +set -euo pipefail + +BACKUP_FILE="${1:-}" +VAULT_DATA="/var/lib/docker/volumes/caddy_vaultwarden_data/_data" +CONTAINER_NAME="vaultwarden" +SMB_SHARE="//192.168.123.2/NetBackup" + +# SMB credentials live in an external file (mode 600), not in this script, so the +# password stays out of version control and out of the process list. +SMB_CRED_FILE="${SMB_CRED_FILE:-$HOME/.smbcredentials}" + +if [ ! -r "$SMB_CRED_FILE" ]; then + echo "ERROR: SMB credentials file not readable: ${SMB_CRED_FILE}" + echo "Create it with: printf 'username = %s\\npassword = %s\\n' | install -m 600 /dev/stdin ${SMB_CRED_FILE}" + exit 1 +fi + +if [ -z "$BACKUP_FILE" ]; then + echo "Usage: $0 " + echo "Available backups in SMB share:" + smbclient "$SMB_SHARE" -A "$SMB_CRED_FILE" -c "cd Vault; ls vaultwarden-*.db" 2>/dev/null | grep "vaultwarden-" + exit 1 +fi + +if [ ! -f "$BACKUP_FILE" ]; then + # Try to download from SMB + echo "Backup file not found locally. Downloading from SMB..." + smbclient "$SMB_SHARE" -A "$SMB_CRED_FILE" -c "cd Vault; get $BACKUP_FILE $BACKUP_FILE" 2>/dev/null + if [ ! -f "$BACKUP_FILE" ]; then + echo "ERROR: Could not find or download backup file: $BACKUP_FILE" + exit 1 + fi +fi + +echo "Restoring Vaultwarden database from: $BACKUP_FILE" +echo "This will replace the current database. All users will need to log in again." +read -p "Are you sure? (yes/no): " CONFIRM +if [ "$CONFIRM" != "yes" ]; then + echo "Aborted." + exit 1 +fi + +# Stop the container +echo "Stopping Vaultwarden container..." +docker stop "$CONTAINER_NAME" + +# Backup current database (just in case) +CURRENT_DB="${VAULT_DATA}/db.sqlite3" +if [ -f "$CURRENT_DB" ]; then + sudo cp "$CURRENT_DB" "${CURRENT_DB}.pre-restore-$(date +%Y%m%d-%H%M%S)" + echo "Current database backed up." +fi + +# Restore the backup +echo "Restoring database..." +sudo cp "$BACKUP_FILE" "$CURRENT_DB" +sudo chown 1000:1000 "$CURRENT_DB" # Adjust if different user runs container + +# Start the container +echo "Starting Vaultwarden container..." +docker start "$CONTAINER_NAME" + +echo "Restore complete! Vaultwarden should be available shortly." +echo "Users can log in with their existing master passwords." \ No newline at end of file diff --git a/vaultwarden-backup-helpers/umount-smb.sh b/vaultwarden-backup-helpers/umount-smb.sh new file mode 100755 index 0000000..53ad5a5 --- /dev/null +++ b/vaultwarden-backup-helpers/umount-smb.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Unmount SMB share for vaultwarden backup cleanup +umount /mnt/netbackup 2>/dev/null \ No newline at end of file diff --git a/vaultwarden-backup.sh b/vaultwarden-backup.sh new file mode 100755 index 0000000..071341e --- /dev/null +++ b/vaultwarden-backup.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Vaultwarden daily backup to SMB share via smbclient + +VAULT_DATA="/var/lib/docker/volumes/caddy_vaultwarden_data/_data" +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" +KEEP_DAYS=60 + +# SMB credentials are stored in an external file (mode 600) rather than inline +# here, so the password never lands in version control or in the process list. +# See vaultwarden-backup-helpers/README.md for setup. +SMB_CRED_FILE="${SMB_CRED_FILE:-$HOME/.smbcredentials}" + +if [ ! -r "$SMB_CRED_FILE" ]; then + echo "$(date): ERROR - SMB credentials file not readable: ${SMB_CRED_FILE}" + echo "$(date): Create it with: printf 'username = %s\\npassword = %s\\n' | install -m 600 /dev/stdin ${SMB_CRED_FILE}" + exit 1 +fi + +mkdir -p "$BACKUP_DIR" + +echo "$(date): Starting vaultwarden backup..." +sudo sqlite3 "${VAULT_DATA}/db.sqlite3" ".backup '${BACKUP_FILE}'" +sudo chown "$(id -u):$(id -g)" "$BACKUP_FILE" + +if [ -f "$BACKUP_FILE" ] && [ -s "$BACKUP_FILE" ]; then + SIZE=$(du -h "$BACKUP_FILE" | cut -f1) + echo "$(date): Backup created ($SIZE), uploading to SMB..." + + smbclient "//${SMB_HOST}/${SMB_SHARE}" -A "$SMB_CRED_FILE" \ + -c "cd ${SMB_DIR}; put ${BACKUP_FILE} vaultwarden-${TIMESTAMP}.db" + + if [ $? -eq 0 ]; then + echo "$(date): Backup finished successfully." + else + echo "$(date): ERROR - SMB upload failed!" + exit 1 + fi + + rm -f "$BACKUP_FILE" +else + echo "$(date): ERROR - sqlite3 backup failed or file is empty!" + rm -f "$BACKUP_FILE" + exit 1 +fi + +# Clean up old backups (older than KEEP_DAYS) +echo "$(date): Cleaning up backups older than ${KEEP_DAYS} days..." +sudo mkdir -p /mnt/netbackup +if ! mountpoint -q /mnt/netbackup; then + sudo mount -t cifs "//${SMB_HOST}/${SMB_SHARE}" /mnt/netbackup \ + -o credentials=/root/.smbcredentials,iocharset=utf8,vers=3.0,uid=$(id -u),gid=$(id -g),file_mode=0666,dir_mode=0777 2>/dev/null +fi + +if mountpoint -q /mnt/netbackup; then + DELETED=$(find "/mnt/netbackup/${SMB_DIR}" -name "vaultwarden-*.db" -mtime +${KEEP_DAYS} -type f -print -delete 2>/dev/null | wc -l) + sudo umount /mnt/netbackup 2>/dev/null + echo "$(date): Deleted ${DELETED} old backup(s)." +else + echo "$(date): WARNING - Could not mount SMB for cleanup, skipping." +fi