tenant-pass

Open source · Multi-tenant security

1 つのアプリの侵害で、全テナントを
失わないために。

tenant-pass は、テナントごとにデータベースのアカウントを持つマルチテナント SaaS のための認証情報ブローカーです。アプリケーションはテナントのパスワードを一切持ちません。ゲートウェイが実際に転送したリクエストに対してだけ、そのテナントの短命なパスワードを受け取ります。

バージョン
v0.1.01.0 より前
ライセンス
Apache-2.0
実装
Go単一の静的バイナリ
ストア
PostgreSQL
MySQL / Redis

The problem

マルチテナント SaaS では、アプリが「全員の鍵束」を持っている

複数の顧客(テナント)を 1 つのアプリケーションで扱う SaaS では、テナントごとにデータベースを分けていても、そこへ接続するアプリケーションはふつうすべてのテナントのデータに届く状態にあります。よくある形は次の 2 つです。

1 つのアプリ = 全テナントの鍵

よくある形 1

1 つの強いアカウントで、全テナントに接続する

アプリケーションは管理者に近い共通のアカウントでデータベースに接続し、どのテナントのデータを読むかはアプリケーションのコードが決めます。コードの不具合や乗っ取りがあれば、データベースの側には止める仕組みがありません。

# アプリケーションの設定
DATABASE_URL=postgres://app_admin:••••@db/app   # 全テナント共通

よくある形 2

テナントごとのパスワードを、中央の表に保存する

テナントごとにアカウントを分けても、そのパスワードを暗号化して中央のテーブルに置き、アプリケーションが復号して使う形です。アプリケーションが復号の鍵を持つ以上、乗っ取られれば全テナントのパスワードを一度に取り出せます。

-- 中央のテーブル
tenant | db_user | encrypted_password
acme   | t_acme  | AES(…)   ← アプリが復号できる

アプリケーションは、乗っ取られうる

リモートでコードを実行される脆弱性、悪意のある依存パッケージ、盗まれたデプロイ用の認証情報など、攻撃者がアプリケーションのプロセスの中でコードを動かせるようになる道はいくつもあります。そうなれば、攻撃者はすべてのテナントのデータに一度に届いてしまいます。

tenant-pass の設計目標

アプリケーションを信頼しない。
乗っ取られたアプリケーションにできることを、「いまリクエストが来ているテナントだけ」に狭める。

脅威モデルでも、アプリケーションは「信頼しない」側に置いています。

Blast radius

アプリが乗っ取られたとき、どこまで届くか

起きていることは、どちらも同じ「アプリケーションの乗っ取り」です。違うのは、攻撃者の手が届くテナントの数です。

tenant-pass なし

届くのは、すべてのテナント

アプリケーション 乗っ取られた acme beta cedar delta ember fern gale harbor iris juno kite lumen ↑ 共通のアカウント、または復号できるパスワードの表から、全テナントへ
tenant-pass あり

届くのは、いまリクエストが来ているテナントだけ

アプリケーション 乗っ取られた tenant-pass パスが無ければ渡さない acme beta cedar delta ember fern gale harbor iris juno kite lumen ↑ 届く先を増やす速さも、1 分あたりのテナント数で制限(既定 60)

届かないテナントリクエストが来ていないテナントのパスは手に入らないので、そのテナントの認証情報は取れません。保存されたパスワードもありません。

届くテナント攻撃者が居座っている間に、利用者が実際に使っているテナントです。アプリケーションがいま処理しているテナントは守れないことを、脅威モデルにも明記しています。

気付くための時間1 分あたりのテナント数の制限(超えると 429 を返します)と、リクエストごとに 1 行ずつ書く監査ログによって、異常に気付いて対応する時間を稼げます。

03 · How it works

パスワードは、
リクエストが来たときにだけ

ゲートウェイがリクエストごとにパスに署名します。アプリケーションは、そのパスと自分の ID を tenant-pass に示して、そのテナントの認証情報を受け取ります。パスワードは既定で 15 分ごとに入れ替わり、期限が来れば使えなくなります。

