Vaultwardenは安全ですか?設定の安全対策
Vaultwardenは保管庫アイテムをクライアント側で暗号化しますが、admin tokenとバックアップファイルが実際のリスクです。平文で保存されるデータや対策を確認できます。
Vaultwarden は安全ですか? 短い答え
Vaultwarden は、最も重要な部分では安全です。すべての vault アイテムは、サーバーに到達する前にデバイス上で暗号化されるためです。サーバーに保存されるのは、サーバー自身では読み取れないデータです。データベース全体をコピーされても、攻撃者が有用な情報を取り出すには master password が必要です。
ただし、この答えは多くの前提に依存しており、問題が起きるのは設定した部分です。推測しやすい token で保護された管理パネル。インターネット全体に公開されたコンテナのポート。平文の config.json。同じホストのホームディレクトリに置かれたバックアップの tarball。これらはいずれも暗号化の問題ではありません。self-hosted の vault が空にされる原因は、すべてこのような設定です。
以下では、インストールが正常に完了していることを前提とします。まだインストールしていない場合は、まず VPS 向けの Vaultwarden インストールガイドでセットアップしてください。その後、ここに戻ってこの一覧を上から順に確認してください。
サーバーが実際に保存するデータ
Vaultwarden は Bitwarden のデータモデルを実装しています。保管庫アイテムの名前、ユーザー名、パスワード、メモ、URI は、リクエストを送信する前に、クライアント上でマスターパスワードから導出した鍵によって暗号化されます。添付ファイルの内容も同じ方法で暗号化されます。サーバーが受け取るのは、UUID(汎用一意識別子)が付加された、内容を判別できないデータです。
暗号文ではない情報もあります。具体的には次のとおりです。
- アカウントのメールアドレス(平文)。
- KDF(鍵導出関数)の設定とソルト。次回のログイン時にクライアントが鍵を再構築するために必要です。
- クライアントが送信するマスターパスワードのハッシュに対する、サーバー側のハッシュ。ログイン認証そのものに使用されます。
- メタデータ: 組織への所属、デバイス名、最終ログイン時刻。
- Vaultwarden のログインを保護する 2 要素認証方式の秘密情報。サーバーが期待されるコードを計算し、入力されたコードと比較する必要があるため、
twofactorテーブルに暗号化されずに保存されます。これは、保管庫アイテム内に保存する TOTP(時間ベースのワンタイムパスワード)秘密情報とは異なります。保管庫アイテム内の TOTP は、他のフィールドと同様に暗号化されます。
データフォルダーは小規模です。Docker によるインストールでは、/data にマウントしたディレクトリがデータフォルダーです。
sudo ls -l /vw-data/db.sqlite3 には、ほぼすべての状態が保存されます。attachments/ には UUID ごとにアップロード済みファイルが保存されます。データベーステーブル以外に保存される重要なデータは、これだけです。sends/ には Send の添付ファイルが保存されます。ここは一時的な用途を想定しています。icon_cache/ は破棄可能なデータです。rsa_key.pem とその関連ファイルは、ログイン済みユーザーの JWT(JSON Web Token)に署名します。そのため、この秘密鍵のコピーがあれば、保管庫へのログインセッションを偽造できます。config.json は管理者ページを有効にした場合にのみ作成されます。プロジェクトの説明は明確で、管理者トークンと SMTP 認証情報が平文で保存されます。
したがって、実際の脅威モデルで重要なのはネットワーク暗号ではなく、ファイルシステムへのアクセスです。このディレクトリへの読み取りアクセスを許すと、全ユーザーのメールアドレス、ログイン用の 2 要素認証秘密情報、セッションを偽造できる鍵、そして時間をかけて攻撃できる全保管庫のオフラインコピーが漏えいします。以下の各手順は、第三者がこのディレクトリにアクセスできないようにするためのものです。
管理者トークンを最初に修正する
/admin は、ユーザー一覧、招待、削除、すべての実行時設定を備えた完全な管理パネルです。保護に使われるのは、1 つの共有シークレットだけです。ユーザー名はありません。ユーザーごとの二要素認証もありません。
以前のガイドでは、openssl rand -base64 48 を使って ADMIN_TOKEN を生成するよう案内していました。この方法でも動作しますが、シークレットが config.json と compose ファイルに平文で書き込まれます。Vaultwarden は Argon2 PHC(password hashing competition)文字列も受け付けるため、保存される値をハッシュにできます。実行中のコンテナに対して生成するには、次のコマンドを実行します。
docker exec -it vaultwarden /vaultwarden hash実行中のコンテナに一切触れずに実行する場合は、次のコマンドを使います。
docker run --rm -it vaultwarden/server /vaultwarden hashパスワードを 2 回入力すると、$argon2id$ で始まる行が出力されます。ベアメタル環境では、./vaultwarden hash を実行します。argon2 CLI を直接使う場合は、プロジェクトが OWASP の最小パラメーターを文書化しています。
echo -n 'MySecretPassword' | argon2 "$(openssl rand -base64 32)" -e -id -k 19456 -t 2 -p 1ここで、1 時間を失う原因になりやすい注意点があります。PHC 文字列には $ 文字が多数含まれます。Docker Compose は $ を変数展開として扱います。これをエスケープせずに environment: ブロックへ貼り付けると、コンテナに渡される値が壊れます。その結果、/admin は正しいはずのトークンを拒否します。安全な方法は 2 つあります。docker-compose.yml では、すべての $ を 2 つ重ねます。
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$UUZxK1FZMkZoRHFQRlVrTXZvS0E3bHpNQW55c2dBN2NORzdsa0Nxd1JhND0$$cUoId+JBUsJutlG4rfDZayExfjq4TCt48aBc9qsc3UI.env ファイルではエスケープは不要ですが、シングルクォートを使用します。
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'続いて、管理パネルへのリクエストをレート制限し、セッションを短くします。
ADMIN_RATELIMIT_SECONDS=300
ADMIN_RATELIMIT_MAX_BURST=3
ADMIN_SESSION_LIFETIME=205 分以内に 3 回誤った試行をすると、そのクライアントから管理パネルへ応答しなくなります。何も操作しない状態が 20 分続くと、管理セッションは期限切れになります。
これらよりも優れた方法は、ページ自体を無効にすることです。ほとんどのインスタンスで必要になるのは、SMTP を設定して最初のユーザーを招待するまでの 1 回だけです。その後は不要です。無効にするには、ADMIN_TOKEN と DISABLE_ADMIN_TOKEN のどちらも設定せず、config.json から "admin_token" キーを削除して、コンテナを再作成します。管理ページは設定をそこへ書き込むため、ファイルからキーを削除することが重要です。config.json にある値が環境変数より優先されます。変数を削除するだけでは、ページは開いたままです。
ドメインを知られる前に登録を閉じる
SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
SHOW_PASSWORD_HINT=falseSIGNUPS_ALLOWED はデフォルトで true です。そのままにすると、ドメインへ到達できる人は誰でもアカウントを作成でき、そのデータはあなたのデータと同じ db.sqlite3 に保存されます。false に設定し、SMTP が動作する管理ページから招待によってユーザーを追加します。INVITATIONS_ALLOWED もデフォルトで true であり、組織の所有者が他のユーザーを招待できます。ユーザーを信頼できる場合は問題ありませんが、単一ユーザーのインスタンスでは false にする必要があります。特定のドメインだけを登録対象にする場合、SIGNUPS_DOMAINS_WHITELIST=example.com は公開登録より制限が強いものの、招待制よりはるかに弱い方法です。
SHOW_PASSWORD_HINT はデフォルトで false であり、そのままにしてください。これが有効だと、ログインフォームに有効なメールアドレスを入力したとき、そのアカウントのマスターパスワードのヒントが返されます。これにより、ヒントが漏洩するだけでなく、そのアドレスが存在することも確認されます。
インスタンスで一定期間でも公開登録を有効にしていた場合は、自分だけがアカウントを持っていると思い込む前に、管理ページを開いてユーザー一覧を確認してください。
公開する意図のなかったポート
Docker イメージはコンテナ内でポート 80 を待ち受けます。ベアメタルへのインストールでは、デフォルトで ROCKET_PORT=8000 を使用します。ドキュメントに記載されている実行コマンドでは、次のように公開します。
--publish 127.0.0.1:8000:80127.0.0.1: のプレフィックスが重要です。代わりに -p 8000:80 と記述すると、Docker は 0.0.0.0 をバインドし、nat テーブルに DNAT(宛先ネットワークアドレス変換)ルールを書き込みます。これらのルールは、ufw が管理する filter チェーンより前に評価されます。そのため、ufw status はポートを拒否として報告する一方で、実際にはそのポートがインターネットから応答します。仕組みの詳細については、Docker のポートが ufw を迂回する仕組みのガイドを参照してください。
実際に待ち受けているポートを確認します。
sudo ss -tlnp | grep 8000正常な結果は、127.0.0.1:8000 にバインドされた 1 行だけです。0.0.0.0:8000 にバインドされた行がある場合、vault が直接公開されています。マッピングを修正してからコンテナを再作成してください。ポートバインディングはコンテナ作成時に固定されるため、docker compose restart では変更されません。
docker compose up -d --force-recreate古いガイドには、もう 1 つポートが記載されています。分離された WebSocket ポートの 3012 です。通知トラフィックがメインの HTTP ポートに移行したため、Vaultwarden 1.31.0 でこのポートのサポートは削除されました。WEBSOCKET_ENABLED と WEBSOCKET_PORT は 1.29.0 以降無視されています。現在使用するスイッチは ENABLE_WEBSOCKET で、デフォルト値は true です。ファイアウォールまたは compose ファイルで 3012 がまだ開かれている場合は、閉じてください。
リバースプロキシで TLS を終端し、Rocket では終端しない
Vaultwarden は Web フレームワークの Rocket を通じて、自身で TLS(トランスポート層セキュリティ)を提供できます。ただし、プロジェクトは本番環境でこの構成を使用しないよう案内しています。Rocket 組み込みの TLS は厳密な SNI(Server Name Indication)に対応していません。そのため、セキュリティ強化の指針でも、インスタンスにはホスト名でアクセスし、ベア IP アドレスでは決してアクセスしないよう求めています。パブリック IP の範囲は常時スキャンされています。IP アドレスで応答する vault は、発見される vault です。
nginx の server block で重要な部分は次のとおりです。
client_max_body_size 525M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}nginx の client_max_body_size のデフォルト値は 1 MB です。そのため、この行がないと添付ファイルのアップロードは失敗します。この場合、nginx のエラーログには 413 Request Entity Too Large が記録されますが、Vaultwarden のログには何も記録されません。Upgrade ヘッダーと Connection ヘッダーは、WebSocket のハンドシェイクを /notifications/hub に渡します。これらを削除しても vault 自体は動作しますが、ページを手動で再読み込みするまで、他のデバイスに変更が反映されなくなります。
Caddy では設定が短く、証明書も自動的に取得します。
vw.example.com {
reverse_proxy 127.0.0.1:8000 {
header_up X-Real-IP {remote_host}
}
}続いて、Vaultwarden にその URL を指定します。
DOMAIN=https://vw.example.com
IP_HEADER=X-Real-IPIP_HEADER のデフォルト値は X-Real-IP です。そのため、プロキシが実際にこのヘッダーを設定していることを確認します。設定されていない場合、すべてのログ行とログインのレート制限で送信元が 127.0.0.1、つまりプロキシ自体として認識されます。その結果、1 人の攻撃者による失敗が、インスタンス上のすべてのユーザーに対してカウントされます。DOMAIN には実際の https URL も設定してください。Vaultwarden はこの値から招待リンクとパスワードリセットリンクを生成し、WebAuthn セキュリティキーもこのオリジンに紐付けられます。
見落とされがちな点が 1 つあります。WebSocket 接続は、/notifications/hub?access_token=[JWT] としてセッショントークンをクエリ文字列に渡します。この値は、プロキシのアクセスログに平文で記録されます。ログ形式で access_token パラメーターを伏せ字にするか、管理下にない場所へそのログが送信されないようにしてください。
ログインエンドポイントでブルートフォース攻撃を防止する
レート制限はデフォルトで有効です(LOGIN_RATELIMIT_SECONDS=60、LOGIN_RATELIMIT_MAX_BURST=10)。攻撃者の試行速度を低下させますが、攻撃そのものを止めることはできません。fail2ban なら停止できます。ただし、その前に Vaultwarden がログファイルへ書き込む必要があります。Vaultwarden は標準ではこれを行いません。
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info
EXTENDED_LOGGING=trueログインに失敗すると、次の1行だけが生成されます。フィルターではこの文字列に一致させます。
[2026-08-07 09:14:22][vaultwarden::api::identity][ERROR] Username or password is incorrect. Try again. IP: 203.0.113.10. Username: user@example.com.フィルターを /etc/fail2ban/filter.d/vaultwarden.local に作成します。
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =jail を /etc/fail2ban/jail.d/vaultwarden.local に作成します。
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
banaction = %(banaction_allports)s
logpath = /vw-data/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400管理者ページを残している場合は、failregex が ^.*Invalid admin token\. IP: <ADDR>.*$ になる2つ目の jail を追加します。管理者ログインの失敗は異なるメッセージで記録されるため、ログイン用フィルターでは検出できません。続いて設定を確認します。
sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwarden正常に動作している jail では、File list にログファイルが表示され、Currently failed: 0 が報告されます。別のネットワークから間違ったパスワードを3回入力すると、そのカウンターが増加し、続いてアドレスが Banned IP list に表示されます。カウンターがまったく増えない場合、通常の原因は logpath です。ここにはコンテナ内の /data/... パスではなく、ホスト上のファイルパスを指定する必要があります。次に多い原因は X-Real-IP の欠落です。この場合、すべての ban の対象が自分のプロキシになります。すでに実行しているはずの SSH jail を含む残りの設定については、Ubuntu 24.04 向けの fail2ban ガイドを参照してください。
マスターパスワードが依然としてシステム全体を左右します
クライアント側暗号化では、マスターパスワードが鍵になります。データベースを攻撃者にコピーされたインスタンスで短いマスターパスワードを使っている場合、この投稿にある設定では保護できません。攻撃者はコピーしたデータに対して、使用するハードウェアが許す速度でオフライン攻撃を実行できるためです。サーバーの設定は、攻撃者自身のマシンには影響しません。
PASSWORD_ITERATIONS=600000 は、新しいアカウントの作成時にクライアントへ渡される KDF の反復回数です。既存のアカウントでは、作成時の値が維持されます。そのため、昨年サインアップしたユーザーの値は、これを引き上げても変わりません。ユーザー自身が Web vault のセキュリティ設定で変更する必要があります。変更すると鍵が再暗号化されます。この点はユーザーに伝えてください。インターフェースには案内が表示されないためです。
次に、アカウントごとに二要素認証を有効にします。保管された暗号文は保護されません。vault の鍵はマスターパスワードだけから生成されるためです。ただし、盗まれたパスワードだけではログインしてコピーを同期できないようにできます。REQUIRE_DEVICE_EMAIL=true を有効にすると、アカウントが認識されていないデバイスから初めてログインしたときに、メール確認が追加されます。
セルフホストの Vault で問題が起きるのはバックアップです
同じ VPS のホームディレクトリに保存したデータフォルダーの tar czf は、ここまでの対策をすべて無効にします。そのアーカイブには、各ユーザーの暗号文が含まれる db.sqlite3、ログインセッションを偽造できる rsa_key.pem、管理者トークンと SMTP パスワードが平文で含まれる config.json が入っています。その 1 ファイルへの読み取りアクセスは、Vault への読み取りアクセスと同じです。
必要なルールは 2 つです。アーカイブをサーバーの外部に移します。外部へ移す前に暗号化します。
正確性に関する問題もあります。サービスの実行中に cp を使って db.sqlite3 をコピーすると、書き込み途中のファイルが生成されることがあります。そのファイルは開けませんが、復元するまで問題に気付けません。SQLite 自身のスナップショットを使用してください。
sqlite3 /vw-data/db.sqlite3 ".backup '/tmp/vw-db-backup.sqlite3'"誰もテストしない復元側については、Vaultwarden のバックアップと復元ガイドで説明しています。
ホスト型 Bitwarden と引き換えに失うもの
率直な評価が必要です。Bitwarden のホスト型サービスは、運用を本業とする担当者が管理しています。公開された第三者監査があり、午前3時にも対応できる当番担当者がいます。セルフホスティングでは、その体制が自分自身のパッチ適用サイクルに置き換わります。
Vaultwarden は、通常のリリースとしてセキュリティ修正を提供します。2026年7月24日にリリースされた Version 1.37.0 が、2026年8月時点の最新バージョンです。リリースノートでは、できるだけ早く更新するよう利用者に求めています。1年前にセットアップして、そのまま忘れていたインスタンスは、1年前のコードを実行しています。latest タグだけでは不十分です。実行中のコンテナは、docker compose pull を実行して再作成するまで、起動時に使用したイメージを保持します。ホストパッケージには Ubuntu で unattended upgrades を有効にする を設定し、コンテナの更新は、実際に確認するカレンダーのリマインダーに登録してください。
率直な読者が導くべき結論は、ここで使われる暗号技術は Bitwarden の設計に基づいており、堅牢である一方、運用上のリスクはすべて自分に移るということです。パッチを適用し、別の場所にもバックアップを保存できるなら、自分で管理する VPS 上の Vaultwarden インスタンスは、パスワードを保管する場所として妥当です。この2つの習慣を実行できないのであれば、ホスト型サービスに料金を支払い、その分の注意を別の作業に使ってください。機能ごとの比較は、Vaultwarden とセルフホスト型 Bitwarden の比較 にあります。
コンテナの基盤となるホストを堅牢化する
Vaultwarden は Linux ホスト上で動作する 1 つのプロセスです。ホストの root は、アプリケーションの設定にかかわらず /vw-data を読み取れます。compose ファイルで user: "1000:1000" を指定してコンテナを非特権ユーザーとして実行し、データフォルダーの所有者も合わせます。コンテナが書き込まないものは :ro を付けて読み取り専用でマウントします。次に、外部からの入口を閉じます。VPS の SSH 堅牢化では、鍵のみのログインとパスワード認証の無効化について説明しています。これにより、上記の対策をすべて回避する単純な攻撃を防げます。
FAQ
パスワード保管庫のデータベースを盗まれた場合、パスワードを読まれる可能性はありますか?
直接読み取られることはありません。すべての保管庫項目は、マスターパスワードから導出した鍵を使ってクライアント側で暗号化されるため、db.sqlite3 に含まれるのは暗号文です。直ちに取得されるのは、各アカウントのメールアドレス、KDF の設定、ログインおよびデバイスのメタデータ、さらに twofactor テーブルにある二要素認証の Secret です。二要素認証の Secret は、サーバーが期待されるコードを計算する必要があるため、暗号化されずに保存されます。また、保管庫の暗号文に対して、時間の制限なくオフライン攻撃を実行できます。そのため、結果を左右するのはマスターパスワードの長さです。
ADMIN_TOKEN を使用すべきですか。それとも管理ページを完全に無効化すべきですか?
可能であれば無効化してください。ほとんどのインスタンスでは、SMTP の設定とユーザーの招待に一度使用するだけで、その後は必要にならないためです。無効化するには、ADMIN_TOKEN と DISABLE_ADMIN_TOKEN のどちらも設定せず、config.json から "admin_token" キーを削除して、コンテナを再作成します。環境変数だけを削除しても不十分です。管理ページで書き込まれた設定は config.json に保存され、こちらが優先されるためです。管理ページを有効にしておく場合は、平文のランダム文字列ではなく、vaultwarden hash で生成した Argon2 ハッシュとしてトークンを保存し、ADMIN_RATELIMIT_MAX_BURST=3 を設定してください。
ADMIN_TOKEN は正しいのに /admin で拒否されます。原因は何ですか?
ほとんどの場合、$ の展開が原因です。Argon2 PHC 文字列には複数の $ 文字が含まれます。Docker Compose は、docker-compose.yml environment: ブロック内でこれらを変数として展開するため、ファイル上の値が正しく見えても、コンテナには壊れた値が渡されます。compose ファイルでは各 $ を $$ に二重化してください。または、値を単一引用符で囲んだ .env ファイルに移してください。この場合、エスケープは必要ありません。その後、コンテナを再作成してください。環境変数の変更は再起動だけでは反映されません。
通知のために、まだポート 3012 を開く必要がありますか?
いいえ。Vaultwarden 1.31.0 で、ポート 3012 の WebSocket トラフィックのサポートは削除されました。通知がメインの HTTP ポートに移行したためです。WEBSOCKET_ENABLED と WEBSOCKET_PORT は 1.29.0 以降無視されています。現在の設定は ENABLE_WEBSOCKET で、デフォルトでは true です。ファイアウォールで 3012 を閉じ、compose ファイルからも削除してください。そのうえで、リバースプロキシが Upgrade ヘッダーと Connection ヘッダーを転送することを確認してください。現在、リアルタイム同期が実際に依存しているのはこれらのヘッダーです。