tenant-pass

脅威モデル

tenant-pass が何を誰から守るのか、そして何を守らないのかをまとめています。守る資産、信頼の前提、乗っ取られたアプリケーションに対する効果、ゲートウェイについて残るリスク、鍵の漏洩・時計の操作・サービス妨害への考え方、はっきり対象外としていることを説明します。

このページの見出し
  1. 守る資産
  2. 信頼の前提
  3. いちばん大きな脅威: 乗っ取られたアプリケーション
  4. ゲートウェイについて残るリスク
  5. そのほかの脅威
  6. はっきり対象外としていること

tenant-pass が何を誰から守るのか、そして何を守らないのかをまとめたページです。後半の「はっきり対象外としていること」も含めて、デプロイする前に読んでおいてください。

守る資産

資産なぜ守るか
ストア(PostgreSQL、MySQL、Redis)の中のテナントのデータ最終的に守りたいものそのもの
テナントの認証情報(ウィンドウごとのパスワード)テナントのデータに直接ログインできる
ローテーションキー持っていれば、この鍵から導出するすべてのパスワードを計算できる
ストアの管理者の認証情報tenant-pass がパスワードを設定するのに使うアカウント
ゲートウェイの署名鍵持っていればパスを発行できる
アプリトークンとクライアント証明書tenant-pass の API を呼ぶための認証

信頼の前提

部品前提
tenant-pass のホストとプロセス信頼します。ローテーションキーとストアの管理者の認証情報を持っています。
ゲートウェイ自分でテナントを確かめたリクエストに だけ パスを署名する、と信頼します。
ディレクトリテナントを正しいアカウントに対応させている、と信頼します(間違っていた場合の被害は、ストアの安全性チェックで抑えます)。
ストアパスワードと有効期限のとおりにログインを判定する、と信頼します。
ネットワーク信頼しません。すべての接続で TLS を使い、証明書とホスト名を検証します。
アプリケーション信頼しません。 乗っ取られたアプリケーションにできることを限るのが、設計の目標です。
時計NTP でおおむね合っていることを前提にします。ずれは renew_before より十分小さくなければなりません。

いちばん大きな脅威: 乗っ取られたアプリケーション

リモートコード実行の脆弱性、悪意のある依存パッケージ、盗まれたデプロイ用の認証情報。きっかけは何であれ、攻撃者がアプリケーションのプロセスの中でコードを動かせるようになった場合を考えます。

tenant-pass を使っていなければ、アプリケーションはふつう、すべてのテナントの認証情報(またはそれを復号する鍵)を持っています。攻撃者は、すべてのテナントのデータをすぐに読めます。

tenant-pass を使っていれば、攻撃者が手に入れるのはアプリトークンです。それでできることは、次のように限られます。

攻撃結果
好きなテナントの認証情報を要求する失敗します。パスが必要で、パスに署名できるのはゲートウェイだけです。
パスを偽造する失敗します。アプリケーションはゲートウェイの秘密鍵を持っていません。
別のアプリケーション向けのパスを手に入れて使う失敗します。aud が、認証したアプリケーションと一致しなければならないからです。
アプリケーションが正しく受け取ったパスを使い回す成功しますが、そのパスのテナントだけで、exp まで(数秒)しか使えません。
同じアプリケーションのほかのワーカーが受け取った認証情報を読むそのプロセスのメモリに有効な認証情報があるテナント、つまりいま処理しているテナントに限って成功します。認証情報は 1 ウィンドウ以内に期限が切れます。
保存されているテナントのパスワードを読む保存されているものはありません。
ゲートウェイに、多くのテナントのリクエストを自分へ転送させる下の「ゲートウェイについて残るリスク」をご覧ください。1 分間に扱うテナントの数の制限で、上限を抑えます。tenant-pass-gateway を使う場合は、テナントごとに有効なユーザーのセッションがなければできません。
パスを、有効期間の長いジョブチケットに替えるアプリケーションが jobs.issue を持つ場合だけできます。正しく受け取ったパスのテナントに限られ、期間も jobs.max_ttl までです。
自分でジョブチケットに署名するできません。発行者の鍵を持っているのは、tenant-pass と、別に運用しているスケジューラーだけです。
tenant-pass に管理者のアカウントを対象にさせるアプリケーションからはできません。対象のアカウントを決めるのはディレクトリで、ストアは特権を持つアカウントや管理対象ではないアカウントを拒否します。

アプリケーションが乗っ取られても、すべてのテナントが一度に漏れることはありません。漏れるのは、攻撃者が入り込んでいる間に実際に使われたテナントだけで、その速さも limits.max_tenants_per_minute で抑えられます。監査ログの量や rate_limited のイベントから異常に気付ければ、対応する時間が稼げます。

ゲートウェイについて残るリスク

パスが言っているのは「ゲートウェイが、このテナントのリクエストをこのアプリケーションに転送した」ということだけです。攻撃者がゲートウェイをだまして、好きなテナントのリクエストを乗っ取ったアプリケーションに転送させられれば、そのテナントの有効なパスが手に入ります。外部向けのゲートウェイに Host: victim.example.com を送る、というのが典型です。

