VaultwardenのVPSバックアップと復元方法
Vaultwardenのdataフォルダーを安全にバックアップする方法を解説します。sqlite3 .backupで稼働中のSQLiteをコピーし、attachments、config.json、rsa_keyファイルを含めて復元まで検証します。
Vaultwarden のバックアップに含めるもの
Vaultwarden のバックアップは、data フォルダー全体のコピーです。その中のデータベースは、正しい方法でコピーする必要があります。書き込み中のデータベースを単純にコピーすると、開けないファイルになることがあるため、cp ではなく sqlite3 db.sqlite3 ".backup out.sqlite3" を実行します。そのうえで、データベースと同じ場所にあるファイルも保持します。このファイル群は、バックアップから漏れやすい部分です。
Docker でインストールしている場合、data フォルダーは /data にマウントした場所です。ホスト上のパスの場合もあれば、名前付きボリュームの場合もあります。bind mount と名前付きボリュームの違いによって、vault が実際に保存されるディスク上の場所が決まります。data フォルダーには次のものが含まれます。
db.sqlite3: すべてのアカウント、vault item、フォルダー、organization。これを失うと vault を失います。db.sqlite3-walとdb.sqlite3-shm: write-ahead log(WAL)と、その共有メモリインデックス。SQLite が最近の書き込みをメインファイルに統合するまで、ここに保存されます。attachments/: vault item にユーザーが添付したファイル。暗号化され、item ごとに 1 つのディレクトリに保存されます。sends/: Bitwarden Send のリンク先にあるファイル。config.json: admin page で保存したすべての設定。rsa_key.pem、古いインストールではrsa_key.derとrsa_key.pub.derも含む: ログイントークンの署名に使う key。icon_cache/: ダウンロードした Web サイトのアイコン。Vaultwarden が必要に応じて再取得するため、このディレクトリだけはバックアップから除外できます。
Vaultwarden のデータベースは安全ですか?ファイルに実際に保存される内容
この点は、2 つのコマンドですぐに確認できます。
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;"1 つ目のコマンドは、ユーザーのメールアドレスを平文で表示します。2 つ目のコマンドは、アイテム名を 1 件表示します。表示例は次のとおりです。
2.k9Qw1nQ0y7Yy2Xw==|E1r0J3l5s7d9f1g3h5j7k9==|Lm4nOp6qRs8tUv0wXy2zAb4cDe6fGh8i=アイテム名、ユーザー名、パスワード、メモは、クライアントで暗号化してから送信されます。そのため、サーバーに保存されるのは、サーバーが読み取れない暗号文です。2. プレフィックスは Bitwarden の暗号化方式を示します。その後に、初期化ベクトル (IV)、暗号文、MAC (メッセージ認証コード) が続きます。各要素は base64 でエンコードされ、| で区切られます。復号に使う鍵はアカウントの master password から導出されます。master password が、利用可能な形式でサーバーに送られることはありません。Vaultwarden を実行する場合も公式サーバーを実行する場合も、この部分は同じです。詳しくは、Vaultwarden と self-hosted Bitwarden の比較で説明します。
データベースの残りの部分は暗号化されていません。メールアドレス、アカウント名、パスワードのヒント、2 要素認証のリカバリーコードは平文で保存されます。作成日時や、アイテムを所有する組織などのメタデータも同様です。そのため、バックアップファイル自体が秘密情報になります。これを入手した者は、ユーザーが誰であるかを把握できます。また、ハードウェアが許す速度で、暗号化されたデータをオフラインで攻撃できます。この事実が、後述するストレージのルールを決めます。コピーはサーバーから持ち出す前に暗号化します。admin token も同じ問題に関係します。self-hosted Vaultwarden の hardening 手順では、この 2 つをまとめて対策します。
Vaultwarden の稼働中に db.sqlite3 をコピーしてもバックアップにならない理由
Vaultwarden はデフォルトで SQLite を WAL モードで実行します(ENABLE_DB_WAL=true)。書き込みはまず db.sqlite3-wal に行われ、チェックポイント処理によってのみ db.sqlite3 に反映されます。db.sqlite3 だけをコピーすると、よくても最後のチェックポイント時点のデータベースしか取得できません。そのため、10 分前に保存したパスワードが、警告なしにアーカイブから欠落する可能性があります。最悪の場合、コピー中にチェックポイント処理が行われ、途中の状態を取得します。SQLite の公式ドキュメントも、WAL から分離されたデータベースでは、コミット済みトランザクションが失われたり、データベースが破損したりする可能性があると警告しています。
cp を使用して 3 つのファイルをすべてコピーしても解決にはなりません。コピーはそれぞれわずかに異なる時点で取得されるため、保存した WAL が記録するページのバージョンと、保存したメインファイルの内容が一致しない可能性があります。SQLite は一方のファイルからもう一方を復旧しますが、結果は正しくありません。問題に気付くのは、かなり後になってからです。
Error: database disk image is malformed.backup は SQLite の Online Backup API を使用するため、この問題を回避できます。SQLite の公式ドキュメントでも、稼働中のデータベースをコピーする方法として説明されています。.backup は読み取りロック中にページを読み取り、書き込みによって処理中のファイルが変更された場合は最初からやり直します。そのため、ディスクに保存されるのは一貫した単一時点の状態です。
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代わりに、マウントされたパスに対してホスト上で実行してください。上のコマンドはその方法を使用しています。データが名前付きボリュームにある場合、docker volume inspect <name> で /var/lib/docker/volumes/ 配下のホストパスを表示できます。
Vaultwarden には、version 1.32.1 以降、独自のバックアップコマンドも用意されています。サーバー上で次を実行します。
docker exec -it vaultwarden /vaultwarden backupこのコマンドは VACUUM INTO を実行し、db_YYYYMMDD_HHMMSS.sqlite3 をデータフォルダーに書き込みます。ここから2点が分かります。コピーは同じディスク上で元のファイルの隣に作成されるため、これはステージング処理であり、まだバックアップではありません。また、SQLite にのみ対応しています。MariaDB または PostgreSQL では The database type is not SQLite. Backups only works for SQLite databases で停止します。
忘れられがちなファイル
attachments/ には、判別しにくい名前で暗号文が保存されています。各添付ファイルに対応するデータベースの行には、暗号化されたファイル名と、クライアントがファイルを復号するために必要な鍵素材が格納されています。データベースがなければ添付ファイルは解読できないデータになり、添付ファイルがなければデータベース内の項目をダウンロードできません。同じ実行で両方を取得してください。
config.json には、管理画面から保存したすべての設定が格納され、その値が対応する環境変数より優先されます。これは利点にも問題にもなります。古い config.json を復元すると、compose ファイルの設定が気付かないうちに上書きされます。また、このファイルには SMTP パスワードや管理者トークンが含まれる可能性があるため、機密情報として扱う必要があります。そのトークンは平文ではなく、Argon2id PHC (password hashing competition) 文字列として保存してください。docker run --rm -it vaultwarden/server /vaultwarden hash で生成できます。
rsa_key.pem は、クライアントのログイン状態を維持する JSON web token (JWT) に署名します。起動時にこのファイルがない場合、Vaultwarden は新しい鍵を生成します。そのため、古い鍵で署名されたすべてのトークンが検証に失敗し、すべてのクライアントがログアウトされます。Vault の内容はマスターパスワードから導出した鍵で暗号化されているため、この影響を受けずに残ります。鍵ファイルを復元すれば、一斉ログアウトを回避できます。
sends/ には、Send リンクの背後にあるファイルが格納されています。これがないと該当するダウンロードだけが失敗し、それ以外には影響しません。
すべてを 1 つのスクリプトにまとめる
#!/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 の行は実際の処理を行います。sqlite3 は PRAGMA integrity_check が破損を報告しても 0 で終了するため、出力を ok と比較することで、不正なコピーをスクリプトの失敗として扱えます。続いて set -euo pipefail がすべての処理を停止します。これにより、tar が破損したデータベースを含む見かけ上正常なアーカイブを作成するのを防ぎます。
最後の tar -tzf で、実際に取得した内容を一覧表示します。最初は出力を確認してください。./db.sqlite3、./rsa_key.pem、./attachments/ を探します。管理画面から設定を保存したことがある場合は ./config.json も確認し、./db.sqlite3-wal が存在しないことも確認します。Vaultwarden はそこで設定を保存した場合にのみ config.json を作成します。そのため、環境変数だけで構成したインストールには config.json がなく、スクリプトもそれをスキップします。journalctl の出力と失敗を報告する unit が必要であれば、cron ではなく systemd service と timer を使って毎晩実行します。
バックアップを一時ディレクトリに復元して検証する
テストしていないバックアップは推測にすぎません。一時ディレクトリに復元すれば、1 分で確認でき、稼働中の環境には何も影響しません。
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;" から確認できる稼働中の値に近いはずです。0 が正しいのは、稼働中の値も 0 の場合だけです。使用中の vault に項目がないこともあり、たとえば組織の vault を削除した直後などが該当します。稼働中の数が 0 ではないのに復元結果が 0 なら、コピーに問題があります。添付ファイルのディレクトリは、想定したサイズとおおむね一致します。誰も添付ファイルをアップロードしない場合は、この確認を省略できます。続いて sudo rm -rf /tmp/vw-check を実行します。そのディレクトリには、すべてのファイルの 2 つ目のコピーが存在するためです。
.backup または VACUUM INTO で作成したデータベースを復元する場合の重要なルールがあります。組み込みのバックアップコマンドもこれに含まれます。サーバーを起動する前に、データフォルダー内の db.sqlite3-wal と db.sqlite3-shm を削除してください。削除しないと、SQLite は別のコピーに属するログを使って復元したデータベースを復旧しようとします。その結果、完全な状態で復元されたデータベースが破損する可能性があります。単純なファイルコピーは例外です。db.sqlite3 を対応する db.sqlite3-wal と一緒にコピーした場合は、2 つをセットで復元してください。その WAL には、メインファイルにまだ反映されていないコミット済みの書き込みが含まれることがあり、db.sqlite3-wal を削除するとそれらが失われるためです。-shm ファイルを復元する必要はありません。上記のスクリプトで作成したアーカイブに WAL が含まれないのは、.backup が完全なデータベースを 1 つ書き込むためです。
サーバーへの復元
これらのコマンドは、コンテナを停止した状態で、自分のサーバー上で実行します。データフォルダーの内容を変更している間、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 で問題ありません。user: を設定している場合は、その uid と gid を使用します。サーバーから書き込めないデータフォルダーを指定すると、ログインページへのすべてのリクエストが失敗します。ログにもその原因が記録されます。
正常に起動すると、最後に次の Rocket 行が表示されます。
[INFO] Rocket has launched from http://0.0.0.0:80その後、ブラウザーからログインし、アイテムを開いて添付ファイルを1つダウンロードします。ログインは成功するのに添付ファイルのダウンロードだけが失敗する場合、アーカイブにはデータベースは含まれていても attachments/ が含まれていません。すべての確認が終わるまで data.old.* を保持し、完了後に削除します。ロールバックも同じ3手順で、ディレクトリの指定を逆にして実行します。
パスがここで示しているものと異なる場合は、VPS向けの Vaultwarden インストールガイドに、これらのコマンドが前提とする compose ファイルを掲載しています。
バックアップを置いてはいけない場所
- データディレクトリと同じディスク上には置かないでください。1 つのボリューム障害で両方のコピーを失います。誤ったパスに対する 1 つの
rm -rfでも同じ結果になります。 - 同じサーバー上には置かないでください。2 つ目のボリューム上でも同様です。攻撃者に root へのアクセスを許すと、同じセッションでバックアップにも到達されます。
- 暗号化せずにオブジェクトストレージへ置かないでください。アーカイブにはメールアドレス、パスワードのヒント、リカバリコード、vault の暗号文が含まれており、オフラインで攻撃される可能性があります。
- プロバイダーのスナップショットだけに依存しないでください。高速に復元できるため用意する価値はありますが、サーバーと同じアカウント内にあるため、アカウントの問題が発生するとスナップショットも失われます。
オフサイトコピーには restic が適しています。restic は、アップロード前にマシン上でリポジトリを暗号化するためです。サーバー上で次を実行します。
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 --prunerestic の対象にはライブデータフォルダーではなくアーカイブディレクトリを指定してください。これにより、すでに確認した一貫性のあるコピーがアップロードされます。リポジトリのパスワードは、保護対象のサーバー以外の場所に保管してください。そのパスワードを失うと、設計上、スナップショットを読み取れなくなります。ストレージが対応している場合は、書き込みはできても削除はできない認証情報をサーバーに付与してください。これにより、サーバーが侵害されても、自身のバックアップ履歴を消去されにくくなります。VPS で restic バックアップを設定する方法では、リポジトリとスケジュールを詳しく説明しています。まだ選択していない場合は、restic と BorgBackup の比較で選択基準を確認できます。
スケジュールに従ってリストアをテストする
月に 1 日を選びます。最新のスナップショットを restic restore latest --tag vaultwarden --target /tmp/vw-check で一時ディレクトリに取得し、同じ PRAGMA integrity_check を実行します。同じ行数も確認し、日付と件数を記録します。6 か月間リストアしていないバックアップは、状態が不明なバックアップです。障害発生時に初めてその状態を確認することになり、最悪のタイミングでの確認になります。
年に 1 回は、完全な手順を実行します。リストアしたデータフォルダーを使い、空いているポートで 2 つ目の Vaultwarden コンテナを起動して、実際のアカウントでログインします。これにより、マスターパスワードの処理経路を最初から最後まで検証できます。行数の確認だけでは検証できません。同じスケジュールで restic check --read-data-subset=10% も実行すると、保存されたデータが単に一覧表示できるだけでなく、読み取り可能であることを確認できます。
FAQ
実行中の Vaultwarden で cp を使って db.sqlite3 をコピーできますか?
いいえ。Vaultwarden は SQLite を WAL モードで実行するため、直近の書き込みは db.sqlite3-wal に残っており、まだ db.sqlite3 には反映されていません。メインファイルだけを cp すると、書き込み内容が通知なく失われます。また、2 つのファイルを別々にコピーすると、後から Error: database disk image is malformed として現れる不整合な組み合わせになる可能性があります。代わりに sqlite3 /path/db.sqlite3 ".backup '/path/out.sqlite3'" を使用してください。これは SQLite の Online Backup API を使用し、サーバーがサービスを継続している間に整合性のある単一ファイルを作成します。
バックアップを取得するために Vaultwarden コンテナを停止する必要がありますか?
いいえ。それが .backup の目的です。実行中のサーバーからデータベースをコピーしても安全です。Attachments と Send files はユーザーがアップロードした時点で書き込まれるため、データベースのコピーと tar の間に追加されたファイルは、その夜のアーカイブに含まれない可能性があります。その場合の損失は、最大で 1 つの添付ファイルです。数秒間の停止が問題にならない場合は、スクリプトの前に docker compose stop、後に docker compose start を実行すると、その可能性もなくせます。
rsa_key ファイルなしで復元するとどうなりますか?
Vaultwarden は起動時に新しい鍵を生成します。この鍵は、セッションを維持する JSON web tokens (JWT) に署名するため、既存のすべてのトークンが検証できなくなり、すべてのクライアントがログアウトされて再度サインインする必要があります。Vault の内容には影響ありません。Vault の内容は RSA 鍵ではなく、各ユーザーのマスターパスワードから導出された鍵で暗号化されているためです。rsa_key.pem をデータフォルダーの他の内容と一緒に復元すれば、復元したことに気付かれません。
バックアップアーカイブをそのままオブジェクトストレージにアップロードしても安全ですか?
いいえ。アイテム名、パスワード、メモは暗号文ですが、メールアドレス、アカウント名、パスワードヒント、2 要素認証のリカバリーコードはデータベース内に平文で保存されています。また、オフライン攻撃者は自分のペースで暗号文の解析を試行できます。マシンから持ち出す前にアーカイブを暗号化してください。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 ファイルと同じアーカイブに含めます。これらは同じ実行で取得し、暗号化したうえで、作成元のサーバー以外の場所に保存してください。