tenant-pass

はじめに(ローカルで試す)

tenant-pass を手元の PostgreSQL 1 台で動かし、パスを認証情報に交換するまでの手順です。鍵とアプリトークンの作り方、テナントのロールの準備、最小の設定ファイル、curl での確認、本番に進む前に読むものをまとめています。

このページの見出し
  1. 用意するもの
  2. 1. インストールする
  3. 2. 鍵とトークンを作る
  4. 3. テナントのロールを用意する
  5. 4. 設定ファイルを書く
  6. 5. 起動して試す
  7. 次に読むもの

手元の 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-keygen-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 を見ます。

次に読むもの