ですから、ゲートウェイがパスに署名してよいのは、そのテナントのエンドユーザーのセッション(またはジョブのための署名付きの許可)を確かめた後だけです。ログインページ、ヘルスチェック、公開のファイル、Webhook のような認証のないエンドポイントには、パスを渡してはいけません(詳しくは ゲートウェイ連携(独自のゲートウェイ))。

アプリケーションが内部のネットワークからゲートウェイに接続できる状態も危険です。好きなホスト名を付けて、自分でゲートウェイを呼べてしまうからです。ネットワークの設定で遮断してください。こうした悪用の速さは、1 分間に扱うテナントの数の制限で抑えられます。

もしゲートウェイが認証されていない通信にもパスを署名してしまうと、tenant-pass の効果は「アプリケーションは、ホスト名を知っているどのテナントの認証情報でも、限られた速さで取得できる」ところまで下がります。パスワードを保存したテーブルよりはまだましですが、本来目指している守りではありません。

そのほかの脅威

tenant-pass が乗っ取られた場合

tenant-pass はローテーションキーとストアの管理者の認証情報を持っているので、乗っ取られればすべてのテナントが漏れます。被害の大きさは tenant-pass を使わない場合と同じです。違うのは、リスクがすべてのアプリケーションに散らばらず、1 つの小さなサービスに集まることです。そのサービスを固く守るために、次のようにしてください。

  • 専用の最小限のホストかコンテナで動かします(distroless のイメージ、root 以外のユーザー、読み取り専用のルートファイルシステム、シェルなし)。
  • ストアの管理者のアカウントには、各ストアのページに書いた必要最小限の権限だけを与えます。
  • tenant-pass にネットワークから接続できるのは、必要なアプリケーションだけにします。
  • ローテーションキーは、tenant-pass にシークレットとしてマウントしたファイルにだけ置きます。

ジョブの発行者(スケジューラー)が乗っ取られた場合

信頼された発行者は、jobs を持つどのアプリケーションについても、どのテナントのチケットでも署名できます。ゲートウェイの鍵と同じくらい大事に扱ってください。アプリケーションとは別に動かし、できれば鍵を Vault Transit に置きます。無効にするときは、trusted_issuers から削除します。

乗っ取られたアプリケーションがセッションのトークンを使い回す場合

tenant-pass-gateway の後ろにあっても、アプリケーションはほかの Web アプリケーションと同じく、ユーザーのセッションの Cookie やトークンを受け取ることがあります。乗っ取られたアプリケーションは、それを使って、トークンの期限が切れるまでに、そのユーザーのテナントクレームにあるすべてのテナントのパスを集めようとするかもしれません。手口は、トークンの使い回しと、ユーザーのブラウザーにほかのテナントのホストへリクエストを送らせることの 2 つです。

使い回しに対しては、tenant-pass-gateway の forward_claims(トークンがアプリケーションに届く前にプロキシで取り除く)と、呼び出し元の認証(アプリケーションがゲートウェイを直接呼ぶのを防ぐ)で対抗できます。ブラウザーを使う経路を塞げるのは、セッションの範囲をテナントごとに分けること、つまりホストごとのセッションの Cookie と identity.single_tenant_tokens だけです。Fetch Metadata の確認は、ユーザーの操作によらないホストをまたぐリクエストを止めますが、ほかのテナントのページも配信している乗っ取られたアプリケーションに対しては確実ではありません。

セッションの範囲を分けていなくても、被害はそのユーザー自身のテナントクレームにあるテナントに、セッションが有効な間だけ限られます。tenant-pass-gateway をご覧ください。

ID プロバイダーが乗っ取られた場合

tenant-pass-gateway は、利用者の ID プロバイダーが署名したトークンを信頼します。ID プロバイダー(またはその署名鍵)を握った人は、どのテナントのセッションでも作れます。その ID プロバイダーに頼るほかのシステムと同じことで、tenant-pass に守れる範囲の外です。

ローテーションキーが漏れた場合

ローテーションキーを持つ人は、その鍵が有効な過去と未来のすべてのウィンドウについて、そこから導出するパスワードをすべて計算できます。tenant-pass に接続する必要すらありません(ストアにログインするには、ストアへのネットワークの経路が要りますが)。すぐに鍵を入れ替えてください(運用)。1 ウィンドウが過ぎれば、古いパスワードはすべて無効になります。PostgreSQL では VALID UNTIL が、MySQL と Redis ではスイーパーと次の発行が、それを担います。

ローテーションキーを Vault / OpenBao Transit に置いておけば、鍵そのものはコピーできません(鍵管理(Vault / OpenBao Transit))。ホストに入り込んだ攻撃者がパスワードを導出できるのは、ホストとその Vault のトークンを握っている間だけになります。

ゲートウェイの鍵が漏れた場合

