From 2154ca1ce0a1b2090f8d079b42fa7732bb709ac3 Mon Sep 17 00:00:00 2001 From: Paul Kartchner Date: Wed, 10 Dec 2025 16:14:34 +0000 Subject: [PATCH] Add Vaultwarden backup system and fix HTTP header validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added BACKUP.md with comprehensive backup/restore documentation - Created backup-vaultwarden.sh for automated PostgreSQL and data backups - Created restore-vaultwarden.sh for safe backup restoration - Fixed HTTP response validation by configuring Vaultwarden-specific headers - Set X-XSS-Protection: 0 (as required by Vaultwarden) - Set X-Frame-Options: SAMEORIGIN for API calls - Removed conflicting secure-headers@file middleware - Added custom vaultwarden-headers middleware - Updated .gitignore to exclude backups/ directory Backup system: - Backs up to /srv/backups/vaultwarden/ (configurable) - Logs to /var/log/vaultwarden/backup.log - 30-day retention policy - Includes PostgreSQL database, RSA key, config, and .env Note: Backup scripts should be moved to /srv/backups/scripts/ for production use 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .gitignore | 3 + BACKUP.md | 145 +++++++++++++++++++++++++++++++++++++++++ backup-vaultwarden.sh | 81 +++++++++++++++++++++++ docker-compose.yml | 9 ++- restore-vaultwarden.sh | 87 +++++++++++++++++++++++++ 5 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 BACKUP.md create mode 100755 backup-vaultwarden.sh create mode 100755 restore-vaultwarden.sh diff --git a/.gitignore b/.gitignore index 76e9fb5..dfe7868 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ # Vaultwarden data directory (contains database and user data) data/ +# Backups directory +backups/ + # Email proxy configuration (contains OAuth credentials) emailproxy-config/ diff --git a/BACKUP.md b/BACKUP.md new file mode 100644 index 0000000..0f567c6 --- /dev/null +++ b/BACKUP.md @@ -0,0 +1,145 @@ +# Vaultwarden Backup and Restore + +## What Gets Backed Up + +### Critical (Required for restore): +- **PostgreSQL Database** - All vault data (passwords, notes, attachments metadata, etc.) +- **rsa_key.pem** - RSA private key (required to decrypt existing vault data) +- **.env file** - Database credentials and SMTP configuration + +### Important: +- **config.json** - Vaultwarden admin settings + +### Excluded (regenerated automatically): +- icon_cache/ - Website favicons +- tmp/ - Temporary files + +## Backup Location + +All backups are stored in: `/srv/backups/vaultwarden/` + +Each backup includes: +- `vaultwarden_db_YYYYMMDD_HHMMSS.dump` - PostgreSQL database (compressed) +- `vaultwarden_data_YYYYMMDD_HHMMSS.tar.gz` - Data directory (config + RSA key) +- `vaultwarden_env_YYYYMMDD_HHMMSS.env` - Environment variables + +## Manual Backup + +Run the backup script manually: + +```bash +/srv/backups/scripts/backup-vaultwarden.sh +``` + +## Automated Backups + +Set up daily automated backups using cron: + +```bash +# Create log directory +sudo mkdir -p /var/log/vaultwarden +sudo chown pkartch:pkartch /var/log/vaultwarden + +# Edit crontab +crontab -e + +# Add this line for daily backups at 2:00 AM +0 2 * * * /srv/backups/scripts/backup-vaultwarden.sh >> /var/log/vaultwarden/backup.log 2>&1 +``` + +Alternative schedules: +```bash +# Every 6 hours +0 */6 * * * /srv/backups/scripts/backup-vaultwarden.sh >> /var/log/vaultwarden/backup.log 2>&1 + +# Twice daily (2 AM and 2 PM) +0 2,14 * * * /srv/backups/scripts/backup-vaultwarden.sh >> /var/log/vaultwarden/backup.log 2>&1 + +# Weekly on Sunday at 3 AM +0 3 * * 0 /srv/backups/scripts/backup-vaultwarden.sh >> /var/log/vaultwarden/backup.log 2>&1 +``` + +## Restore from Backup + +1. List available backups: +```bash +/srv/backups/scripts/restore-vaultwarden.sh +``` + +2. Restore a specific backup: +```bash +/srv/backups/scripts/restore-vaultwarden.sh 20251210_050042 +``` + +**Warning**: Restore will: +- Stop the Vaultwarden container +- Replace the PostgreSQL database +- Replace data directory files +- Restart the container + +## Retention Policy + +- Backups older than 30 days are automatically deleted +- To change retention, edit `RETENTION_DAYS` in `backup-vaultwarden.sh` + +## Off-Site Backup Recommendations + +The backup directory is stored locally. For disaster recovery, consider: + +1. **Sync to another server**: +```bash +# Using rsync +rsync -avz /srv/backups/vaultwarden/ user@backup-server:/backups/vaultwarden/ +``` + +2. **Upload to cloud storage** (S3, Backblaze B2, etc.): +```bash +# Using rclone (example) +rclone sync /srv/backups/vaultwarden/ remote:vaultwarden-backups/ +``` + +3. **Add to existing backup solution** (e.g., Restic, Borg, Duplicati) + +## Testing Restores + +Test your backups regularly: + +```bash +# Test restore on a different machine or use a test database +PGDATABASE="vaultwarden_test" ./restore-vaultwarden.sh 20251210_050042 +``` + +## Backup Verification + +Check backup integrity: + +```bash +# List backup contents +docker run --rm -v /srv/backups/vaultwarden:/backup postgres:18-alpine \ + pg_restore --list /backup/vaultwarden_db_20251210_050042.dump | head -20 + +# Verify data archive +tar -tzf /srv/backups/vaultwarden/vaultwarden_data_20251210_050042.tar.gz +``` + +## Troubleshooting + +### Backup fails with "Permission denied" +- Ensure the backup directory is writable by your user +- Check Docker has access to mount the backup directory + +### Restore fails with version mismatch +- Update the PostgreSQL Docker image version in restore script to match your database + +### Large backup sizes +- Database size grows with number of users and vault items +- Consider increasing retention period if backups are large +- Attachments are stored in the database (not as files) + +## Security Notes + +- Backup files contain sensitive data (encrypted vault data + encryption keys) +- Protect backup directory with appropriate file permissions +- Encrypt backups before uploading to cloud storage +- Store .env file separately as it contains database credentials +- Backups are stored outside the git repository in `/srv/backups/vaultwarden/` diff --git a/backup-vaultwarden.sh b/backup-vaultwarden.sh new file mode 100755 index 0000000..3d0b1f8 --- /dev/null +++ b/backup-vaultwarden.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Vaultwarden Backup Script +# Backs up PostgreSQL database and critical data files + +set -e + +# Configuration +BACKUP_DIR="/srv/backups/vaultwarden" +DATE=$(date +%Y%m%d_%H%M%S) +PGHOST="postgresql.pkartchner.com" +PGPORT="5432" +PGUSER="vaultwarden" +PGDATABASE="vaultwarden" +PGPASSWORD="lXleSC6e5mL1ZZs8qwG+NWhNh478ipGptRXsEMZRs28=" +RETENTION_DAYS=30 + +# Create backup directory +mkdir -p "$BACKUP_DIR" + +echo "=== Vaultwarden Backup Started: $(date) ===" + +# PostgreSQL database backup (custom format - compressed and optimized) +echo "Backing up PostgreSQL database..." +docker run --rm \ + -e PGPASSWORD="$PGPASSWORD" \ + -v "$BACKUP_DIR:/backup" \ + postgres:18-alpine \ + pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" \ + -F c -b -v -f "/backup/vaultwarden_db_$DATE.dump" 2>&1 | grep -v "^$" + +if [ ${PIPESTATUS[0]} -eq 0 ]; then + echo "✓ Database backup completed" +else + echo "✗ Database backup failed" + exit 1 +fi + +# Data directory backup (critical files only) +echo "Backing up data directory..." +tar -czf "$BACKUP_DIR/vaultwarden_data_$DATE.tar.gz" \ + -C /srv/docker-compose/vaultwarden \ + --exclude='data/icon_cache' \ + --exclude='data/tmp' \ + data/config.json data/rsa_key.pem 2>&1 + +if [ $? -eq 0 ]; then + echo "✓ Data directory backup completed" +else + echo "✗ Data directory backup failed" + exit 1 +fi + +# Environment file backup (contains credentials) +echo "Backing up .env file..." +cp /srv/docker-compose/vaultwarden/.env "$BACKUP_DIR/vaultwarden_env_$DATE.env" +echo "✓ Environment file backup completed" + +# Calculate backup sizes +DB_SIZE=$(du -h "$BACKUP_DIR/vaultwarden_db_$DATE.dump" | cut -f1) +DATA_SIZE=$(du -h "$BACKUP_DIR/vaultwarden_data_$DATE.tar.gz" | cut -f1) + +echo "" +echo "=== Backup Summary ===" +echo "Database backup: $DB_SIZE" +echo "Data backup: $DATA_SIZE" +echo "Location: $BACKUP_DIR" + +# Cleanup old backups +echo "" +echo "Cleaning up backups older than $RETENTION_DAYS days..." +DELETED=$(find "$BACKUP_DIR" -name "vaultwarden_*" -mtime +$RETENTION_DAYS -type f) +if [ -n "$DELETED" ]; then + echo "$DELETED" + find "$BACKUP_DIR" -name "vaultwarden_*" -mtime +$RETENTION_DAYS -type f -delete + echo "✓ Old backups cleaned up" +else + echo "No old backups to clean up" +fi + +echo "" +echo "=== Vaultwarden Backup Completed: $(date) ===" diff --git a/docker-compose.yml b/docker-compose.yml index 6bb2c73..7c971b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,13 @@ services: labels: - "traefik.enable=true" + # Vaultwarden-specific security headers + - "traefik.http.middlewares.vaultwarden-headers.headers.customResponseHeaders.X-XSS-Protection=0" + - "traefik.http.middlewares.vaultwarden-headers.headers.customResponseHeaders.X-Content-Type-Options=nosniff" + - "traefik.http.middlewares.vaultwarden-headers.headers.customResponseHeaders.Referrer-Policy=same-origin" + - "traefik.http.middlewares.vaultwarden-headers.headers.customResponseHeaders.X-Robots-Tag=noindex, nofollow" + - "traefik.http.middlewares.vaultwarden-headers.headers.customResponseHeaders.X-Frame-Options=SAMEORIGIN" + # HTTP to HTTPS redirect - "traefik.http.routers.vaultwarden-http.rule=Host(`${DOMAIN}`)" - "traefik.http.routers.vaultwarden-http.entrypoints=http" @@ -36,7 +43,7 @@ services: - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt" - "traefik.http.routers.vaultwarden.service=vaultwarden" - "traefik.http.services.vaultwarden.loadbalancer.server.port=80" - - "traefik.http.routers.vaultwarden.middlewares=geoblock@file,secure-headers@file,crowdsec-bouncer@file" + - "traefik.http.routers.vaultwarden.middlewares=geoblock@file,vaultwarden-headers,crowdsec-bouncer@file" # WebSocket router (required for real-time sync) - "traefik.http.routers.vaultwarden-ws.rule=Host(`${DOMAIN}`) && Path(`/notifications/hub`)" diff --git a/restore-vaultwarden.sh b/restore-vaultwarden.sh new file mode 100755 index 0000000..b56de67 --- /dev/null +++ b/restore-vaultwarden.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# Vaultwarden Restore Script +# Restores PostgreSQL database and critical data files from backup + +set -e + +# Check if backup timestamp provided +if [ -z "$1" ]; then + echo "Usage: $0 YYYYMMDD_HHMMSS" + echo "" + echo "Available backups:" + ls -1 /srv/backups/vaultwarden/vaultwarden_db_*.dump 2>/dev/null | \ + sed 's/.*vaultwarden_db_\(.*\)\.dump/ \1/' | sort -r + exit 1 +fi + +TIMESTAMP=$1 +BACKUP_DIR="/srv/backups/vaultwarden" +PGHOST="postgresql.pkartchner.com" +PGPORT="5432" +PGUSER="vaultwarden" +PGDATABASE="vaultwarden" +PGPASSWORD="lXleSC6e5mL1ZZs8qwG+NWhNh478ipGptRXsEMZRs28=" + +# Verify backup files exist +DB_BACKUP="$BACKUP_DIR/vaultwarden_db_$TIMESTAMP.dump" +DATA_BACKUP="$BACKUP_DIR/vaultwarden_data_$TIMESTAMP.tar.gz" + +if [ ! -f "$DB_BACKUP" ]; then + echo "Error: Database backup not found: $DB_BACKUP" + exit 1 +fi + +if [ ! -f "$DATA_BACKUP" ]; then + echo "Error: Data backup not found: $DATA_BACKUP" + exit 1 +fi + +echo "=== Vaultwarden Restore Started: $(date) ===" +echo "WARNING: This will replace your current Vaultwarden data!" +echo "Database backup: $DB_BACKUP" +echo "Data backup: $DATA_BACKUP" +echo "" +read -p "Are you sure you want to continue? (yes/no): " CONFIRM + +if [ "$CONFIRM" != "yes" ]; then + echo "Restore cancelled" + exit 0 +fi + +# Stop Vaultwarden container +echo "" +echo "Stopping Vaultwarden container..." +cd /srv/docker-compose/vaultwarden +docker compose stop vaultwarden +echo "✓ Container stopped" + +# Restore database +echo "" +echo "Restoring PostgreSQL database..." +docker run --rm \ + -e PGPASSWORD="$PGPASSWORD" \ + -v "$BACKUP_DIR:/backup" \ + postgres:18-alpine \ + pg_restore -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" \ + --clean --if-exists -v "/backup/vaultwarden_db_$TIMESTAMP.dump" 2>&1 | \ + grep -E "(restoring|processing|creating|finished)" || true + +echo "✓ Database restored" + +# Restore data directory +echo "" +echo "Restoring data directory..." +tar -xzf "$DATA_BACKUP" -C /srv/docker-compose/vaultwarden +echo "✓ Data directory restored" + +# Start Vaultwarden container +echo "" +echo "Starting Vaultwarden container..." +docker compose start vaultwarden +sleep 3 +docker compose ps vaultwarden +echo "✓ Container started" + +echo "" +echo "=== Vaultwarden Restore Completed: $(date) ===" +echo "Please verify your Vaultwarden instance is working correctly."