Vaultwarden 備份與還原:VPS 完整教學
使用 sqlite3 .backup 複製線上 Vaultwarden 資料庫,並保留 attachments、config.json 與 rsa_key 檔案,還原後先驗證備份可用。
Vaultwarden 備份必須包含的內容
Vaultwarden 備份是整個資料目錄的副本,其中的資料庫也必須使用正確方式複製。請執行 sqlite3 db.sqlite3 ".backup out.sqlite3",不要執行 cp,因為直接複製正在寫入的資料庫,可能會產生無法開啟的檔案。接著保留與資料庫同層的其他檔案,這是最容易被忽略的部分。
在 Docker 安裝中,資料目錄就是掛載到 /data 的位置。它可能是主機上的路徑,也可能是命名 volume;bind mount 與命名 volume 的差異會決定 vault 實際儲存在哪裡。該目錄包含以下內容。
db.sqlite3:所有帳戶、vault 項目、資料夾與組織。遺失此檔案就會遺失 vault。db.sqlite3-wal和db.sqlite3-shm:預寫式日誌(WAL)及其共享記憶體索引。SQLite 將最近的寫入內容合併至主檔案前,會先儲存在這裡。attachments/:使用者附加至 vault 項目的檔案。檔案會經過加密,並依項目分別儲存在目錄中。sends/:Bitwarden Send 連結所指向的檔案。config.json:從管理頁面儲存的所有設定。rsa_key.pem,以及較舊安裝中的rsa_key.der和rsa_key.pub.der:用於簽署登入 token 的金鑰。icon_cache/:下載的網站圖示。這是唯一可以略過的目錄,因為 Vaultwarden 會在需要時重新擷取圖示。
Vaultwarden 資料庫安全嗎?檔案實際儲存的內容
只要執行兩個命令即可確認,而且現在就能執行。
sudo apt update && sudo apt install -y sqlite3
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select email from users;"
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select name from ciphers limit 1;"第一個命令會以明文列出使用者的電子郵件地址。第二個命令會列出一個項目名稱,結果如下:
2.k9Qw1nQ0y7Yy2Xw==|E1r0J3l5s7d9f1g3h5j7k9==|Lm4nOp6qRs8tUv0wXy2zAb4cDe6fGh8i=項目名稱、使用者名稱、密碼與備註會在傳送前由用戶端加密,因此伺服器儲存的是無法讀取的密文。2. 前綴是 Bitwarden 的加密類型,後面依序是初始化向量(IV)、密文及 MAC(訊息驗證碼);每個部分都使用 base64 編碼,並以 | 分隔。解密所需的金鑰衍生自帳戶的主密碼,而主密碼不會以可供使用的形式傳送到伺服器。無論執行 Vaultwarden 或官方伺服器,這部分都相同,Vaultwarden 與自架 Bitwarden 的比較會進一步說明。
資料庫的其餘部分並未加密。電子郵件地址、帳戶名稱、密碼提示與雙因素驗證復原碼都以明文儲存,旁邊還有建立時間及項目所屬組織等中繼資料。因此,備份檔案本身就是機密資料。任何持有該檔案的人都能得知使用者身分,並以其硬體允許的速度離線攻擊加密資料。這項事實決定了後文的儲存規則:副本在離開伺服器前必須先加密。管理員 token 是同一問題的另一部分,而自架 Vaultwarden 的強化設定會一併處理這兩項問題。
為什麼 Vaultwarden 執行時複製 db.sqlite3 不是備份
Vaultwarden 預設以 WAL 模式執行 SQLite(ENABLE_DB_WAL=true)。寫入內容會先寫入 db.sqlite3-wal,只有 checkpoint 才會將其整合至 db.sqlite3。如果只複製 db.sqlite3,取得的會是上次 checkpoint 時的資料庫狀態。因此,10 分鐘前儲存的密碼可能會遺失在封存中,而且不會有任何警告。
使用 cp 複製這 3 個檔案也無法解決問題。複本是在略有差異的時間點擷取,因此所保存的 WAL 可能包含頁面版本,但所保存的主檔案已與這些版本不一致。SQLite 接著會嘗試從其中一個檔案復原另一個檔案,結果可能不正確。你可能要很久之後才會發現:
Error: database disk image is malformed.backup 可避免這個問題,因為它使用 SQLite 的 Online Backup API。SQLite 將這套 API 文件化為複製使用中的資料庫時所採用的方法。它會在讀取鎖定下讀取頁面;如果寫入者在讀取期間修改檔案,則會重新開始。因此,最終寫入磁碟的內容會對應到同一個一致的時間點。
使用 sqlite3 .backup 建立資料庫副本
sudo apt update && sudo apt install -y sqlite3
sudo install -d -m 700 /var/backups/vaultwarden
OUT=/var/backups/vaultwarden/db-$(date '+%Y%m%d-%H%M').sqlite3
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 ".backup '$OUT'"
sudo sqlite3 "$OUT" "PRAGMA integrity_check;"最後一個指令會單獨輸出 ok。如果輸出其他內容,表示副本無法使用。請勿保留該副本,也不要刪除先前的副本。整個流程會在執行中的伺服器上進行,因此不會登出任何使用者,也不會重新啟動容器。
sqlite3 工具不在 Vaultwarden 容器內。此映像檔以 debian:trixie-slim 為基礎,包含 ca-certificates、curl、libmariadb3、libpq5 和 openssl,因此 docker exec vaultwarden sqlite3 ... 會失敗並顯示:
exec: "sqlite3": executable file not found in $PATH請改在主機上,針對掛載路徑執行該工具。上面的指令就是這樣做的。如果資料位於具名 volume 中,docker volume inspect <name> 會輸出主機上的路徑,位置在 /var/lib/docker/volumes/ 下。
Vaultwarden 從 1.32.1 版起也提供自己的備份指令。在伺服器上執行:
docker exec -it vaultwarden /vaultwarden backup此指令會執行 VACUUM INTO,並將 db_YYYYMMDD_HHMMSS.sqlite3 寫入資料目錄。這會帶來兩個結果。副本會與原始檔案位於同一個磁碟上,因此這只是暫存步驟,尚未構成備份。此外,這項功能僅支援 SQLite;使用 MariaDB 或 PostgreSQL 時,會因 The database type is not SQLite. Backups only works for SQLite databases 而停止。
容易被遺忘的檔案
attachments/ 以不透明名稱儲存密文。每個附件的資料庫資料列都包含加密後的檔名,以及用戶端解密檔案所需的金鑰材料。缺少附件時,資料庫中的內容會變成無法讀取的雜訊;缺少資料庫時,使用者仍會看到項目,但下載會失敗。請在同一次備份中同時保存兩者。
config.json 儲存您從管理頁面保存的所有設定,其值的優先順序高於相符的環境變數。這有利也有弊:還原舊的 config.json 會在不知不覺中覆寫 compose 檔案中的設定,而該檔案本身也屬於敏感資料,因為其中可能包含 SMTP 密碼與管理員 token。請將該 token 儲存為 Argon2id PHC(password hashing competition)字串,不要使用純文字。docker run --rm -it vaultwarden/server /vaultwarden hash 會為您產生一組。
rsa_key.pem 用來簽署讓用戶端保持登入狀態的 JSON Web Token (JWT)。如果啟動時找不到該檔案,Vaultwarden 會產生新的金鑰,因此所有由舊金鑰簽署的 token 都會停止通過驗證,所有用戶端也會被登出。Vault 內容不受此影響,因為它們是使用由主密碼衍生的金鑰加密。還原金鑰檔案即可避免所有用戶端同時登出。
sends/ 儲存 Send 連結所指向的檔案。缺少這些檔案只會使相關下載失敗,不會影響其他功能。
將所有內容放在同一個指令碼中
#!/bin/bash
set -euo pipefail
DATA=/opt/vaultwarden/data
DEST=/var/backups/vaultwarden
STAMP=$(date '+%Y%m%d-%H%M%S')
STAGE=$(mktemp -d /tmp/vw-stage.XXXXXX)
install -d -m 700 "$DEST"
sqlite3 "$DATA/db.sqlite3" ".backup '$STAGE/db.sqlite3'"
test "$(sqlite3 "$STAGE/db.sqlite3" 'PRAGMA integrity_check;')" = "ok"
cp -a "$DATA"/rsa_key* "$STAGE/"
for extra in config.json attachments sends; do
if [ -e "$DATA/$extra" ]; then cp -a "$DATA/$extra" "$STAGE/"; fi
done
tar -C "$STAGE" -czf "$DEST/vw-$STAMP.tar.gz" .
chmod 600 "$DEST/vw-$STAMP.tar.gz"
rm -rf "$STAGE"
tar -tzf "$DEST/vw-$STAMP.tar.gz"將它儲存為 /usr/local/sbin/vw-backup.sh、chmod 700,然後以 root 身分執行。test 這一行會執行實際工作:即使 PRAGMA integrity_check 回報資料損毀,sqlite3 仍會傳回 0,因此將輸出與 ok 比較,才能讓錯誤的複製作業使指令碼失敗。接著 set -euo pipefail 會停止所有操作,而不是讓 tar 在損壞的資料庫外建立看似完整的封存檔。
最後的 tar -tzf 會列出實際擷取的內容。第一次執行時請仔細閱讀。您要確認有 ./db.sqlite3、./rsa_key.pem、./config.json 和 ./attachments/,並確認沒有 ./db.sqlite3-wal。如果希望取得 journalctl 輸出,以及能回報失敗狀態的 unit,請使用 systemd service 與 timer 每晚執行,而不要使用 cron。
驗證備份:將其還原至暫存目錄
未經測試的備份只能算是推測。將備份還原至暫存目錄只需幾分鐘,也不會觸碰正式環境。
sudo install -d -m 700 /tmp/vw-check
sudo tar -C /tmp/vw-check -xzf /var/backups/vaultwarden/vw-20260805-030000.tar.gz
ls -l /tmp/vw-check
sudo sqlite3 /tmp/vw-check/db.sqlite3 "PRAGMA integrity_check;"
sudo sqlite3 /tmp/vw-check/db.sqlite3 "select count(*) from users;"
sudo sqlite3 /tmp/vw-check/db.sqlite3 "select count(*) from ciphers;"
sudo du -sh /tmp/vw-check/attachments有 4 項結果需要確認。integrity_check 會列出 ok。使用者數量應符合你所知的帳戶數量。加密資料數量應接近 sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select count(*) from ciphers;" 顯示的正式環境數值;正在使用的 vault 不應為 0。附件目錄的大小應大致符合預期。如果沒有人上傳附件,可以略過這項檢查。接著執行 sudo rm -rf /tmp/vw-check,因為該目錄現在存放了所有資料的第二份副本。
還原任何手動複製的資料目錄時,請遵守一項規則:啟動伺服器前,先刪除 db.sqlite3-wal 和 db.sqlite3-shm。否則 SQLite 會嘗試使用屬於另一份資料庫的日誌來復原還原的資料庫,導致原本完整的資料庫損毀。上述腳本產生的封存檔不會包含這些檔案,因為 .backup 會寫入一份完整的資料庫。
還原至伺服器
請在自己的伺服器上執行以下指令,並先停止容器。資料目錄變更時,Vaultwarden 不得同時寫入資料。
cd /opt/vaultwarden
docker compose stop vaultwarden
sudo mv data data.old.$(date '+%Y%m%d-%H%M%S')
sudo install -d -m 700 data
sudo tar -C data -xzf /var/backups/vaultwarden/vw-20260805-030000.tar.gz
sudo chown -R root:root data
docker compose start vaultwarden
docker compose logs --tail 20 vaultwardenchown 必須指定容器執行時使用的使用者。預設映像檔以 root 執行,因此除非在 compose 檔案中設定 user:,否則使用 root:root 即可。若伺服器無法寫入資料目錄,登入頁面會在每個請求上失敗,日誌也會記錄原因。
正常啟動時,最後會出現 Rocket 行:
[INFO] Rocket has launched from http://0.0.0.0:80接著使用瀏覽器登入、開啟一個項目,並下載一個附件。若登入正常但附件下載失敗,表示封存檔包含資料庫,卻沒有包含 attachments/。在所有項目確認正常前,請保留 data.old.*;確認後再刪除它。回復時使用相同的 3 個步驟,但將兩個目錄對調。
如果路徑與本文不同,VPS 的 Vaultwarden 安裝指南會提供這些指令所使用的 compose 檔案。
備份不應放置的位置
- 不要放在與資料目錄相同的磁碟上。只要一個磁碟區故障,兩份副本都會遺失;在錯誤路徑執行一個
rm -rf也會造成相同結果。 - 不要放在同一台伺服器上,即使使用第二個磁碟區也一樣。攻擊者取得 root 權限後,能在同一個工作階段存取備份。
- 不要存放在未加密的物件儲存中,因為封存檔包含電子郵件地址、密碼提示、復原碼及可遭離線攻擊的 vault ciphertext。
- 不要只依賴供應商的快照。快照能快速還原,值得保留;但快照與伺服器位於同一個帳戶中,因此帳戶發生問題時,快照也會一併受影響。
異地副本適合使用 restic,因為 restic repository 會先在機器上加密,再上傳任何資料。在伺服器上:
sudo apt install -y restic
export RESTIC_REPOSITORY=s3:https://s3.example.com/vaultwarden-backups
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic init
restic backup /var/backups/vaultwarden --tag vaultwarden
restic snapshots --tag vaultwarden
restic forget --tag vaultwarden --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune讓 restic 指向封存目錄,而不是即時資料目錄。如此一來,上傳的內容就是你已經驗證過的一致副本。將 repository password 儲存在受保護伺服器以外的位置:密碼遺失後,快照會依設計無法讀取。若儲存體支援,請只授予伺服器可寫入但不可刪除的憑證,讓伺服器遭入侵時無法清除自己的備份歷程。在 VPS 上設定 restic 備份完整說明 repository 與排程;如果你尚未決定,也可以參考restic 與 BorgBackup 的比較。
依排程測試還原
每月選定一天。使用 restic restore latest --tag vaultwarden --target /tmp/vw-check 將最新的快照拉取至暫存目錄,執行相同的 PRAGMA integrity_check,再執行相同的資料列計數,最後記錄日期與計數結果。連續 6 個月沒有人還原過的備份,其狀態無從確認;等到服務中斷時才確認狀態,是最糟的時機。
每年執行一次完整測試。在備用埠上啟動第二個 Vaultwarden 容器,並掛載已還原的資料目錄,再使用實際帳號登入。這能從頭到尾驗證主密碼流程,單靠資料列計數無法做到。依相同排程執行 restic check --read-data-subset=10%,可確認儲存的資料確實可讀取,而不只是能列出項目。
FAQ
我可以在 Vaultwarden 執行時使用 cp 複製 db.sqlite3 嗎?
不可以。Vaultwarden 以 WAL 模式執行 SQLite,因此最近的寫入內容會留在 db.sqlite3-wal 中,尚未寫入 db.sqlite3。只複製主要檔案的 cp 會讓這些內容無聲遺失;分別複製兩個檔案,也可能留下不一致的檔案組合,之後才顯示為 Error: database disk image is malformed。請改用 sqlite3 /path/db.sqlite3 ".backup '/path/out.sqlite3'"。它使用 SQLite 的 Online Backup API,在伺服器持續提供服務的同時產生一個一致的檔案。
我必須停止 Vaultwarden 容器才能進行備份嗎?
不必,這正是 .backup 的用途。伺服器執行中也能安全複製資料庫。使用者上傳檔案時才會寫入附件與 Send 檔案,因此若檔案在複製資料庫後、執行 tar 前加入,該檔案可能會遺漏在當晚的封存檔中;最壞情況只是遺失一個附件。若幾秒鐘的停機時間可以接受,請在指令碼前執行 docker compose stop,完成後執行 docker compose start,即可避免這種情況。
如果還原時沒有 rsa_key 檔案,會發生什麼事?
Vaultwarden 會在啟動時產生新金鑰。該金鑰會簽署維持工作階段有效的 JSON web tokens (JWT),因此所有現有 token 都會停止通過驗證,所有用戶端都會被登出,必須重新登入。Vault 內容不受影響,因為內容是使用各使用者的主密碼衍生出的金鑰加密,而不是使用 RSA 金鑰加密。將 rsa_key.pem 與其餘資料目錄一併還原,使用者不會察覺這次還原。
備份封存檔可以直接安全地上傳到物件儲存空間嗎?
不可以。項目名稱、密碼與備註都是密文,但電子郵件地址、帳戶名稱、密碼提示及雙因素復原碼會以明文儲存在資料庫中,離線攻擊者也能自行持續破解密文。封存檔離開主機前,請先加密。restic repository 會代為處理這項工作,而 gpg --symmetric --cipher-algo AES256 vw-20260805-030000.tar.gz 會產生一個可交給任何儲存服務的單一加密檔案。
如何在 PostgreSQL 或 MariaDB 上備份 Vaultwarden?
SQLite 的步驟不適用,內建指令會以 The database type is not SQLite. Backups only works for SQLite databases 拒絕執行。請使用原生工具傾印資料庫,例如 pg_dump 或 mysqldump,其餘規則維持不變。傾印檔應與 attachments/、sends/、config.json 及 rsa_key 檔案放在同一個封存檔中,於同一次執行中取得、加密,並儲存在製作該封存檔的伺服器以外的位置。