ブラウザ 利用者のセッション ゲートウェイ プロキシ+forward-auth アプリケーション billing(信頼しない) tenant-pass 状態を持たない データベース PostgreSQL ほか pass ディレクトリ acme→t_acme 12345678
  1. 01ブラウザ → ゲートウェイ利用者が acme.example.com を開きます。プロキシは、利用者が勝手に付けた Tenant-Pass ヘッダーを取り除いてから、ゲートウェイに問い合わせます。
  2. 02パスに署名ゲートウェイは、セッション(ID プロバイダーが発行した JWT)を検証します。利用者がそのホストのテナントに属するときだけ、30 秒で切れるパスに署名します。
  3. 03パスを付けて転送プロキシはパスを Tenant-Pass ヘッダーに入れて、リクエストをアプリケーションに転送します。
  4. 04パスを交換アプリケーションは、自分のアプリトークン(またはクライアント証明書)とパスを、POST /v1/credentials で tenant-pass に送ります。
  5. 05テナントを特定tenant-pass は、パスの署名、中身(クレーム)、期限、宛先(aud)を検証し、テナント数の制限を確かめます。そのうえで、ディレクトリからテナントとアカウントを調べます。
  6. 06パスワードを設定ロックを取り、アカウントが安全か、時計がずれていないかを確かめます。そのうえで、計算したパスワードのベリファイア(照合用のハッシュ)を書き込みます。すでに同じ値なら書き込みません。
  7. 07認証情報を返すユーザー名、パスワード、接続先と、有効期限(expires_at)、取り直す時刻(refresh_at)を返します。失敗したときの理由は、監査ログにだけ書きます。
  8. 08接続アプリケーションは、そのテナント専用のアカウントでデータベースに接続します。パスワードはメモリの中だけに置きます。

仕組みを詳しく見る(パスの中身・パスワードの導出・ローテーション)→

Defenses

何を、どうやって守るのか

どれか 1 つが破られたら終わり、という作りにはしていません。パス、パスワード、データベース、時計、鍵のそれぞれに制限をかけています。前提はどれも「アプリケーションは乗っ取られうる」です。

01 pass

実際のリクエストにだけ発行

認証情報を得るには、ゲートウェイが署名した「パス」が必要です。パスは「いま、テナント X 宛てのリクエストをあなたに転送している」という証明です。乗っ取られたアプリケーションはパスを作れないので、来てもいないテナントの認証情報は取れません。

02 aud

1 つのアプリケーションに結び付く

パスには転送先のアプリケーション名(aud)が書かれています。tenant-pass は、それが認証したアプリケーションと一致するかを確かめます。別のアプリケーションから盗んだパスは使えません。

03 ttl

パスの有効期間は数秒

上限は設定の pass.max_ttl(既定 60 秒、上限 5 分)を超えられません。推奨は 30 秒です。期限を過ぎたパスは、猶予なしで拒否します。

04 stateless

パスワードを保存しない

テナントのパスワードは、ローテーションキーから HMAC で計算(導出)します。どこにも保存しないので、盗まれるパスワードの一覧がそもそもありません。同じ入力からはいつも同じ値になるため、tenant-pass を複数台(レプリカ)並べても、台どうしで情報をやり取りする必要はありません。

05 expiry

有効期限はデータベースが守る

PostgreSQL では、パスワードに有効期限(VALID UNTIL)を設定します。tenant-pass が止まっていても、期限が来ればデータベース自身がログインを拒否します。MySQL と Redis には期限の仕組みがないため、パスワードを最大 2 世代だけ持たせ、期限の切れたものは tenant-pass が定期的に削除します。

06 verifier

平文のパスワードを流さない

データベースへは、平文のパスワードではなく、照合用のハッシュだけを送ります(PostgreSQL は SCRAM ベリファイア、MySQL は caching_sha2_password のハッシュ、Redis は SHA-256 の ACL ハッシュ)。そのため、パスワードがデータベースのログやレプリケーションに現れません。

07 rate

被害が広がる速さに上限

アプリケーションが 1 分間に取得できる「異なる」テナントの数を制限します(既定 60)。同じテナントの繰り返しは数えません。急に多くのテナントに触れ始めたら、429 の応答と監査ログでわかります。

08 account

危険なアカウントには触れない

パスワードを変えるのは、「管理対象」の印が付いたアカウントだけです。管理者権限や、サーバー全体に及ぶ権限を持つアカウントは拒否します。ロールの継承を通じて権限を持つ場合も同じです。ディレクトリが誤ったアカウントを指していても、変更しません。

09 clock

自分の時計を信じない

書き込む前に、自分の時計をデータベースの時計と比べます。ずれが大きいレプリカは処理を拒否するので、パスワードの期限を未来へ延ばしたり、過去へ戻したりすることはできません。

10 jobs

後から動く処理にはジョブチケット

キューに入れた処理や夜間のバッチのために、パスの有効期間を延ばすことはしません。代わりに、1 つのテナントと 1 つのアプリケーションに限った「ジョブチケット」という別の形式を使います。

11 kms

鍵は KMS の中に置ける

ローテーションキーと署名鍵は、HashiCorp Vault / OpenBao の Transit(鍵を外に出さずに暗号の計算をする機能)に置けます。鍵は Vault の外に出ないので、tenant-pass のホストに侵入されても、鍵をコピーされることはありません。

