Le Duy Khuong

Chuỗi: vaultwarden-secret-management · Phần 8

Năng suất & công cụ dev

Backup và Disaster Recovery — Vault là sinh mệnh của bạn

Chiến lược backup SQLite với WAL checkpoint, chính sách lưu trữ, bản sao offsite, và quy trình restore đã kiểm chứng.

2026-03-2012 phút đọcVI

Phần 8 của 5160% hoàn thành

Backup và Disaster Recovery — Vault là sinh mệnh của bạn

Vault là Single Point of Failure

Vault chứa mọi thứ — API key, database password, SSH key, bot token, OAuth credential. Nếu vault mất, bạn mất quyền truy cập vào toàn bộ hạ tầng. Không phải một service, không phải một database — mà tất cả cùng lúc.

Điều này biến vault thành single point of failure quan trọng nhất trong hệ thống của bạn. Và giải pháp duy nhất cho single point of failure là redundancy — backup đáng tin cậy, đã được kiểm chứng, và có quy trình restore rõ ràng.

Backup không phải optional. Backup là bắt buộc từ ngày đầu tiên bạn đưa Vaultwarden vào sử dụng.

Bài viết này đi sâu vào cấu trúc dữ liệu của Vaultwarden, kỹ thuật backup SQLite đúng cách, script production-ready, quy trình restore, và các kịch bản disaster recovery thực tế.


Cấu trúc dữ liệu Vaultwarden

Trước khi backup, cần hiểu rõ Vaultwarden lưu gì và ở đâu.

Các thành phần dữ liệu

/data/                          # Docker volume mount (mặc định)
├── db.sqlite3                  # Database chính — QUAN TRỌNG NHẤT
├── db.sqlite3-wal              # Write-Ahead Log
├── db.sqlite3-shm              # Shared Memory file
├── config.json                 # Runtime configuration
├── rsa_key.der                 # RSA private key (JWT signing)
├── rsa_key.pub.der             # RSA public key
├── rsa_key.pem                 # RSA key (PEM format)
├── attachments/                # Encrypted file attachments
│   └── <uuid>/                 # Mỗi attachment có thư mục riêng
│       └── <file>
├── sends/                      # Bitwarden Send files
│   └── <uuid>/
│       └── <file>
└── icon_cache/                 # Website favicon cache
    └── <domain>.png

Mô tả chi tiết từng thành phần

db.sqlite3 — Database chính

Đây là file quan trọng nhất. Chứa toàn bộ:

  • Thông tin user (email, master password hash, KDF settings)
  • Tất cả vault items (encrypted: logins, notes, cards, identities)
  • Organizations và collections
  • Cấu hình 2FA
  • Emergency access settings
  • Policies và cấu hình tổ chức

Mất file này = mất toàn bộ vault.

db.sqlite3-wal — Write-Ahead Log

SQLite sử dụng WAL (Write-Ahead Logging) mode để tăng hiệu suất concurrent read/write. WAL file chứa các thay đổi chưa được flush vào database chính.

  • Kích thước thay đổi liên tục
  • Có thể chứa dữ liệu mới nhất chưa được ghi vào db.sqlite3
  • Bắt buộc phải backup cùng với db.sqlite3

db.sqlite3-shm — Shared Memory

File shared memory hỗ trợ WAL mode. Chứa index cho WAL file.

  • Thường nhỏ (vài KB)
  • Cần backup cùng db.sqlite3 và WAL để đảm bảo consistency

attachments/ — File đính kèm đã mã hoá

Khi user đính kèm file vào vault item, file được mã hoá client-side và lưu tại đây.

  • Mã hoá bằng key của user — server không thể đọc
  • Mất thư mục này = mất tất cả file đính kèm
  • Kích thước có thể lớn tuỳ usage

config.json — Cấu hình runtime

Chứa các setting được thay đổi qua Admin Panel (khác với environment variables trong Docker).

rsa_key.* — RSA Keys

Dùng cho JWT token signing. Nếu mất, tất cả session hiện tại bị invalid (user cần đăng nhập lại), nhưng dữ liệu không mất.

icon_cache/ — Cache favicon

Cache của website icon. Có thể tái tạo hoàn toàn — không cần backup. Vaultwarden sẽ tự download lại khi cần.


Nguyên lý backup SQLite

SQLite không giống PostgreSQL hay MySQL — bạn không thể đơn giản pg_dump hay mysqldump. SQLite lưu tất cả trong file, nhưng việc copy file khi database đang hoạt động có thể dẫn đến corruption.

WAL Mode là gì và tại sao quan trọng

Khi SQLite chạy ở WAL mode (mặc định của Vaultwarden):

  1. Write operations ghi vào WAL file thay vì trực tiếp vào database
  2. Read operations đọc từ database + WAL file
  3. Checkpoint là quá trình flush dữ liệu từ WAL vào database chính

