Open source · Multi-tenant security
1 つのアプリの侵害で、
全テナントを
失わないために。
tenant-pass は、テナントごとにデータベースのアカウントを持つマルチテナント SaaS のための認証情報ブローカーです。アプリケーションはテナントのパスワードを一切持ちません。ゲートウェイが実際に転送したリクエストに対してだけ、そのテナントの短命なパスワードを受け取ります。
- バージョン
- v0.1.01.0 より前
- ライセンス
- Apache-2.0
- 実装
- Go単一の静的バイナリ
- ストア
- PostgreSQL
MySQL / Redis
- aud
- billing
- tenant
- acme
- iat
- 2026-11-03T12:00:00Z
- exp
- 2026-11-03T12:00:30Z
- jti
- hamSSKegCc-kk82SdW2dpw
footer {"kid":"gw1"}tenant-pass/v1/pass
ゲートウェイが、リクエストのたびに署名する「パス」。
アプリケーションは、これをテナントの認証情報に交換します。
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 なし 届くのは、すべてのテナント
tenant-pass あり 届くのは、いまリクエストが来ているテナントだけ
届かないテナントリクエストが来ていないテナントのパスは手に入らないので、そのテナントの認証情報は取れません。保存されたパスワードもありません。
届くテナント攻撃者が居座っている間に、利用者が実際に使っているテナントです。アプリケーションがいま処理しているテナントは守れないことを、脅威モデルにも明記しています。
気付くための時間1 分あたりのテナント数の制限(超えると 429 を返します)と、リクエストごとに 1 行ずつ書く監査ログによって、異常に気付いて対応する時間を稼げます。
03 · How it works
パスワードは、
リクエストが来たときにだけ
ゲートウェイがリクエストごとにパスに署名します。アプリケーションは、そのパスと自分の ID を tenant-pass に示して、そのテナントの認証情報を受け取ります。パスワードは既定で 15 分ごとに入れ替わり、期限が来れば使えなくなります。
- 01ブラウザ → ゲートウェイ利用者が acme.example.com を開きます。プロキシは、利用者が勝手に付けた Tenant-Pass ヘッダーを取り除いてから、ゲートウェイに問い合わせます。
- 02パスに署名ゲートウェイは、セッション(ID プロバイダーが発行した JWT)を検証します。利用者がそのホストのテナントに属するときだけ、30 秒で切れるパスに署名します。
- 03パスを付けて転送プロキシはパスを Tenant-Pass ヘッダーに入れて、リクエストをアプリケーションに転送します。
- 04パスを交換アプリケーションは、自分のアプリトークン(またはクライアント証明書)とパスを、POST /v1/credentials で tenant-pass に送ります。
- 05テナントを特定tenant-pass は、パスの署名、中身(クレーム)、期限、宛先(aud)を検証し、テナント数の制限を確かめます。そのうえで、ディレクトリからテナントとアカウントを調べます。
- 06パスワードを設定ロックを取り、アカウントが安全か、時計がずれていないかを確かめます。そのうえで、計算したパスワードのベリファイア(照合用のハッシュ)を書き込みます。すでに同じ値なら書き込みません。
- 07認証情報を返すユーザー名、パスワード、接続先と、有効期限(expires_at)、取り直す時刻(refresh_at)を返します。失敗したときの理由は、監査ログにだけ書きます。
- 08接続アプリケーションは、そのテナント専用のアカウントでデータベースに接続します。パスワードはメモリの中だけに置きます。
Defenses
何を、どうやって守るのか
どれか 1 つが破られたら終わり、という作りにはしていません。パス、パスワード、データベース、時計、鍵のそれぞれに制限をかけています。前提はどれも「アプリケーションは乗っ取られうる」です。
実際のリクエストにだけ発行
認証情報を得るには、ゲートウェイが署名した「パス」が必要です。パスは「いま、テナント X 宛てのリクエストをあなたに転送している」という証明です。乗っ取られたアプリケーションはパスを作れないので、来てもいないテナントの認証情報は取れません。
1 つのアプリケーションに結び付く
パスには転送先のアプリケーション名(aud)が書かれています。tenant-pass は、それが認証したアプリケーションと一致するかを確かめます。別のアプリケーションから盗んだパスは使えません。
パスの有効期間は数秒
上限は設定の pass.max_ttl(既定 60 秒、上限 5 分)を超えられません。推奨は 30 秒です。期限を過ぎたパスは、猶予なしで拒否します。
パスワードを保存しない
テナントのパスワードは、ローテーションキーから HMAC で計算(導出)します。どこにも保存しないので、盗まれるパスワードの一覧がそもそもありません。同じ入力からはいつも同じ値になるため、tenant-pass を複数台(レプリカ)並べても、台どうしで情報をやり取りする必要はありません。
有効期限はデータベースが守る
PostgreSQL では、パスワードに有効期限(VALID UNTIL)を設定します。tenant-pass が止まっていても、期限が来ればデータベース自身がログインを拒否します。MySQL と Redis には期限の仕組みがないため、パスワードを最大 2 世代だけ持たせ、期限の切れたものは tenant-pass が定期的に削除します。
平文のパスワードを流さない
データベースへは、平文のパスワードではなく、照合用のハッシュだけを送ります(PostgreSQL は SCRAM ベリファイア、MySQL は caching_sha2_password のハッシュ、Redis は SHA-256 の ACL ハッシュ)。そのため、パスワードがデータベースのログやレプリケーションに現れません。
被害が広がる速さに上限
アプリケーションが 1 分間に取得できる「異なる」テナントの数を制限します(既定 60)。同じテナントの繰り返しは数えません。急に多くのテナントに触れ始めたら、429 の応答と監査ログでわかります。
危険なアカウントには触れない
パスワードを変えるのは、「管理対象」の印が付いたアカウントだけです。管理者権限や、サーバー全体に及ぶ権限を持つアカウントは拒否します。ロールの継承を通じて権限を持つ場合も同じです。ディレクトリが誤ったアカウントを指していても、変更しません。
自分の時計を信じない
書き込む前に、自分の時計をデータベースの時計と比べます。ずれが大きいレプリカは処理を拒否するので、パスワードの期限を未来へ延ばしたり、過去へ戻したりすることはできません。
後から動く処理にはジョブチケット
キューに入れた処理や夜間のバッチのために、パスの有効期間を延ばすことはしません。代わりに、1 つのテナントと 1 つのアプリケーションに限った「ジョブチケット」という別の形式を使います。
鍵は KMS の中に置ける
ローテーションキーと署名鍵は、HashiCorp Vault / OpenBao の Transit(鍵を外に出さずに暗号の計算をする機能)に置けます。鍵は Vault の外に出ないので、tenant-pass のホストに侵入されても、鍵をコピーされることはありません。
迷ったら止まる
未知の設定キー、弱い鍵、証明書を検証しない TLS、誰でも書き込めるシークレットファイルがあると、起動しません。リクエストの処理でも、判断に迷う場面ではすべて拒否します。拒否の理由はクライアントに返さず、監査ログに書きます。
Separation of keys
1 か所を破られても、
全部はそろわない
ゲートウェイの秘密鍵とローテーションキーは、同じ場所に置きません。パスに署名できる者はパスワードを導出できず、パスワードを導出できる者はパスに署名できません。アプリケーションは、そのどちらも持ちません。
ゲートウェイ tenant-pass アプリ 署名する
ゲートウェイ
持つもの
パスの署名鍵(Ed25519 の秘密鍵)
✓ パスに署名できる✕ パスワードは計算できない
導出する
tenant-pass
持つもの
ゲートウェイの公開鍵、アプリトークンのハッシュ、ローテーションキー、データベースの管理者の認証情報
✓ パスワードを計算し、データベースに設定できる✕ パスには署名できない
交換する
アプリケーション
持つもの
自分のアプリトークン(またはクライアント証明書)
✓ 届いたパスを認証情報に交換できる✕ パスもパスワードも作れない
ただし、tenant-pass 自身はローテーションキーとデータベースの管理者の認証情報を持っています。tenant-pass のホストが乗っ取られれば、全テナントのデータが危険にさらされます。そのため tenant-pass 自体を小さく作り、専用の最小構成で動かすことを前提にしています(脅威モデル)。
Stores
PostgreSQL・MySQL・Redis
データベースごとに持っている仕組み(有効期限、2 つのパスワード、ACL)に合わせて、パスワードのローテーションのしかたを変えています。どのデータベースにも、送るのはハッシュかベリファイアだけです。
| PostgreSQL | MySQL | Redis / Valkey | |
|---|---|---|---|
| アカウント | ロール | ユーザー | ACL ユーザー |
| 対応バージョン | 16 以降 | 8.0.27 以降、8.4、9.x | Redis 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(最初の公開バージョン・1.0 より前)
パスの形式とパスワードの導出方式にはバージョン(tenant-pass/v1)が付いていて、テストベクター(決まった入力と、期待する出力の組)で検証しています。ただし、設定や API の細部はまだ変わる可能性があります。外部のセキュリティ監査はまだ受けていません。デプロイする前に、必ず脅威モデルをお読みください。
Architecture 仕組み → パスの中身、パスワードの導出とローテーション、ストアごとの動き。
Threat model セキュリティ → 守るものと守らないもの、残るリスク、脆弱性の報告。
Get started 導入 → 手元で試す手順と、本番に向けたチェックリスト。
Reference API → すべてのエンドポイント、エラーコード、curl の例。 ソースコード(Apache License 2.0)は GitHub のリポジトリにあります。組み込みや導入のご相談はお問い合わせからどうぞ。