手元の PostgreSQL 1 台で tenant-pass を動かし、パス(pass)を 1 つのテナントのデータベースのパスワードに交換してみます。仕組みの全体は 設計、何を守り何を守らないかは 脅威モデル にまとめてあります。
tenant-pass の現在のバージョンは v0.1.0(2026-11-03 公開)で、まだ 1.0 に達していません。パスの形式とパスワードの導出方式には
tenant-pass/v1というバージョンが付いていますが、設定や API の細部は今後変わる可能性があります。また、外部のセキュリティ監査はまだ受けていません。
用意するもの
- Go(
go installでビルドします。できあがるのは 1 つの静的バイナリです) - PostgreSQL 16 以降(MySQL 8.0.27 以降、Redis 7.0 以降 / Valkey 7.2 以降も使えます。ここでは PostgreSQL で説明します)
curl
ここでの手順は暗号化しない接続を使うので、手元での評価にだけ使ってください。本番ではすべての接続を TLS にし、証明書とホスト名も検証します(運用)。
1. インストールする
go install github.com/kobesoft-labs/tenant-pass/cmd/tenant-pass@latest
go install github.com/kobesoft-labs/tenant-pass/cmd/tenant-pass-gateway@latest # すぐに使えるゲートウェイ(任意)
tenant-pass のコマンドは次のとおりです。
| コマンド | すること |
|---|---|
serve | サーバーを起動する |
check-config | 設定ファイルを検証する(-connect を付けると、ディレクトリとすべてのノードにも接続する) |
gen-gateway-key | ゲートウェイ用の Ed25519 の鍵ペアを作る(-private・-public) |
gen-signing-key | gen-gateway-key と同じ(ジョブチケットの鍵、スケジューラーの鍵に使う) |
gen-rotation-key | ローテーションキーのファイルを作る(-out) |
gen-app-token | アプリトークンとそのハッシュを作る |
sign-pass | ゲートウェイの鍵でパスに署名する(試験と連携の確認用) |
sign-job-ticket | スケジューラーの鍵でジョブチケットに署名する |
version | バージョンを表示する |
2. 鍵とトークンを作る
# ゲートウェイの鍵ペア(秘密鍵はゲートウェイだけが持つ。tenant-pass には公開鍵だけを渡す)
tenant-pass gen-gateway-key -private gw.key -public gw.pub
# ローテーションキー(tenant-pass だけが持つ)
tenant-pass gen-rotation-key -out rotation.key # *.key は git と Docker のビルドで無視される
# アプリトークン: token はアプリケーションへ、token_sha256 は設定ファイルへ
tenant-pass gen-app-token
gen-app-token を実行すると、次の 2 行が出ます。tpa_ で始まるトークンはアプリケーションにシークレットとして渡し、tenant-pass の設定にはハッシュだけを書きます。
token: tpa_…
token_sha256: 3f1c…(16 進 64 文字)
ゲートウェイの秘密鍵とローテーションキーは、決して同じ場所に置かないでください。ゲートウェイはパスに署名できてもパスワードは導出できず、tenant-pass はその逆です。片方が盗まれても、それだけではテナントのパスワードに届かない。tenant-pass の設計はこの分担を前提にしています。
3. テナントのロールを用意する
スーパーユーザー(またはプロビジョニング用のロール)で、次の SQL を実行します。1 文ずつの理由は PostgreSQL に書いてあります。
-- tenant-pass 専用のデータベース(テナントが接続できてはいけない)
CREATE DATABASE tenant_pass;
REVOKE ALL ON DATABASE tenant_pass FROM PUBLIC;
-- tenant-pass がログインするアカウント
CREATE ROLE tenant_pass_admin LOGIN CREATEROLE PASSWORD '...';
GRANT CONNECT ON DATABASE tenant_pass TO tenant_pass_admin;
-- 目印: tenant-pass はこのロールのメンバーにしか手を触れない
CREATE ROLE tenant_pass_managed NOLOGIN;
-- テナント acme のロール
CREATE ROLE t_acme LOGIN IN ROLE tenant_pass_managed;
GRANT t_acme TO tenant_pass_admin WITH ADMIN OPTION, INHERIT FALSE, SET FALSE;
tenant-pass は自分ではロールを作らず、権限も付けません。テナントとそのアカウントは、これまでどおり利用者のプロビジョニング(環境の準備)の処理で作ってください。
4. 設定ファイルを書く
手元で試すための最小の設定です。HTTP もデータベース接続も暗号化しないので、insecure_plaintext* と allow_insecure_plaintext を両方とも有効にしています。本番では使わないでください。
{
"listen": "127.0.0.1:8443",
"insecure_plaintext_http": true,
"allow_insecure_plaintext": true,
"gateway_keys": [{"id": "gw1", "public_key_file": "./gw.pub"}],
"apps": [{"name": "billing", "token_sha256": "<gen-app-token の token_sha256>"}],
"directory": {"static": {"tenants": [
{"id": "acme", "hosts": ["acme.example.com"], "node": "pg1", "account": "t_acme", "enabled": true, "apps": ["billing"]}
]}},
"rotation": {
"keys": [{"id": "k1", "secret_file": "./rotation.key", "not_before": "2026-01-01T00:00:00Z"}]
},
"nodes": [{
"name": "pg1",
"type": "postgres",
"endpoint": {
"address": "127.0.0.1:5432",
"user": "tenant_pass_admin",
"password_file": "./pg1-admin-password",
"database": "tenant_pass",
"insecure_plaintext": true
},
"managed_group": "tenant_pass_managed",
"account_pattern": "^t_[a-z0-9_]{1,40}$",
"connect": {"host": "127.0.0.1", "port": 5432, "database": "app"}
}]
}
書くときに知っておくと困らないことを挙げておきます(全項目は 設定リファレンス)。
知らないキーや重複したキーがあると、起動の時点でエラーになります。タイプミスした設定が黙って無視される、ということは起きません。
パスワードや鍵などのシークレットは設定ファイルに直接書かず、別のファイル(*_file)で渡します。そのファイルは tenant-pass を実行するユーザーか root の持ち物にし、グループやほかのユーザーが書き込めないようにしておきます(chmod 600 で十分です)。
ローテーションキーの not_before は、その鍵を使い始める時刻です。起動する時点で、いちばん古い鍵の not_before が少なくとも 1 ウィンドウ分は過去になっていなければなりません。
connect は、クライアントに返す接続先です。本番では PgBouncer などのコネクションプーラーを指すことが多いでしょう。
5. 起動して試す
tenant-pass check-config -config config.json -connect # 起動時と同じ検査+接続の確認
tenant-pass serve -config config.json
別の端末を開き、ゲートウェイとアプリケーションの役をコマンドで演じます。APP_TOKEN には、手順 2 で出た tpa_ のトークンを入れておいてください。
# ゲートウェイの役: acme.example.com 宛てのリクエストを billing に転送した、というパスに署名する(有効期間は 30 秒)
PASS=$(tenant-pass sign-pass -key gw.key -kid gw1 -aud billing -host acme.example.com)
# アプリケーションの役: パスを認証情報に交換する
curl -s http://127.0.0.1:8443/v1/credentials \
-H "Authorization: Bearer $APP_TOKEN" -H 'Content-Type: application/json' \
-d "{\"pass\":\"$PASS\"}"
{
"tenant": "acme", "store": "postgres",
"host": "127.0.0.1", "port": 5432, "database": "app",
"username": "t_acme", "password": "lRG6msAkg88kIIjJU-N6yWZ6wzx1gQj62GYG71hcgjE",
"expires_at": "2026-11-03T12:08:59Z", "refresh_at": "2026-11-03T12:03:59Z"
}
パスワードと時刻は実行するたびに変わります。返ってきた username と password で、t_acme としてログインできるはずです。このとき tenant-pass が PostgreSQL に送ったのは平文のパスワードではなく、SCRAM ベリファイア(パスワードから計算した検証用の値)でした。ロールの VALID UNTIL には expires_at と同じ時刻が入っています。
うまくいったら、わざと失敗させてみてください。何を拒むのかが手で確かめられます。
- 30 秒待ってから同じパスを使う →
400 {"error":"invalid_pass"}(パスの期限切れ) -aud reportsで署名したパスを、billing のトークンで使う →400 invalid_pass(audが一致しない)- トークンを変える →
401 unauthorized - ディレクトリにないホストで署名する →
403 denied
どの失敗でも、クライアントには理由を返しません。理由を知りたいときは、標準出力に出る監査ログ(1 リクエストにつき 1 行の JSON)の reason を見ます。
次に読むもの
- 本物のゲートウェイを置く: tenant-pass-gateway(Caddy・nginx・Traefik・Envoy の forward-auth)、または 独自のゲートウェイ
- アプリケーション側の書き方: クライアント連携
- 後から動く処理: ジョブチケット
- 本番の構成と監視: 運用
- MySQL・Redis を使う: MySQL・Redis / Valkey