tenant-pass の API は、設定の listen のアドレスで HTTPS(TLS 1.2 以上)で待ち受けます。内部のネットワークから呼ぶためのもので、インターネットに公開するものではありません。レスポンスにはすべて Cache-Control: no-store が付きます。
POST /v1/credentials
パス(またはジョブチケット)を、テナントのデータベースの認証情報と交換します。アプリケーションがテナントのデータベースに接続する前に呼ぶ API で、呼ぶのは次の 3 つの場合です。手元にキャッシュした認証情報がないとき、refresh_at を過ぎたとき、データベースで認証に失敗したとき(流れは クライアント連携 を参照)。
呼び出すには、設定の apps に登録したアプリケーションとして認証します。認証の手段はアプリトークン、クライアント証明書、またはその両方です。ジョブチケットを送る場合は、そのアプリケーションに jobs の設定も要ります。
リクエスト
POST /v1/credentials HTTP/1.1
Host: tenant-pass.internal:8443
Authorization: Bearer tpa_9b0Z…
Content-Type: application/json
{"pass": "v4.public.…", "repair": false}
| 項目 | 説明 |
|---|---|
Authorization ヘッダー | Bearer <app token> の形で、ちょうど 1 つ送ります。省略してよいのは、クライアント証明書だけで認証するアプリケーションに限ります。 |
| クライアント証明書 | client_cert_uri を設定したアプリケーションでは必須です。 |
Content-Type ヘッダー | application/json です(charset などのパラメーターは付けてもかまいません)。 |
| ボディ | 最大 4096 バイトの JSON のオブジェクトで、下の表のメンバーだけを含めます。知らないメンバーや重複したメンバーがあると拒否されます。 |
ボディのメンバーは次のとおりです。
| メンバー | 型 | 必須 | 説明 |
|---|---|---|---|
pass | string | pass か ticket のどちらか | ゲートウェイから受け取ったパスです。パスの形式(tenant-pass/v1) をご覧ください。 |
ticket | string | pass か ticket のどちらか | ジョブチケットです。アプリケーションに jobs の設定が必要です。バックグラウンドジョブ(ジョブチケット) をご覧ください。 |
repair | bool | いいえ | tenant-pass から受け取った認証情報をストアが拒んだ(認証に失敗した)直後にだけ true にします。PostgreSQL では、ロールの状態が最新に見えてもパスワードを書き直します。書き直しはレプリカごと・アカウントごとに 10 秒に 1 回までで、それを超えたリクエストは repair なしとして扱います。MySQL と Redis は毎回実際のパスワードのハッシュを確かめているので、付けても動きは変わりません。 |
レスポンス(200 OK)
{
"tenant": "acme",
"store": "postgres",
"host": "pgbouncer.internal",
"port": 6432,
"database": "app",
"username": "t_acme",
"password": "lRG6msAkg88kIIjJU-N6yWZ6wzx1gQj62GYG71hcgjE",
"expires_at": "2026-11-03T12:08:59Z",
"refresh_at": "2026-11-03T12:03:59Z"
}
| メンバー | 説明 |
|---|---|
tenant | ディレクトリが返したテナント ID です。 |
store | postgres、mysql、redis のどれかです。 |
host、port、database | 接続先です(設定の nodes[].connect)。database は、設定していない場合と、Redis の場合には含めません。 |
username | アカウントの名前です。 |
password | base64url の 43 文字です。シークレットとして扱い、プロセスのメモリの中だけに置いてください。 |
expires_at | この時刻を過ぎると、PostgreSQL ではこのパスワードで新しい接続を開けなくなり、MySQL と Redis では次のスイープでパスワードが削除されることがあります。すでに開いている接続には影響しません。 |
refresh_at | この時刻を過ぎたら、認証情報を取り直してください。この時刻からは、次のウィンドウのパスワードが返ります。 |
同じウィンドウの間なら、どのレプリカに、どのワーカーから問い合わせても、返るパスワードは同じです。
エラー
エラーのレスポンスボディは常に {"error": "<code>"} の形です。細かい理由はクライアントには返さず、監査ログにだけ残します。
| ステータス | error | 意味 |
|---|---|---|
| 400 | invalid_request | リクエストボディが、空でない pass を持つ JSON のオブジェクトになっていません。または、知らないメンバーや重複したメンバー、JSON の後ろに余分なデータがあります。 |
| 400 | invalid_pass | パスを受け付けられません。形式が誤っている、知らない鍵で署名されている、署名やクレームが正しくない、期限が切れている、aud が呼び出し元のアプリケーションと違う、のいずれかです。 |
| 400 | invalid_ticket | ジョブチケットを受け付けられません。理由はパスの場合と同じです。 |
| 401 | unauthorized | アプリケーションの認証に失敗しました。WWW-Authenticate: Bearer realm="tenant-pass" を付けて返します。 |
| 403 | denied | テナントが存在しない、アプリケーションがそのテナントに許可されていない、テナントが無効になっている、ストアがアカウントを拒否した、のいずれかです。チケットの場合はさらに、アプリケーションでジョブチケットが有効になっていない、発行者が許可されていない、そのパスがすでにチケットと交換済み、のときにも返ります。 |
| 404 | not_found | URL のパスが存在しません。 |
| 405 | method_not_allowed | メソッドが POST ではありません。 |
| 413 | request_too_large | リクエストボディが 4096 バイトを超えています。 |
| 415 | unsupported_media_type | Content-Type が application/json ではありません。 |
| 429 | rate_limited | このレプリカで、アプリケーションが直近 1 分間に limits.max_tenants_per_minute を超える数のテナントを要求しました。すでに要求したテナントへの繰り返しのリクエストは数えません。 |
| 503 | busy | 同時に処理できるリクエストの数(limits.max_concurrent_requests)に達しています。間隔を空けてやり直してください。 |
| 503 | unavailable | ディレクトリかストアのエラー、タイムアウト、またはこのレプリカとストアのサーバーの時計がずれています。間隔を空けてやり直してください。別のレプリカなら成功することもあります。 |
400・401・403 は、同じパスでは何度やっても成功しません。やり直してよいのは 429 と 503 だけです。
例
curl -s https://tenant-pass.internal:8443/v1/credentials \
-H "Authorization: Bearer $APP_TOKEN" -H 'Content-Type: application/json' \
-d "{\"pass\":\"$PASS\"}"
POST /v1/job-tickets
パスを、同じテナントのジョブチケットと交換します。エクスポートやインポートのように、リクエストが終わった後で動く処理にも認証情報を取らせたいとき、ユーザーのリクエストを処理している間に呼びます(バックグラウンドジョブ(ジョブチケット))。
認証は POST /v1/credentials と同じです。そのうえで、設定に job_tickets.signing_key があり、アプリケーションに "jobs": {"issue": true} が書かれていなければなりません。
リクエスト
POST /v1/job-tickets HTTP/1.1
Authorization: Bearer tpa_…
Content-Type: application/json
{"pass": "v4.public.…", "ttl": "15m"}
| メンバー | 型 | 必須 | 説明 |
|---|---|---|---|
pass | string | はい | 呼び出し元のアプリケーションに対して有効なパスです。 |
ttl | string | いいえ | ほしいチケットの有効期間です。秒単位の整数で、1m 以上にします。apps[].jobs.max_ttl と job_tickets.max_ttl を超えることはできません。省略するとアプリケーションの jobs.max_ttl になります。 |
レスポンス(200 OK)
{"ticket": "v4.public.…", "tenant": "acme", "expires_at": "2026-11-03T12:30:00Z"}
| メンバー | 説明 |
|---|---|
ticket | ジョブチケットです。シークレットとして扱ってください。 |
tenant | チケットのテナント ID です。 |
expires_at | チケットの有効期限です。 |
テナントは発行の時点で確定します。そのため、発行の時点でテナントが存在し、有効で、アプリケーションに許可されていなければなりません。チケットの発行も、アプリケーションの「1 分間に扱うテナントの数」に数えます。
エラー
エラーは /v1/credentials と同じです。アプリケーションでチケットの発行が有効になっていなければ 403 denied が返ります。1 つのパスと交換できるチケットは 1 枚だけなので、同じパスで 2 回目を頼んだときも 403 denied です。
GET /healthz
プロセスが生きているかを確かめるためのもの(ライブネス)です。動いている間は 200 {"status":"ok"} が返ります。認証は要りません。
GET /readyz
リクエストを受け付けられるかを確かめます(レディネス)。認証は要りません。
| レスポンス | 意味 |
|---|---|
200 {"status":"ready"} | ディレクトリとすべてのノードが応答しています。 |
503 {"status":"draining"} | 停止の処理(SIGTERM を受けた後)が始まっています。そのレプリカをロードバランサーから外してください。 |
503 {"status":"unavailable"} | ディレクトリか、どれかのノードに接続できません。 |
ディレクトリとノードを確かめた結果は 5 秒間キャッシュされます。
監査ログ
tenant-pass は、リクエストごとに 1 行の JSON を標準出力に書き出します。msg は、/v1/credentials では "credential"、/v1/job-tickets では "job_ticket" です。
{"time":"2026-11-03T12:00:01Z","level":"INFO","msg":"credential","request_id":"43f007226bb0e766","app":"billing","repair":false,"jti":"hamSSKegCc-kk82SdW2dpw","host":"acme.example.com","tenant":"acme","node":"pg1","account":"t_acme","store":"postgres","window":1993007,"key_id":"k1","changed":true,"expires_at":"2026-11-03T12:08:59Z","result":"ok","duration_ms":7}
| 項目 | 説明 |
|---|---|
request_id | このリクエストのランダムな ID です。 |
via | pass か ticket です。 |
issuer | チケットの場合に、チケットに署名した鍵の ID を記録します。 |
result | ok、またはクライアントに返したエラーコードです。 |
reason | 失敗の理由です。app_auth、body_size、body_json、content_type、pass_malformed、pass_unknown_key、pass_signature、pass_claims、pass_expired、pass_audience、distinct_tenants、distinct_tenants_jobs、pass_or_ticket、jobs_not_enabled、job_issue_not_enabled、issuer_not_allowed、ticket_lifetime、pass_already_used、ticket_*(pass_* と同じ意味)、ttl、sign_error、body_read、method、path、tenant_not_found、tenant_disabled、directory_error、directory_mismatch、unknown_node、account_refused、clock_skew、timeout、store_error、max_concurrent_requests のどれかです。 |
error | directory_error、store_error、account_refused などの場合に、エラーの種類(SQLSTATE、MySQL のエラー番号、Redis のエラーのプレフィックス、拒否の理由)を記録します。データベースサーバーのエラーメッセージはそのまま写しません。データベースサーバー以外で起きたエラー(ネットワーク、TLS、タイムアウト)は文面ごと記録するので、ホスト名や管理者のユーザー名が含まれることがあります。ただし、パスワードや鍵は決して含みません。 |
changed | ストアを変更したかどうかです。 |
window、key_id | 認証情報が、どのウィンドウとどのローテーションキーのものかを示します。 |
パスワード、ベリファイア、ハッシュ、パス、ジョブチケット、アプリトークン、ローテーションキーは、ログに決して出しません。クライアントの IP アドレスも記録しないので、必要ならロードバランサーのログで調べてください。
ログのレベルは、成功が INFO、クライアント側のエラーが WARN です。directory_error、store_error、account_refused、clock_skew、timeout、unknown_node、directory_mismatch、sign_error だけは ERROR になります。どれでアラートを鳴らすかは 運用 にまとめてあります。
この文書は、リポジトリにある docs/ja/api.md をもとにしています(英語版は docs/en/api.md)。