ゲートウェイの鍵を持つ人は、どのテナント、どのアプリケーションのパスでも発行できます。ただし、そのパスを使うには、アプリトークンか証明書でそのアプリケーションとして認証しなければなりません。gateway_keys からその鍵を削除し、新しい鍵をデプロイしてください。

アプリトークンが漏れた場合

アプリトークンだけでは、パスがない限り何もできません。それでも、設定のトークンのハッシュは置き換えてください。

ネットワーク上の攻撃者

アプリケーションから tenant-pass へ、tenant-pass からストアとディレクトリへの接続は、どれも TLS 1.2 以上で、証明書とホスト名を検証します。暗号化しない接続は、2 つの設定を明示的に有効にしない限り使えず、それもテスト専用です。データベースのドライバーは接続文字列(DSN)を使わずに設定するので、libpq の sslmode=prefer のような、ドライバーの安全でない既定値が紛れ込むこともありません。

ストアへの通信とストアのログに含まれるパスワード

tenant-pass がストアに送るのは、ベリファイアやハッシュだけです。PostgreSQL には SCRAM-SHA-256 のベリファイア、MySQL には caching_sha2_password のハッシュ、Redis には SHA-256 の ACL ハッシュを送り、どれもそれ自体ではログインに使えません。平文のパスワードがあるのは、tenant-pass のメモリと API のレスポンスの中だけです。

とはいえ、SCRAM のベリファイアや caching_sha2_password のハッシュがあれば、手元で総当たりしてパスワードを推測すること自体はできます。tenant-pass のパスワードは 256 ビットのランダムな値なので、現実には当てられません。それでも、できれば管理者のロールでは log_statement = ddl のような文をログに残す設定をオフにしておいてください。

時計の操作

各レプリカは、書き込む前に自分の時計をストアのサーバーの時計と比べ、差が rotation.max_clock_skew を超えていれば書き込みを拒否します(clock_skew)。時計が狂ったレプリカが、パスワードの期限を未来へ延ばしたり、アカウントを過去のウィンドウに戻したりはできません。ただし、狂っているのがストアのサーバーの時計だった場合は、tenant-pass もそれに合わせてしまいます。データベースのサーバーでも NTP を動かし、clock_skew を監視してください。

サービス妨害

tenant-pass は、インターネットに公開して使うようには作っていません。内部のネットワークに置くことが前提です。そのうえで、次のような上限を設けています。

  • リクエストボディは 4 KiB、ヘッダーは 16 KiB まで。読み書きにはタイムアウトがある。
  • 同時に処理するリクエストの数は limits.max_concurrent_requests まで。超えた分にはすぐに 503 busy を返す。
  • 1 件のリクエストの処理時間は limits.issue_timeout まで。
  • レディネスの確認の結果は 5 秒間キャッシュするので、これを連打してストアに負荷をかけることはできない。
  • repair によるパスワードの書き直しは、レプリカごと・アカウントごとに 10 秒に 1 回まで。

はっきり対象外としていること

次のことは、tenant-pass の守備範囲に入っていません。

  • 乗っ取られたゲートウェイ、ディレクトリ、tenant-pass のホスト、ストアから、テナントのデータを守ること。
  • 乗っ取られたアプリケーションから、そのアプリケーションがいま処理しているテナントを守ること。
  • 認証済みのアプリケーションに、どのテナントが存在するかを隠すこと(denied は「そのテナントは存在しない」と「許可されていない」を区別しませんが、応答にかかる時間の差で見分けられるかもしれません)。
  • ほかのテナントのオブジェクトへの権限の付与を見つけること(テナント A のスキーマのテーブルへの権限を、テナント B のロールに与えている場合など)。テナントのオブジェクト同士を分けるのはプロビジョニングの仕事です。tenant-pass が拒否するのは管理者の権限とサーバー全体に及ぶ権限で、テナントをまたぐ個別の権限までは見ません。
  • テナントが共有のデータベースサーバーの接続を使い切ってしまうのを防ぐこと(管理者のロールのための接続を確保してください。各ストアのページをご覧ください)。
  • 同じアカウントを指す 2 つのディレクトリの項目(SQL のディレクトリの場合)や、2 つのノード名を見つけること。静的なディレクトリは共有されたアカウントを拒否しますが、SQL のディレクトリでは利用者のスキーマ次第です。
  • 認証情報の期限が切れたときや、アプリケーションを無効にしたときに、すでに開いているデータベースのセッションを閉じること(運用 をご覧ください)。
  • ストアの中でのテナントのアカウント自身の操作から守ること。たとえば、PostgreSQL のロールは自分のパスワードを変えられますが、それで締め出されるのは、次のウィンドウか repair までの間の、自分自身のテナントだけです。
  • データベースでの権限の管理の代わりになること。テナントのアカウントには、そのテナントのデータに必要な権限(たとえば 1 つのスキーマやデータベースの所有権)だけを与えてください。

この文書は、リポジトリにある docs/ja/threat-model.md をもとにしています(英語版は docs/en/threat-model.md)。