Điều này có nghĩa: tại bất kỳ thời điểm nào, db.sqlite3 có thể không chứa dữ liệu mới nhất. Dữ liệu mới nằm trong WAL file.

Checkpoint trước khi copy

-- Flush tất cả dữ liệu từ WAL vào database chính
PRAGMA wal_checkpoint(TRUNCATE);

TRUNCATE mode:

  • Flush tất cả WAL data vào database
  • Truncate WAL file về kích thước 0
  • Sau checkpoint: db.sqlite3 chứa đầy đủ dữ liệu

Copy an toàn vs không an toàn

An toàn:

# Cách 1: Checkpoint rồi copy tất cả files
sqlite3 /data/db.sqlite3 "PRAGMA wal_checkpoint(TRUNCATE);"
cp /data/db.sqlite3 /backup/db.sqlite3
cp /data/db.sqlite3-wal /backup/db.sqlite3-wal 2>/dev/null || true
cp /data/db.sqlite3-shm /backup/db.sqlite3-shm 2>/dev/null || true
 
# Cách 2: Sử dụng SQLite backup API (tốt nhất)
sqlite3 /data/db.sqlite3 ".backup /backup/db.sqlite3"

Không an toàn (TRÁNH):

# NGUY HIỂM: Copy khi đang có write operation
cp /data/db.sqlite3 /backup/db.sqlite3
# → Có thể bị corruption nếu Vaultwarden đang ghi dữ liệu
 
# NGUY HIỂM: Copy db mà không copy WAL
cp /data/db.sqlite3 /backup/db.sqlite3
# → Mất dữ liệu chưa được checkpoint

SQLite .backup command

Cách an toàn nhất là sử dụng SQLite backup API thông qua .backup command:

sqlite3 /data/db.sqlite3 ".backup /backup/db.sqlite3"

Ưu điểm:

  • Atomic — đảm bảo consistency
  • Hoạt động ngay cả khi database đang có connection active
  • Không cần stop Vaultwarden
  • Tự xử lý WAL

Script backup Production-Ready

Dưới đây là script backup hoàn chỉnh, sẵn sàng cho production:

#!/usr/bin/env bash
set -euo pipefail
 
# vaultwarden-backup.sh — Production-ready backup script
# Sử dụng: ./vaultwarden-backup.sh [--dry-run]
#
# Yêu cầu: sqlite3 CLI, rsync (cho offsite backup)
 
# ============================================================
# CẤU HÌNH
# ============================================================
 
# Đường dẫn dữ liệu Vaultwarden (Docker volume mount)
VAULT_DATA="/opt/vaultwarden/data"
 
# Thư mục backup local
BACKUP_DIR="/opt/backups/vaultwarden"
 
# Offsite backup (comment out nếu không dùng)
OFFSITE_HOST="backup-server"
OFFSITE_DIR="/backups/vaultwarden"
 
# Retention policy
DAILY_RETENTION=7       # Giữ 7 bản daily
WEEKLY_RETENTION=4      # Giữ 4 bản weekly (mỗi Chủ nhật)
 
# Kích thước tối thiểu db.sqlite3 (bytes) — dưới ngưỡng = có thể corruption
MIN_DB_SIZE=10240       # 10 KB
 
# Dry-run mode
DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
    DRY_RUN=true
fi
 
# ============================================================
# HÀM TIỆN ÍCH
# ============================================================
 
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
DAY_OF_WEEK=$(date +%u)  # 1=Monday, 7=Sunday
LOG_FILE="${BACKUP_DIR}/logs/backup-${TIMESTAMP}.log"
 
log() {
    local msg="[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*"
    echo "$msg"
    if [ "$DRY_RUN" = false ]; then
        echo "$msg" >> "$LOG_FILE"
    fi
}
 
die() {
    log "FATAL: $*"
    exit 1
}
 
# ============================================================
# KIỂM TRA ĐIỀU KIỆN TIÊN QUYẾT
# ============================================================
 
preflight_check() {
    log "--- Pre-flight check ---"
 
    # Kiểm tra sqlite3 có sẵn không
    command -v sqlite3 > /dev/null 2>&1 || die "sqlite3 not found. Install: apt install sqlite3"
 
    # Kiểm tra thư mục dữ liệu tồn tại
    [ -d "$VAULT_DATA" ] || die "Vault data directory not found: $VAULT_DATA"
 
    # Kiểm tra database file tồn tại
    [ -f "${VAULT_DATA}/db.sqlite3" ] || die "Database file not found: ${VAULT_DATA}/db.sqlite3"
 
    # Kiểm tra database integrity
    local integrity
    integrity=$(sqlite3 "${VAULT_DATA}/db.sqlite3" "PRAGMA integrity_check;" 2>&1)
    if [ "$integrity" != "ok" ]; then
        log "WARNING: Database integrity check failed: $integrity"
        log "WARNING: Proceeding with backup anyway (backup of corrupted db is better than no backup)"
    else
        log "OK: Database integrity check passed"
    fi
 
    # Tạo thư mục backup nếu chưa có
    if [ "$DRY_RUN" = false ]; then
        mkdir -p "${BACKUP_DIR}/daily"
        mkdir -p "${BACKUP_DIR}/weekly"
        mkdir -p "${BACKUP_DIR}/logs"
    fi
 
    log "OK: Pre-flight check passed"
}
 
