SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

Vaultwardenは安全?管理者トークンとバックアップを強化

Vaultwardenは項目をクライアント側で暗号化し、サーバーに平文を保存しません。実際のリスクは管理者トークンとバックアップファイルです。設定を確認します。

Vaultwarden は安全か? 結論

Vaultwarden は、最も重要な部分で安全です。各 vault 項目は、サーバーに届く前にデバイス上で暗号化されるためです。サーバーが保存するのは、読み取れないデータの塊です。データベース全体をコピーされても、攻撃者が有用な情報を取り出すには master password が必要です。

ただし、この結論には多くの前提があります。問題が起きるのは、設定した部分です。推測しやすい token で保護した管理パネル。インターネット全体に公開されたコンテナポート。平文の config.json。同じサーバーのホームディレクトリに置かれたバックアップの tarball。これらはいずれも暗号技術の問題ではありません。self-hosted vault の中身が抜き取られる原因は、すべてこのような設定です。

以下では、インストールが完了し、正常に動作していることを前提とします。まだ環境がない場合は、先に VPS への Vaultwarden インストールガイドでセットアップしてください。その後、ここに戻り、この一覧を上から順に確認してください。

サーバーが実際に保存するデータ

Vaultwarden は Bitwarden のデータモデルを実装しています。ボールト項目の名前、ユーザー名、パスワード、メモ、URI は、リクエストを送信する前に、クライアント内でマスターパスワードから導出した鍵を使って暗号化されます。添付ファイルの内容も同じ方法で暗号化されます。サーバーが受け取るのは、UUID(汎用一意識別子)が付加された内容を判別できないデータです。

暗号文にならないデータもあります。どのデータが該当するかを正確に把握してください。

  • アカウントのメールアドレス(平文)。
  • KDF(鍵導出関数)の設定とソルト。次回のログイン時にクライアントが鍵を再構築するために必要です。
  • クライアントが送信するマスターパスワードのハッシュに対する、サーバー側のハッシュ。ログイン認証そのものに使用されます。
  • メタデータ:組織のメンバーシップ、デバイス名、最終ログイン時刻。
  • Vaultwarden ログインを保護する 2 要素認証方式の秘密情報。サーバーが期待されるコードを計算し、入力されたコードと比較する必要があるため、twofactor テーブルに暗号化されていない状態で保存されます。これは、ボールト項目の内部に保存する 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つの共有 Secret だけです。ユーザー名はありません。ユーザーごとの2要素認証もありません。

以前のガイドでは、openssl rand -base64 48を使ってADMIN_TOKENを生成するよう案内しています。これは機能しますが、Secret が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$で始まる行が出力されます。bare-metal 環境では、./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=20

5分以内に3回失敗すると、そのクライアントからの管理パネルへの応答を停止します。管理セッションは、20分間操作がないと期限切れになります。

これらの対策よりもよい方法は、ページ自体を無効にすることです。ほとんどのインスタンスで必要なのは、SMTP を設定して最初のユーザーを招待するときだけです。その後は二度と必要ありません。無効にするには、ADMIN_TOKENもDISABLE_ADMIN_TOKENも設定せず、config.jsonから"admin_token"キーを削除して、コンテナを再作成します。管理ページは設定をそこへ書き込むため、ファイルからキーを削除することが重要です。config.jsonにある値が環境変数より優先されます。変数を削除するだけでは、ページは開いたままです。

ドメインを知られる前に登録を停止する

SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
SHOW_PASSWORD_HINT=false

SIGNUPS_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:80

127.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 をまだ開いている場合は、閉じてください。

Rocket ではなくリバースプロキシで TLS を終端する

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 にその情報を設定します。

DOMAIN=https://vw.example.com
IP_HEADER=X-Real-IP

IP_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 がログファイルへ書き込むよう設定する必要があります。標準状態では書き込みません。

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 には影響しません。ボルトへのオフライン攻撃のコストを決めるのは、この KDF の設定です。KDF は各アカウントに属します。数年前に登録したユーザーは、より古くて低い KDF 設定のままになっている可能性があります。その場合は、Web ボルトのセキュリティ設定で KDF を引き上げる必要があります。これにより、鍵が再暗号化されます。サーバー側で代わりに実行することはできないため、ユーザーにその手順を案内してください。

次に、アカウントごとに2要素認証を有効にします。2要素認証は、ボルトの鍵がマスターパスワードだけから生成されるため、暗号文自体は保護しません。ただし、盗まれたパスワードだけではログインしてコピーを同期できないようにします。REQUIRE_DEVICE_EMAIL=true を有効にすると、新しいデバイスからのログイン時にメール確認が必須になります。Vaultwarden がそのメールを送信できない場合、ログインは失敗します。そのため、認識されていないデバイスからのログインが通知なしに完了することはありません。動作する SMTP が必要です。SMTP が使えない場合、新しいデバイスからのログインはすべて拒否されます。

自己ホスト型 vault でバックアップに失敗する理由

同じ VPS のホームディレクトリにデータフォルダーの tar czf を残すと、ここまでの対策がすべて無効になります。そのアーカイブには、全ユーザーの暗号文を含む db.sqlite3、ログインセッションを偽造できる rsa_key.pem、管理者トークンと SMTP パスワードが平文で入った config.json が含まれます。そのファイルへの読み取りアクセスは、vault への読み取りアクセスと同じです。

守るべきルールは2つです。アーカイブをサーバーの外部へ移します。外部へ移す前に暗号化します。

正確性にも問題があります。サービスの実行中に cp を使って db.sqlite3 をコピーすると、書き込み途中のファイルが生成され、開けなくなることがあります。この問題が判明するのは復元時です。SQLite 自身のスナップショットを使用します。