12 strict

迷ったら止まる

未知の設定キー、弱い鍵、証明書を検証しない TLS、誰でも書き込めるシークレットファイルがあると、起動しません。リクエストの処理でも、判断に迷う場面ではすべて拒否します。拒否の理由はクライアントに返さず、監査ログに書きます。

Separation of keys

1 か所を破られても、
全部はそろわない

ゲートウェイの秘密鍵とローテーションキーは、同じ場所に置きません。パスに署名できる者はパスワードを導出できず、パスワードを導出できる者はパスに署名できません。アプリケーションは、そのどちらも持ちません。

ゲートウェイ tenant-pass アプリ

署名する

ゲートウェイ

持つもの
パスの署名鍵(Ed25519 の秘密鍵)

✓ パスに署名できる✕ パスワードは計算できない

導出する

tenant-pass

持つもの
ゲートウェイの公開鍵、アプリトークンのハッシュ、ローテーションキー、データベースの管理者の認証情報

✓ パスワードを計算し、データベースに設定できる✕ パスには署名できない

交換する

アプリケーション

持つもの
自分のアプリトークン(またはクライアント証明書)

✓ 届いたパスを認証情報に交換できる✕ パスもパスワードも作れない

ただし、tenant-pass 自身はローテーションキーとデータベースの管理者の認証情報を持っています。tenant-pass のホストが乗っ取られれば、全テナントのデータが危険にさらされます。そのため tenant-pass 自体を小さく作り、専用の最小構成で動かすことを前提にしています(脅威モデル)。

Stores

PostgreSQL・MySQL・Redis

データベースごとに持っている仕組み(有効期限、2 つのパスワード、ACL)に合わせて、パスワードのローテーションのしかたを変えています。どのデータベースにも、送るのはハッシュかベリファイアだけです。

PostgreSQLMySQLRedis / Valkey
アカウントロールユーザーACL ユーザー
対応バージョン16 以降8.0.27 以降、8.4、9.xRedis 7.0 以降、Valkey 7.2 以降
送るものSCRAM-SHA-256 ベリファイアcaching_sha2_password のハッシュSHA-256 の ACL ハッシュ
有効期限データベースが強制する(VALID UNTIL)tenant-pass が定期的に削除する(デュアルパスワードを使う)tenant-pass が定期的に削除する(複数のパスワードを使う)
ローテーション時の重なりなし(クライアントが 1 回だけリトライする)新旧どちらも有効新旧どちらも有効
「管理対象」の印グループロールのメンバーであること印としてのロールの付与印としてのチャネルルール

テナントのディレクトリ(どのテナントがどのアカウントを使うかの対応表)は、設定ファイルに直接書くか、PostgreSQL / MySQL への独自の SQL クエリで調べるかを選べます。クエリは読み取り専用のトランザクションで実行します。ゲートウェイは、Caddy・nginx・Traefik・Envoy の forward-auth(転送の前に認可を問い合わせる仕組み)として組み込めます。

Scope

汎用のシークレット管理ではありません

tenant-pass が扱うのは、マルチテナント SaaS における「リクエストに結び付いた、テナント単位の」データベースへのアクセスという、限られた 1 つの場面です。リクエストと関係なく動的な認証情報を発行したいなら、HashiCorp Vault / OpenBao のデータベースシークレットエンジンが向いています。

役割が違うので、両方を組み合わせることもできます。たとえば、tenant-pass の鍵(ローテーションキーと署名鍵)は Vault / OpenBao の Transit に置けます。

tenant-pass がしないこと

  • ✕アカウントの作成・削除・権限の付与(このようなアカウントの用意は、より強い権限を持つ別の仕組みで、利用者の側が行います)
  • ✕データベースへの通信のプロキシ
  • ✕エンドユーザーの認証(ゲートウェイの役割です)
  • ✕認証情報の保存
  • ✕乗っ取られたゲートウェイ・ディレクトリ・tenant-pass のホスト・データベースからの保護

目標としないこと(すべて)を見る →

v0.1.0 · PREVIEW

いまの状態: v0.1.0(最初の公開バージョン・1.0 より前)

パスの形式とパスワードの導出方式にはバージョン(tenant-pass/v1)が付いていて、テストベクター(決まった入力と、期待する出力の組)で検証しています。ただし、設定や API の細部はまだ変わる可能性があります。外部のセキュリティ監査はまだ受けていません。デプロイする前に、必ず脅威モデルをお読みください。

ソースコード(Apache License 2.0)は GitHub のリポジトリにあります。組み込みや導入のご相談はお問い合わせからどうぞ。