# ============================================================
# BACKUP DATABASE
# ============================================================
 
backup_database() {
    log "--- Backing up database ---"
 
    local backup_file="${BACKUP_DIR}/daily/vaultwarden-${TIMESTAMP}.sqlite3"
 
    if [ "$DRY_RUN" = true ]; then
        log "DRY-RUN: Would checkpoint WAL"
        log "DRY-RUN: Would backup to ${backup_file}"
        return
    fi
 
    # Bước 1: Checkpoint WAL
    log "Checkpointing WAL..."
    sqlite3 "${VAULT_DATA}/db.sqlite3" "PRAGMA wal_checkpoint(TRUNCATE);" 2>&1 | \
        while read -r line; do log "  checkpoint: $line"; done
 
    # Bước 2: Backup bằng SQLite backup API
    log "Running SQLite backup..."
    sqlite3 "${VAULT_DATA}/db.sqlite3" ".backup '${backup_file}'"
 
    # Bước 3: Verify kích thước backup
    local backup_size
    backup_size=$(stat -f%z "$backup_file" 2>/dev/null || stat -c%s "$backup_file" 2>/dev/null)
 
    if [ "$backup_size" -lt "$MIN_DB_SIZE" ]; then
        log "WARNING: Backup file suspiciously small: ${backup_size} bytes (threshold: ${MIN_DB_SIZE})"
        log "WARNING: Possible corruption — keeping file for investigation"
    else
        log "OK: Backup size: ${backup_size} bytes"
    fi
 
    # Bước 4: Verify integrity của backup
    local backup_integrity
    backup_integrity=$(sqlite3 "$backup_file" "PRAGMA integrity_check;" 2>&1)
    if [ "$backup_integrity" != "ok" ]; then
        log "WARNING: Backup integrity check failed: $backup_integrity"
    else
        log "OK: Backup integrity verified"
    fi
 
    log "OK: Database backed up to ${backup_file}"
}
 
# ============================================================
# BACKUP ATTACHMENTS VÀ CONFIG
# ============================================================
 
backup_extras() {
    log "--- Backing up attachments and config ---"
 
    local extras_dir="${BACKUP_DIR}/daily/extras-${TIMESTAMP}"
 
    if [ "$DRY_RUN" = true ]; then
        log "DRY-RUN: Would backup attachments and config to ${extras_dir}"
        return
    fi
 
    mkdir -p "$extras_dir"
 
    # Config file
    if [ -f "${VAULT_DATA}/config.json" ]; then
        cp "${VAULT_DATA}/config.json" "${extras_dir}/"
        log "OK: config.json copied"
    fi
 
    # RSA keys
    for key_file in rsa_key.der rsa_key.pub.der rsa_key.pem; do
        if [ -f "${VAULT_DATA}/${key_file}" ]; then
            cp "${VAULT_DATA}/${key_file}" "${extras_dir}/"
        fi
    done
    log "OK: RSA keys copied"
 
    # Attachments
    if [ -d "${VAULT_DATA}/attachments" ]; then
        cp -r "${VAULT_DATA}/attachments" "${extras_dir}/"
        local attachment_count
        attachment_count=$(find "${extras_dir}/attachments" -type f 2>/dev/null | wc -l)
        log "OK: ${attachment_count} attachment files copied"
    else
        log "INFO: No attachments directory found (OK if no attachments used)"
    fi
 
    # Sends
    if [ -d "${VAULT_DATA}/sends" ]; then
        cp -r "${VAULT_DATA}/sends" "${extras_dir}/"
        log "OK: Sends directory copied"
    fi
 
    log "OK: Extras backed up to ${extras_dir}"
}
 
# ============================================================
# WEEKLY BACKUP (Chủ nhật)
# ============================================================
 
