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.
This commit is contained in:
bdm
2026-09-28 06:24:25 -07:00
commit d27e93d220
9 changed files with 453 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 <email> [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 <email> [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"

View File

@@ -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 <email> <input-file.json>
# 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 <email> <input-file.json>"
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."

View File

@@ -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

View File

@@ -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 <backup-file.db>
# 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' <user> <pass> | install -m 600 /dev/stdin ${SMB_CRED_FILE}"
exit 1
fi
if [ -z "$BACKUP_FILE" ]; then
echo "Usage: $0 <backup-file.db>"
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."

View File

@@ -0,0 +1,3 @@
#!/bin/bash
# Unmount SMB share for vaultwarden backup cleanup
umount /mnt/netbackup 2>/dev/null

65
vaultwarden-backup.sh Executable file
View File

@@ -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' <user> <pass> | 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