sqlite3 /vw-data/db.sqlite3 ".backup '/tmp/vw-db-backup.sqlite3'"

誰もテストしないことが多い復元側については、Vaultwarden のバックアップと復元ガイドで説明しています。

ホスト型 Bitwarden と引き換えに失うもの

正直に考える必要があります。Bitwarden のホスト型サービスは、運用を本業とする担当者が運営しています。公開された第三者監査があり、午前3時にも対応できる担当者が待機しています。Self-hosting に切り替えると、その体制は自分自身のパッチ適用サイクルに置き換わります。

Vaultwarden は通常のリリースとしてセキュリティ修正を提供しており、その頻度も高いです。2026年10月7日にプロジェクトのリリースフィードを確認した時点で、現行リリースは 1.37.4 です。これは2026年10月5日に公開されました。リリースノートには7件のセキュリティアドバイザリが記載されており、最も重大なものは High と評価されています。できるだけ早く更新するよう強く推奨されています。これは、2026年7月24日の 1.37.0 以降で4回目のリリースです。1.37.1 は7月29日、1.37.2 は8月22日(version 2026.8.0 以降のクライアントに必要)、1.37.3 は9月13日に公開されました。1.37.3 では、認証情報または 2FA が変更されたときに 2FA の remember token を無効化し、prelogin と auth request のエンドポイントにレート制限を適用します。管理者を完全には信頼できない組織を運用している場合、1.37.4 のリリースノートでは、更新後に各組織の API key もローテーションするよう推奨しています。1年前にセットアップして放置したインスタンスは、1年前のコードで動作しています。latest tag だけでは対策になりません。実行中のコンテナは、docker compose pull を実行して再作成するまで、起動時に使用した image を保持します。ホストパッケージには Ubuntu で unattended upgrades を有効にする を設定し、コンテナの更新は、実際に確認するカレンダーリマインダーに登録してください。

正直な読者が導くべき結論は、暗号方式は Bitwarden の設計に基づいており、十分な強度を保っている一方で、運用上のリスクはすべて自分に移るということです。パッチを適用し、別の場所にもバックアップを保存できるなら、自分で管理する VPS 上の Vaultwarden インスタンスは、パスワードを保管する場所として妥当です。その2つの習慣を実行できないなら、ホスト型サービスの料金を支払い、別の作業に時間を使ってください。機能ごとの比較は Vaultwarden と self-hosted Bitwarden の比較 にあります。

コンテナの基盤となるホストを堅牢化する

Vaultwarden は Linux ホスト上で動作する 1 つのプロセスです。そのホストの root は、アプリケーションの設定に関係なく /vw-data を読み取れます。compose ファイルで user: "1000:1000" を指定し、権限のないユーザーとしてコンテナを実行します。データフォルダーの所有者も一致させます。コンテナが書き込まないものは、:ro を指定して読み取り専用でマウントします。

次に、入口を閉じます。VPS での SSH 堅牢化では、鍵のみのログインとパスワード認証の無効化について説明しています。これにより、ここまでの対策をすべてすり抜ける単純な攻撃を防げます。

FAQ

パスワードを盗まれた Vaultwarden データベースから読み取られる可能性はありますか?

直接読み取られることはありません。すべての vault 項目は、master password から導出した鍵を使ってクライアント側で暗号化されるため、db.sqlite3 に保存されるのは暗号文です。直ちに取得されるのは、各アカウントのメールアドレス、KDF 設定、ログインおよびデバイスのメタデータ、twofactor テーブル内の二要素認証用 Secret です。サーバーが想定コードを計算する必要があるため、二要素認証用 Secret は暗号化されずに保存されます。また、vault の暗号文に対してオフライン攻撃を無期限に実行できます。そのため、結果を決めるのは master password の長さです。

ADMIN_TOKEN を使用するべきですか、それとも管理ページを完全に無効化するべきですか?

可能であれば無効化してください。多くのインスタンスでは、SMTP の設定とユーザーの招待に一度使用すれば、その後は必要ありません。無効化するには、ADMIN_TOKEN と DISABLE_ADMIN_TOKEN のいずれも設定せず、config.json から "admin_token" キーを削除して、コンテナを再作成します。環境変数だけを削除しても不十分です。管理ページで書き込まれた設定は config.json に保存され、こちらが優先されるためです。管理ページを残す場合は、平文のランダム文字列ではなく、vaultwarden hash で生成した Argon2 ハッシュとして token を保存し、ADMIN_RATELIMIT_MAX_BURST=3 を設定してください。

ADMIN_TOKEN は正しいのに /admin で拒否されます。何が問題ですか?

ほとんどの場合、$ の展開が原因です。Argon2 PHC 文字列には複数の $ 文字が含まれます。Docker Compose は docker-compose.yml environment: ブロック内でこれらを変数として展開するため、ファイル上は正しく見えても、コンテナには壊れた値が渡されます。compose ファイル内のすべての $ を $$ に二重化してください。または、値を単一引用符で囲んだ .env ファイルに移してください。この場合、エスケープは不要です。その後、コンテナを再作成してください。再起動だけでは環境変数の変更が反映されません。

通知に port 3012 を引き続き開く必要はありますか?

いいえ。port 3012 の WebSocket トラフィックは、通知がメインの HTTP port に移行したため、Vaultwarden 1.31.0 で削除されました。WEBSOCKET_ENABLED と WEBSOCKET_PORT は 1.29.0 以降無視されています。現在の設定は ENABLE_WEBSOCKET で、デフォルトは true です。ファイアウォールで 3012 を閉じ、compose ファイルからも削除してください。そのうえで、リバースプロキシが Upgrade と Connection ヘッダーを転送することを確認してください。現在のリアルタイム同期はこれらに依存しています。