promote_weekly() {
    if [ "$DAY_OF_WEEK" != "7" ]; then
        log "INFO: Not Sunday — skipping weekly promotion"
        return
    fi
 
    log "--- Promoting to weekly backup ---"
 
    if [ "$DRY_RUN" = true ]; then
        log "DRY-RUN: Would promote today's backup to weekly"
        return
    fi
 
    local weekly_dir="${BACKUP_DIR}/weekly/week-${TIMESTAMP}"
    mkdir -p "$weekly_dir"
 
    # Copy database backup mới nhất
    local latest_db
    latest_db=$(ls -t "${BACKUP_DIR}"/daily/vaultwarden-*.sqlite3 2>/dev/null | head -1)
    if [ -n "$latest_db" ]; then
        cp "$latest_db" "$weekly_dir/"
        log "OK: Database promoted to weekly"
    fi
 
    # Copy extras mới nhất
    local latest_extras
    latest_extras=$(ls -dt "${BACKUP_DIR}"/daily/extras-* 2>/dev/null | head -1)
    if [ -n "$latest_extras" ]; then
        cp -r "$latest_extras" "$weekly_dir/"
        log "OK: Extras promoted to weekly"
    fi
 
    log "OK: Weekly backup created at ${weekly_dir}"
}
 
# ============================================================
# RETENTION — XOÁ BACKUP CŨ
# ============================================================
 
cleanup_old_backups() {
    log "--- Cleaning up old backups ---"
 
    if [ "$DRY_RUN" = true ]; then
        log "DRY-RUN: Would clean up backups older than retention policy"
        return
    fi
 
    # Daily: giữ N bản mới nhất
    local daily_dbs
    daily_dbs=$(ls -t "${BACKUP_DIR}"/daily/vaultwarden-*.sqlite3 2>/dev/null)
    local count=0
    while IFS= read -r file; do
        count=$((count + 1))
        if [ $count -gt "$DAILY_RETENTION" ]; then
            rm -f "$file"
            log "CLEANUP: Removed old daily: $(basename "$file")"
        fi
    done <<< "$daily_dbs"
 
    # Daily extras: giữ N bản mới nhất
    local daily_extras
    daily_extras=$(ls -dt "${BACKUP_DIR}"/daily/extras-* 2>/dev/null)
    count=0
    while IFS= read -r dir; do
        count=$((count + 1))
        if [ $count -gt "$DAILY_RETENTION" ] && [ -d "$dir" ]; then
            rm -rf "$dir"
            log "CLEANUP: Removed old daily extras: $(basename "$dir")"
        fi
    done <<< "$daily_extras"
 
    # Weekly: giữ N bản mới nhất
    local weekly_dirs
    weekly_dirs=$(ls -dt "${BACKUP_DIR}"/weekly/week-* 2>/dev/null)
    count=0
    while IFS= read -r dir; do
        count=$((count + 1))
        if [ $count -gt "$WEEKLY_RETENTION" ] && [ -d "$dir" ]; then
            rm -rf "$dir"
            log "CLEANUP: Removed old weekly: $(basename "$dir")"
        fi
    done <<< "$weekly_dirs"
 
    log "OK: Cleanup complete"
}
 
# ============================================================
# OFFSITE BACKUP
# ============================================================
 
offsite_sync() {
    if [ -z "${OFFSITE_HOST:-}" ]; then
        log "INFO: Offsite backup not configured — skipping"
        return
    fi
 
    log "--- Syncing to offsite ---"
 
    if [ "$DRY_RUN" = true ]; then
        log "DRY-RUN: Would rsync to ${OFFSITE_HOST}:${OFFSITE_DIR}"
        return
    fi
 
    rsync -avz --delete \
        "${BACKUP_DIR}/daily/" \
        "${OFFSITE_HOST}:${OFFSITE_DIR}/daily/" \
        2>&1 | tail -3 | while read -r line; do log "  rsync: $line"; done
 
    rsync -avz --delete \
        "${BACKUP_DIR}/weekly/" \
        "${OFFSITE_HOST}:${OFFSITE_DIR}/weekly/" \
        2>&1 | tail -3 | while read -r line; do log "  rsync: $line"; done
 
    log "OK: Offsite sync complete"
}
 
# ============================================================
# THỰC THI
# ============================================================
 
main() {
    log "=========================================="
    log "Vaultwarden Backup — ${TIMESTAMP}"
    if [ "$DRY_RUN" = true ]; then
        log "MODE: DRY-RUN (no changes will be made)"
    fi
    log "=========================================="
 
    preflight_check
    backup_database
    backup_extras
    promote_weekly
    cleanup_old_backups
    offsite_sync
 
    log "=========================================="
    log "Backup complete."
    log "=========================================="
}
 
main "$@"

Lưu script và cấp quyền thực thi:

chmod +x /opt/scripts/vaultwarden-backup.sh
 
# Test với dry-run trước
/opt/scripts/vaultwarden-backup.sh --dry-run

Thiết lập Cron

Cấu hình cron cho backup tự động

# Mở crontab
crontab -e

Thêm các dòng sau:

# Vaultwarden backup — chạy mỗi ngày lúc 3:00 AM
0 3 * * * /opt/scripts/vaultwarden-backup.sh >> /opt/backups/vaultwarden/logs/cron.log 2>&1
 
# Integrity check hàng tuần (Chủ nhật 2:00 AM, trước backup)
0 2 * * 0 sqlite3 /opt/vaultwarden/data/db.sqlite3 "PRAGMA integrity_check;" >> /opt/backups/vaultwarden/logs/integrity.log 2>&1

Xác nhận cron đã hoạt động

# Xem crontab hiện tại
crontab -l
 
# Kiểm tra cron log (tuỳ distro)
# Ubuntu/Debian
grep -i cron /var/log/syslog | tail -20
 
# CentOS/RHEL
grep -i cron /var/log/cron | tail -20
 
# Kiểm tra backup file đã được tạo
ls -la /opt/backups/vaultwarden/daily/

RPO và RTO

Hai khái niệm quan trọng nhất trong disaster recovery planning:

RPO — Recovery Point Objective

"Bạn chấp nhận mất bao nhiêu dữ liệu?"

RPO đo bằng thời gian: khoảng cách giữa backup gần nhất và thời điểm sự cố.

Tần suất backupRPONghĩa là
Hàng ngày (3:00 AM)24 giờMất tối đa 24 giờ dữ liệu
Mỗi 6 giờ6 giờMất tối đa 6 giờ dữ liệu
Mỗi giờ1 giờMất tối đa 1 giờ dữ liệu
Real-time replication~0Gần như không mất dữ liệu

Khuyến nghị cho Vaultwarden:

  • Minimum: Hàng ngày (RPO 24h) — phù hợp nếu vault ít thay đổi
  • Recommended: Mỗi 6-12 giờ (RPO 6-12h) — cân bằng giữa an toàn và tài nguyên
  • Không cần real-time: Vault thường không thay đổi liên tục — daily đã đủ cho hầu hết trường hợp

RTO — Recovery Time Objective

"Bạn chấp nhận downtime bao lâu?"

RTO đo thời gian từ khi phát hiện sự cố đến khi service hoạt động trở lại.

Thành phầnThời gian ước tính
Phát hiện sự cố5-30 phút (tuỳ monitoring)
Chuẩn bị môi trường mới (nếu cần)10-30 phút
Restore database1-5 phút
Restore attachments1-10 phút (tuỳ kích thước)
Start Vaultwarden1-2 phút
Verify5-10 phút
Tổng RTO ước tính30 phút - 1.5 giờ

Cách giảm RPO

# Tăng tần suất backup
# Thay vì 1 lần/ngày → 4 lần/ngày
0 */6 * * * /opt/scripts/vaultwarden-backup.sh
 
# Hoặc sử dụng Litestream cho continuous SQLite replication
# https://litestream.io — stream WAL changes liên tục tới S3/remote
litestream replicate /opt/vaultwarden/data/db.sqlite3 s3://my-bucket/vaultwarden/

Quy trình Restore chi tiết

Khi cần khôi phục vault, thực hiện từng bước sau:

Bước 1: Dừng Vaultwarden

# Docker Compose
docker compose -f /opt/vaultwarden/docker-compose.yml down
 
# Hoặc Docker standalone
docker stop vaultwarden

Quan trọng: PHẢI dừng container trước khi thay thế database. Ghi đè file khi Vaultwarden đang chạy sẽ gây corruption.

Bước 2: Xác định backup cần restore

# Liệt kê backup có sẵn
echo "=== Daily backups ==="
ls -lh /opt/backups/vaultwarden/daily/vaultwarden-*.sqlite3
 
echo "=== Weekly backups ==="
ls -lh /opt/backups/vaultwarden/weekly/*/vaultwarden-*.sqlite3
 
# Chọn backup phù hợp (ví dụ: bản gần nhất)
RESTORE_DB=$(ls -t /opt/backups/vaultwarden/daily/vaultwarden-*.sqlite3 | head -1)
echo "Will restore from: $RESTORE_DB"
 
# Kiểm tra integrity của backup trước khi restore
sqlite3 "$RESTORE_DB" "PRAGMA integrity_check;"
# Phải trả về "ok"

Bước 3: Xác định đường dẫn volume

# Nếu dùng Docker volume
docker volume inspect vaultwarden_data | jq -r '.[0].Mountpoint'
# Ví dụ: /var/lib/docker/volumes/vaultwarden_data/_data
 
# Nếu dùng bind mount (phổ biến hơn)
# Xem docker-compose.yml hoặc docker inspect
docker inspect vaultwarden | jq '.[0].Mounts'
# Ví dụ mount path: /opt/vaultwarden/data

Bước 4: Thay thế database

VAULT_DATA="/opt/vaultwarden/data"
 
# Backup file hiện tại (đề phòng)
if [ -f "${VAULT_DATA}/db.sqlite3" ]; then
    mv "${VAULT_DATA}/db.sqlite3" "${VAULT_DATA}/db.sqlite3.broken.$(date +%Y%m%d)"
    mv "${VAULT_DATA}/db.sqlite3-wal" "${VAULT_DATA}/db.sqlite3-wal.broken.$(date +%Y%m%d)" 2>/dev/null || true
    mv "${VAULT_DATA}/db.sqlite3-shm" "${VAULT_DATA}/db.sqlite3-shm.broken.$(date +%Y%m%d)" 2>/dev/null || true
fi
 
# Copy backup vào
cp "$RESTORE_DB" "${VAULT_DATA}/db.sqlite3"
 
# Nếu có extras backup, restore attachments
RESTORE_EXTRAS=$(ls -dt /opt/backups/vaultwarden/daily/extras-* | head -1)
if [ -d "${RESTORE_EXTRAS}/attachments" ]; then
    rm -rf "${VAULT_DATA}/attachments"
    cp -r "${RESTORE_EXTRAS}/attachments" "${VAULT_DATA}/"
fi
 
# Restore config nếu cần
if [ -f "${RESTORE_EXTRAS}/config.json" ]; then
    cp "${RESTORE_EXTRAS}/config.json" "${VAULT_DATA}/"
fi
 
# Restore RSA keys
for key_file in rsa_key.der rsa_key.pub.der rsa_key.pem; do
    if [ -f "${RESTORE_EXTRAS}/${key_file}" ]; then
        cp "${RESTORE_EXTRAS}/${key_file}" "${VAULT_DATA}/"
    fi
done

Bước 5: Sửa quyền sở hữu file

Vaultwarden container thường chạy với UID 1000. Nếu quyền sai, container sẽ không đọc được database.

# Đặt ownership đúng
chown -R 1000:1000 "${VAULT_DATA}/db.sqlite3"
chown -R 1000:1000 "${VAULT_DATA}/attachments" 2>/dev/null || true
chown -R 1000:1000 "${VAULT_DATA}/config.json" 2>/dev/null || true
 
# Đặt permission đúng
chmod 600 "${VAULT_DATA}/db.sqlite3"
chmod 700 "${VAULT_DATA}/attachments" 2>/dev/null || true

Bước 6: Khởi động lại Vaultwarden

# Docker Compose
docker compose -f /opt/vaultwarden/docker-compose.yml up -d
 
# Kiểm tra container đã start
docker ps | grep vaultwarden
 
# Xem log khởi động
docker logs vaultwarden --tail 50

Bước 7: Xác nhận hoạt động

# Kiểm tra web interface
curl -sf https://vault.example.com/ | head -5
 
# Đăng nhập thử
# → Mở trình duyệt, truy cập vault, đăng nhập bằng master password
 
# Kiểm tra items có đầy đủ không
# → Kiểm tra số lượng items, organizations, collections
 
# Kiểm tra 2FA vẫn hoạt động
# → Nếu 2FA settings nằm trong database (TOTP secret), cần xác nhận
 
# Kiểm tra attachments
# → Mở một item có attachment, tải xuống và verify

Kịch bản Disaster Recovery

Kịch bản 1: SQLite Corruption

Triệu chứng: Vaultwarden báo lỗi database, không thể đọc vault.

[ERROR] rusqlite::Error::SqliteFailure: database disk image is malformed

Xử lý:

# 1. Thử repair trước
sqlite3 /opt/vaultwarden/data/db.sqlite3 ".recover" > /tmp/recovered.sql
sqlite3 /tmp/recovered.sqlite3 < /tmp/recovered.sql
 
# Kiểm tra recovered database
sqlite3 /tmp/recovered.sqlite3 "PRAGMA integrity_check;"
 
# 2. Nếu repair thành công → dùng recovered database
# 3. Nếu repair thất bại → restore từ backup (quy trình ở trên)

Kịch bản 2: Host Machine bị hỏng

Triệu chứng: Server không khởi động được, disk failure, hardware failure.

Xử lý:

# 1. Provision host mới (VM, VPS, bare metal)
# 2. Cài đặt Docker
apt update && apt install -y docker.io docker-compose-v2
 
# 3. Deploy Vaultwarden (từ docker-compose.yml đã lưu)
mkdir -p /opt/vaultwarden/data
# Copy docker-compose.yml từ git/backup
 
# 4. Restore từ offsite backup
rsync -avz backup-server:/backups/vaultwarden/daily/ /opt/backups/vaultwarden/daily/
 
# 5. Restore database (quy trình ở trên)
RESTORE_DB=$(ls -t /opt/backups/vaultwarden/daily/vaultwarden-*.sqlite3 | head -1)
cp "$RESTORE_DB" /opt/vaultwarden/data/db.sqlite3
chown 1000:1000 /opt/vaultwarden/data/db.sqlite3
 
# 6. Start Vaultwarden
docker compose -f /opt/vaultwarden/docker-compose.yml up -d
 
# 7. Cập nhật DNS (nếu IP thay đổi)
# 8. Verify

Kịch bản 3: Ransomware

Triệu chứng: Tất cả file bị encrypt, ransom note xuất hiện.

Xử lý:

# 1. KHÔNG TRUY CẬP vào máy bị nhiễm
# 2. Isolate máy (ngắt mạng)
# 3. Provision host hoàn toàn mới
 
# 4. Restore từ OFFSITE backup (backup local có thể đã bị encrypt)
# Offsite backup qua SSH/rsync/cloud storage
rsync -avz backup-server:/backups/vaultwarden/ /opt/backups/vaultwarden/
 
# 5. Kiểm tra integrity của backup (ransomware có thể đã encrypt backup nếu accessible)
sqlite3 /opt/backups/vaultwarden/daily/vaultwarden-latest.sqlite3 "PRAGMA integrity_check;"
 
# 6. Restore và deploy (quy trình ở trên)
# 7. ROTATE TẤT CẢ SECRETS (coi như đã bị compromise)
# 8. Điều tra phạm vi ảnh hưởng

Bài học: Đây là lý do offsite backup (hoặc air-gapped backup) quan trọng. Nếu backup nằm trên cùng máy hoặc cùng mạng, ransomware có thể encrypt luôn.

Kịch bản 4: Xoá nhầm Vault Items

Triệu chứng: User vô tình xoá item hoặc collection quan trọng.

Xử lý:

# Vaultwarden có Trash (Recycle Bin) — item xoá nằm trong trash 30 ngày
# → Kiểm tra Trash trước
 
# Nếu đã purge khỏi Trash:
# 1. Xác định thời điểm xoá
# 2. Tìm backup trước thời điểm đó
ls -lt /opt/backups/vaultwarden/daily/vaultwarden-*.sqlite3
 
# 3. KHÔNG restore toàn bộ vault (sẽ mất thay đổi mới)
# 4. Mount backup database readonly, export item cần thiết
sqlite3 /opt/backups/vaultwarden/daily/vaultwarden-20260315.sqlite3 \
    "SELECT * FROM ciphers WHERE name LIKE '%keyword%';"
 
# 5. Hoặc: tạo Vaultwarden instance tạm với backup database
docker run -d --name vw-restore \
    -v /opt/backups/vaultwarden/daily/extras-20260315:/data \
    -p 8888:80 \
    vaultwarden/server:latest
 
# 6. Đăng nhập vào instance tạm, export item cần thiết
# 7. Import vào vault chính
# 8. Dừng và xoá instance tạm
docker rm -f vw-restore

Kiểm tra phục hồi định kỳ (Restore Drill)

Backup chưa được test = backup có thể không hoạt động. Thực hiện restore drill hàng tháng:

Checklist Restore Drill

## Restore Drill — [Tháng] [Năm]
 
**Ngày thực hiện:** YYYY-MM-DD
**Người thực hiện:** _______________
**Backup sử dụng:** [filename + date]
 
### Các bước thực hiện
 
- [ ] Tạo test instance (Docker, port khác production)
- [ ] Copy backup database vào test instance data directory
- [ ] Copy backup attachments (nếu có)
- [ ] Fix permissions (UID 1000)
- [ ] Start test instance
- [ ] Đăng nhập thành công bằng master password
- [ ] Kiểm tra số lượng items (so sánh với production)
- [ ] Mở 3 items ngẫu nhiên, verify nội dung
- [ ] Tải xuống 1 attachment (nếu có), verify
- [ ] Kiểm tra organization access
- [ ] Dừng và xoá test instance
- [ ] Ghi nhận kết quả
 
### Kết quả
- [ ] PASS — Restore thành công, dữ liệu đầy đủ
- [ ] PARTIAL — Restore thành công nhưng có vấn đề: ___
- [ ] FAIL — Không thể restore: ___
 
### Thời gian restore thực tế: ___ phút
### Ghi chú: _______________

Script tự động hoá restore drill

#!/usr/bin/env bash
set -euo pipefail
 
# restore-drill.sh — Automated restore verification
 
BACKUP_DB=$(ls -t /opt/backups/vaultwarden/daily/vaultwarden-*.sqlite3 | head -1)
TEST_PORT=8888
TEST_DATA="/tmp/vw-restore-drill"
 
echo "=== Restore Drill ==="
echo "Backup: $BACKUP_DB"
echo "Test port: $TEST_PORT"
 
# Setup
rm -rf "$TEST_DATA"
mkdir -p "$TEST_DATA"
cp "$BACKUP_DB" "${TEST_DATA}/db.sqlite3"
chown -R 1000:1000 "$TEST_DATA"
 
# Start test instance
docker run -d --name vw-drill \
    -v "${TEST_DATA}:/data" \
    -p "${TEST_PORT}:80" \
    -e ADMIN_TOKEN=drill-test-token \
    vaultwarden/server:latest
 
echo "Waiting for startup..."
sleep 5
 
# Health check
if curl -sf "http://localhost:${TEST_PORT}/alive" > /dev/null 2>&1; then
    echo "PASS: Vaultwarden started successfully"
else
    echo "FAIL: Vaultwarden failed to start"
    docker logs vw-drill --tail 20
    docker rm -f vw-drill
    rm -rf "$TEST_DATA"
    exit 1
fi
 
# Kiểm tra admin panel accessible
if curl -sf "http://localhost:${TEST_PORT}/admin" -o /dev/null; then
    echo "PASS: Admin panel accessible"
else
    echo "WARN: Admin panel not accessible"
fi
 
# Cleanup
echo "Cleaning up..."
docker rm -f vw-drill
rm -rf "$TEST_DATA"
 
echo "=== Restore Drill Complete ==="

Chiến lược Offsite Backup

rsync qua SSH / Tailscale

# rsync qua SSH (đơn giản, đáng tin cậy)
rsync -avz -e "ssh -p 22" \
    /opt/backups/vaultwarden/ \
    user@backup-host:/backups/vaultwarden/
 
# rsync qua Tailscale (zero-config VPN)
rsync -avz \
    /opt/backups/vaultwarden/ \
    backup-host:/backups/vaultwarden/
# Tailscale tự xử lý encryption và routing

Cloud Storage (Encrypted)

Sử dụng rclone để đẩy backup lên cloud storage đã encrypt:

# Cấu hình rclone (một lần)
rclone config
# → Tạo remote "b2-encrypted" với Backblaze B2 + crypt overlay
 
# Sync backup lên cloud
rclone sync /opt/backups/vaultwarden/ b2-encrypted:vaultwarden-backup/
 
# Verify
rclone ls b2-encrypted:vaultwarden-backup/daily/

Quan trọng: Luôn encrypt trước khi đẩy lên cloud. Dù database đã encrypt ở application layer, defense-in-depth yêu cầu encrypt thêm ở storage layer.

USB Drive (Air-gapped)

Cho mức bảo mật cao nhất — backup offline, không kết nối mạng:

# Hàng quý: copy backup ra USB drive
# 1. Cắm USB drive
# 2. Mount
sudo mount /dev/sdb1 /mnt/usb
 
# 3. Copy
rsync -avz /opt/backups/vaultwarden/weekly/ /mnt/usb/vaultwarden-backup/
 
# 4. Unmount
sudo umount /mnt/usb
 
# 5. Cất USB ở nơi an toàn (khác vị trí server)

Air-gapped backup bảo vệ khỏi: ransomware, network compromise, cloud provider outage, và insider threat.


Tóm tắt chuỗi bài viết

Bài viết này kết thúc chuỗi 8 bài về Vaultwarden. Dưới đây là tóm tắt toàn bộ hành trình:

BàiChủ đềNội dung chính
1Giới thiệu VaultwardenTại sao cần password manager, Vaultwarden vs Bitwarden
2Cài đặt và cấu hìnhDocker deployment, reverse proxy, TLS
3Quản lý người dùng và tổ chứcUsers, organizations, collections, sharing
4Bitwarden CLI và tự động hoáCLI commands, scripting, CI/CD integration
5Bảo mật VaultwardenHardening, 2FA, admin panel, fail2ban
6Secret management cho đội ngũWorkflow, naming conventions, access control
7Xoay vòng và Audit SecretRotation schedule, automation, compliance checklist
8Backup và Disaster RecoverySQLite backup, restore procedure, DR scenarios

Từ cài đặt ban đầu đến vận hành production với backup và disaster recovery — bạn đã có đầy đủ kiến thức để triển khai và duy trì một hệ thống quản lý secret đáng tin cậy cho đội ngũ của mình.

Nguyên tắc xuyên suốt: Secret management không phải là "set and forget". Nó đòi hỏi vận hành liên tục — rotation, audit, backup, và kiểm tra định kỳ. Vaultwarden cung cấp nền tảng; quy trình và kỷ luật vận hành là trách nhiệm của bạn.

LDK

Le Duy Khuong

AI Transformation & Digital Strategy. Writing about agentic systems, engineering leadership, and building in public.