04API reference
API リファレンス
tenant-pass の API は、設定の listen のアドレス(既定 :8443)で待ち受ける HTTPS(TLS 1.2 以上)の API です。呼べるのは内部ネットワークからだけにしてください。エンドポイントは 4 つで、やり取りはすべて JSON です。
| メソッド | パス | 認証 | 内容 |
|---|---|---|---|
| POST | /v1/credentials | アプリケーション | パス(またはジョブチケット)を、テナントの認証情報に交換する |
| POST | /v1/job-tickets | アプリケーション | パスを、同じテナントのジョブチケットに交換する(アプリケーションごとに有効にする) |
| GET | /healthz | なし | プロセスが動いているかを確かめる(ライブネス) |
| GET | /readyz | なし | リクエストを受け付けられるかを確かめる(レディネス) |
共通の決まり
- ・通信は HTTPS(TLS 1.2 以上)です。クライアントは、tenant-pass の証明書を必ず検証してください。平文の HTTP は試験のためだけのもので、使うには 2 つの設定(insecure_plaintext_http と allow_insecure_plaintext)が必要です。
- ・すべての応答に付くヘッダー: Cache-Control: no-store、Pragma: no-cache、X-Content-Type-Options: nosniff、Content-Security-Policy: default-src 'none'; frame-ancestors 'none'、Referrer-Policy: no-referrer。応答にシークレットが含まれるため、途中のプロキシにもクライアントにも残さないようにしています。
- ・エラーのレスポンスボディは、常に {"error": "<code>"} の形です。失敗の理由は返さず、監査ログに書きます。
- ・POST のボディは、4096 バイトまでの JSON オブジェクトです。未知のフィールド、重複したフィールド、後ろに続く余分なデータは拒否します。ヘッダーは 16 KiB までです。
- ・検査の順序は、URL → メソッド → Content-Type → 同時に処理しているリクエスト数 → アプリケーションの認証 → リクエストボディ → パスまたはチケット → 1 分あたりのテナント数 → ディレクトリ → ストア、です(詳しくは仕組みのページ)。
アプリケーションの認証
POST のエンドポイントは、呼び出したアプリケーションを認証します。方法はアプリトークンとクライアント証明書の 2 つで、両方を求めることもできます。認証できたアプリケーションの名前(apps[].name)は、パスの aud と一致しなければなりません。
アプリトークン
Authorization: Bearer tpa_… を送ります。このヘッダーは 1 つだけにしてください。設定ファイルには、トークンの SHA-256 を 16 進で書いた token_sha256 だけを書きます。
クライアント証明書
設定の client_cert_uri(例: SPIFFE ID)に一致する URI SAN を持つ証明書で認証します。tls.client_ca_file の設定が必要です。一致する URI SAN が複数ある証明書は拒否します。
両方
両方を設定したアプリケーションは、すべてのリクエストで両方が必要です。証明書だけで認証するアプリケーションは、Authorization ヘッダーを省略できます。
POST/v1/credentials
パス(またはジョブチケット)を、テナントの認証情報に交換します。同じウィンドウの中なら、どのレプリカに問い合わせても、どのワーカーが問い合わせても、同じパスワードが返ります。
- 使う場面
- テナントのリクエストを処理するときに、その認証情報をまだ持っていない場合や、refresh_at を過ぎた場合。ジョブチケットを持つワーカーが処理を始めるとき。
- 認証
- 必要です(アプリケーションの認証)。
- 主なエラー
- 400 invalid_pass・401 unauthorized・403 denied・429 rate_limited・503 unavailable(エラーコード)。
リクエスト
POST /v1/credentials HTTP/1.1
Host: tenant-pass.internal:8443
Authorization: Bearer tpa_9b0Z…
Content-Type: application/json
{"pass": "v4.public.…", "repair": false} | フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| pass | string | pass か ticket のどちらか一方 | ゲートウェイから受け取ったパス(パスの形式)。 |
| ticket | string | pass か ticket のどちらか一方 | ジョブチケット。アプリケーションに jobs の設定が必要です(ジョブチケット)。 |
| repair | bool | いいえ | tenant-pass から得た認証情報がストアで拒否された直後に、true にします。PostgreSQL では、ロールが最新の状態に見えてもパスワードを書き直します(レプリカごと、アカウントごとに 10 秒に 1 回まで。それを超えた分は通常どおり処理します)。MySQL と Redis では毎回ハッシュを確かめているので、付けても動きは変わりません。 |
例
パスで取得する
curl -s https://tenant-pass.internal:8443/v1/credentials \
--cacert tenant-pass-ca.pem \
-H "Authorization: Bearer $APP_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"pass\":\"$PASS\"}" 認証エラー(28P01 など)を受けた直後に取り直す
curl -s https://tenant-pass.internal:8443/v1/credentials \
--cacert tenant-pass-ca.pem \
-H "Authorization: Bearer $APP_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"pass\":\"$PASS\",\"repair\":true}" ジョブチケットで取得する(ワーカー)
curl -s https://tenant-pass.internal:8443/v1/credentials \
--cacert tenant-pass-ca.pem \
-H "Authorization: Bearer $APP_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"ticket\":\"$TICKET\"}" クライアント証明書で認証する
curl -s https://tenant-pass.internal:8443/v1/credentials \
--cacert tenant-pass-ca.pem \
--cert reports.crt --key reports.key \
-H 'Content-Type: application/json' \
-d "{\"pass\":\"$PASS\"}" レスポンス 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。アプリケーションは、どのテナントかをここから判断し、Host ヘッダーからは判断しないでください。 |
| store | postgres・mysql・redis のいずれか。 |
| host・port・database | 接続先(設定の nodes[].connect)。database は、設定していない場合と Redis の場合は省略されます。 |
| username | アカウント名。 |
| password | base64url の 43 文字。シークレットとして扱い、プロセスのメモリの中だけに置いてください。 |
| expires_at | この時刻を過ぎると、PostgreSQL ではこのパスワードで新しい接続を開けなくなります。MySQL と Redis では、次の定期削除で消される可能性があります。すでに開いている接続には影響しません。 |
| refresh_at | この時刻以降に取り直してください。この時刻から、tenant-pass は次のウィンドウのパスワードを返します。PostgreSQL では、これが実質の期限です(誰かが取り直した時点で、以前のパスワードでは新しく接続できなくなります)。 |
POST/v1/job-tickets
後から動く処理が認証情報を取れるように、パスを同じテナントのジョブチケットに交換します。設定に job_tickets.signing_key があり、アプリケーションで "jobs": {"issue": true} を有効にしている必要があります。
- 使う場面
- エクスポートやインポートなど、ユーザーのリクエストから始まり、キューに入れて後から動かす処理を登録するとき(ジョブチケット)。
- 認証
- 必要です(アプリケーションの認証)。
- 主なエラー
- 400 invalid_request・400 invalid_pass・403 denied・429 rate_limited(下の説明と エラーコード)。
curl -s https://tenant-pass.internal:8443/v1/job-tickets \
--cacert tenant-pass-ca.pem \
-H "Authorization: Bearer $APP_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"pass\":\"$PASS\",\"ttl\":\"15m\"}" | フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| pass | string | はい | 呼び出したアプリケーション宛ての、有効なパス。 |
| ttl | string | いいえ | チケットに求める有効期間。Go の期間の書き方で指定します(例 "15m")。秒単位(小数は不可)で、1 分以上です。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"} - ・テナントは、発行の時点で確かめます(存在し、有効で、そのアプリケーションに許可されていること)。チケットには、正式なテナント ID が書かれます。
- ・チケットの発行も、認証情報と同じ「1 分あたりのテナント数」の制限に数えます。制限をすり抜ける手段にはなりません。
- ・1 つのパスと交換できるチケットは、レプリカごとに 1 枚です。2 回目は 403 denied(監査ログでは pass_already_used)になります。ほかの理由で発行に失敗したときは、そのパスをもう一度使えるので、リトライできます。
- ・アプリケーションでチケットの発行が有効になっていなければ、403 denied(監査ログでは job_issue_not_enabled)になります。そのほかのエラーは /v1/credentials と同じです。
GET/healthz
プロセスが動いているかを確かめます(ライブネス)。動いている間は 200 を返します。コンテナのヘルスチェックに使います。認証は不要で、GET と HEAD を受け付けます。
200 {"status":"ok"} GET/readyz
リクエストを受け付けられるかを確かめます(レディネス)。ロードバランサーが振り分け先を決めるのに使います。認証は不要です。ディレクトリとノードの確認結果は 5 秒間キャッシュするので、繰り返し呼ばれてもストアに負荷はかかりません。
200 {"status":"ready"} ディレクトリとすべてのノードが応答
503 {"status":"draining"} 停止中(SIGTERM)。ロードバランサーから外す
503 {"status":"unavailable"} ディレクトリかいずれかのノードに届かない エラーコード
エラーのレスポンスボディは、常に {"error": "<code>"} の形です。400・401・403 は、同じパスで何度試しても成功しない失敗です。クライアントはログに残し、そのリクエストをエラーにしてください。429・503 は、間を空けてリトライしてかまわない失敗です。
| 状態コード | error | 意味 | 扱い |
|---|---|---|---|
| 400 | invalid_request | リクエストボディが正しくありません。JSON のオブジェクトでない、pass と ticket の両方がある(またはどちらもない)、未知のフィールドや重複したフィールドがある、後ろに余分なデータが続く、などです。/v1/job-tickets では、pass がない場合や ttl が正しくない場合も含みます。 | リトライしない |
| 400 | invalid_pass | パスを受け付けませんでした。形式が正しくない、鍵が未知、署名が正しくない、クレームが正しくない、期限切れ、aud が呼び出したアプリケーションでない、のいずれかです。 | リトライしない |
| 400 | invalid_ticket | ジョブチケットを受け付けませんでした。理由はパスと同じで、有効期間がアプリケーションの jobs.max_ttl を超える場合も含みます。 | リトライしない |
| 401 | unauthorized | アプリケーションの認証に失敗しました。応答に WWW-Authenticate: Bearer realm="tenant-pass" が付きます。 | リトライしない |
| 403 | denied | テナントが存在しない、そのアプリケーションに許可されていない、テナントが無効、ストアがアカウントを拒否した、のいずれかです。チケットの場合は、そのアプリケーションでジョブチケットが有効になっていない、発行者が許可されていない、そのパスがすでにチケットと交換済み、も含みます。 | リトライしない |
| 404 | not_found | 存在しない URL です。 | リトライしない |
| 405 | method_not_allowed | メソッドが POST ではありません(応答に Allow: POST が付きます)。/healthz と /readyz は GET と HEAD を受け付けます。 | リトライしない |
| 413 | request_too_large | リクエストボディが 4096 バイトを超えています。 | リトライしない |
| 415 | unsupported_media_type | Content-Type が application/json ではありません(charset などのパラメーターは付けてかまいません)。 | リトライしない |
| 429 | rate_limited | 直近 1 分間に求めた異なるテナントの数が、このレプリカで limits.max_tenants_per_minute を超えました。チケットの場合は jobs.max_tenants_per_minute です。すでに求めたテナントを繰り返し求める分は制限しません。 | リトライ可 |
| 503 | busy | 同時に処理しているリクエスト数が limits.max_concurrent_requests に達しました。 | リトライ可 |
| 503 | unavailable | ディレクトリやストアのエラー、タイムアウト、このレプリカとストアのサーバーの時計のずれ、(KMS を使う場合)Vault に届かない、などです。別のレプリカでは成功することがあります。 | リトライ可 |
監査ログ
リクエストごとに、1 行の JSON を標準出力に書きます。/v1/credentials の記録は "msg":"credential"、/v1/job-tickets の記録は "msg":"job_ticket" です。成功は INFO、クライアント側のエラーは WARN、ディレクトリやストアの問題と安全性の問題(directory_error・store_error・account_refused・clock_skew・timeout・unknown_node・directory_mismatch・sign_error)は ERROR です。
{"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 |
| jti | パスまたはチケットの ID(ゲートウェイのログと突き合わせるのに使う) |
| result | ok か、返したエラーコード |
| reason | 失敗の理由(値は右の一覧) |
| error | エラーの分類(SQLSTATE、MySQL のエラー番号、Redis のエラーのプレフィックスなど)。サーバーが返したメッセージそのものは含めない |
| changed | ストアのパスワードを書き換えたかどうか |
| window・key_id | どのウィンドウ・どのローテーションキーの認証情報か |
reason の値
- リクエスト
- method, path, content_type, max_concurrent_requests, app_auth, body_size, body_read, body_json, pass_or_ticket, ttl
- パス
- pass_malformed, pass_unknown_key, pass_signature, pass_claims, pass_expired, pass_audience
- チケット
- ticket_malformed, ticket_unknown_key, ticket_signature, ticket_claims, ticket_expired, ticket_audience, ticket_lifetime, jobs_not_enabled, job_issue_not_enabled, issuer_not_allowed, pass_already_used
- 制限
- distinct_tenants, distinct_tenants_jobs
- ディレクトリ
- tenant_not_found, tenant_disabled, directory_error, directory_mismatch, unknown_node
- ストア
- account_refused, clock_skew, timeout, store_error, sign_error
決して書かないもの: パスワード、ベリファイア、ハッシュ、パス、ジョブチケット、アプリトークン、ローテーションキー。クライアントの IP アドレスも書きません(必要なら、ロードバランサーのログで確かめてください)。
アラートを出すべき条件は、運用 › 監視 にあります(account_refused、pass_signature・pass_unknown_key、pass_audience、distinct_tenants など)。
tenant-pass-gateway の応答(forward-auth)
付属のゲートウェイは、プロキシから認可の問い合わせを受けるサービスです。待ち受けるアドレス(既定 127.0.0.1:9180)では、どの URL へのリクエストも認可の問い合わせとして扱います。ヘルスチェック用の URL はないので、必要なら health_listen で別のアドレスを指定し、そこで GET /healthz を受け付けます。リクエストボディは使わず、ヘッダーだけを見ます。
プロキシが送るヘッダー
- X-Forwarded-Host(request.host_header。Envoy の ext_authz では Host)— 1 つだけ送ります。クライアントが送った値に追記するのではなく、プロキシが値を設定します
- Authorization: Bearer(authorization_header が true の場合)、または identity.cookie で指定した Cookie
- Sec-Fetch-Site・Sec-Fetch-Mode・Sec-Fetch-User(Cookie でセッションを扱う場合)
- Tenant-Pass-Gateway-Secret(caller_secret_file を設定した場合。ループバック以外で待ち受けるときは、これか mTLS が必須)
プロキシは、ゲートウェイを呼ぶ前に、クライアントが送った Tenant-Pass ヘッダー(と forward_claims のヘッダー)を取り除いてください。
| 状態コード | result | 意味 | パス |
|---|---|---|---|
| 200 | ok | パスを発行しました。プロキシは、応答の Tenant-Pass ヘッダー(と forward_claims のヘッダー)を、アプリケーションへ転送するリクエストにコピーします。 | あり |
| 200 | no_route | ホストがどのルートにも一致しません(テナントのアプリケーションではない)。 | なし |
| 200 | unauthenticated | セッションがない、無効、または期限切れです(unauthenticated: "pass_through" の場合)。 | なし |
| 401 | unauthenticated | 上と同じ状況です(unauthenticated: "deny" の場合)。 | なし |
| 401 | unauthorized_caller | caller_secret_file を設定しているのに、Tenant-Pass-Gateway-Secret ヘッダーがないか、値が違います。 | なし |
| 400 | bad_host | ホストのヘッダーがない、正しくない、またはあいまいです(複数の値、リスト、IP アドレス、ASCII 以外の文字)。 | なし |
| 403 | forbidden | セッションは有効ですが、別のテナントのものです。または、single_tenant_tokens / tenant_from_token の設定があるのに、トークンが 1 つのテナントを指していません。 | なし |
| 503 | busy | 同時に処理しているリクエスト数が max_concurrent_requests(既定 256)に達しました。 | なし |
| 503 | unavailable | 署名に失敗しました(Transit の鍵を使う場合など)。 | なし |
レスポンスボディは {"result": "<code>"} の形です。判定ごとに 1 行の JSON(host・app・tenant・result・reason)をログに書きます。トークンやパスはログに書きません。
コマンド
| tenant-pass … | 主なオプション | 内容 |
|---|---|---|
| serve | -config FILE | サーバーを起動する |
| check-config | -config FILE [-connect] | 設定ファイルを検査する。-connect を付けると、ディレクトリとすべてのノードにも接続して確かめる |
| gen-gateway-key | -private FILE -public FILE | ゲートウェイ用の Ed25519 の鍵ペアを作る(PEM 形式。秘密鍵は PKCS #8、公開鍵は PKIX) |
| gen-signing-key | -private FILE -public FILE | gen-gateway-key と同じ鍵ペアを作る(ジョブチケットやスケジューラーの署名用) |
| gen-rotation-key | -out FILE | ローテーションキーのファイルを作る |
| gen-app-token | アプリトークン(tpa_…)と、設定に書く token_sha256 を表示する | |
| sign-pass | -key -kid -aud (-host | -tenant) [-ttl 30s] | ゲートウェイの鍵でパスに署名する(試験や、連携の確認に使う) |
| sign-job-ticket | -key -kid -aud -tenant [-ttl 1h] | スケジューラーの鍵でジョブチケットに署名する(有効期間は 24 時間まで。tenant-pass 側の max_ttl も適用される) |
| version | バージョンを表示する |
ゲートウェイ側のコマンドは tenant-pass-gateway serve -config FILE、tenant-pass-gateway check-config -config FILE、tenant-pass-gateway version です。
Go の pass パッケージ
独自のゲートウェイやスケジューラーでは、github.com/kobesoft-labs/tenant-pass/pass を import すれば、そのまま署名できます。ほかの言語でも、PASETO v4 のライブラリと パスの形式 の仕様に沿って実装できます。その場合は、出力がテストベクターとバイト単位で一致することを確かめてください。
import "github.com/kobesoft-labs/tenant-pass/pass" s := pass.Signer{KeyID: "gw1", Key: privateKey} // ed25519.PrivateKey now := time.Now() p, err := s.Sign(pass.Claims{ Audience: "billing", Host: "acme.example.com", // または Tenant: "acme" IssuedAt: now, Expiry: now.Add(30 * time.Second), }) // スケジューラー: ジョブチケット(暗黙のアサーション tenant-pass/v1/job) t, err := s.SignJob(ctx, pass.JobClaims{Audience: "reports", Tenant: "acme", IssuedAt: now, Expiry: now.Add(time.Hour)})
| 名前 | 説明 |
|---|---|
| Signer.Sign / SignContext | パスに署名する。SignContext は context を受け取る版で、Vault Transit などで署名する Remote を設定した場合にも使える |
| Signer.SignJob | ジョブチケットに署名する |
| Verifier.Verify(p, audience) | パスを検証し、Claims を返す |
| ErrMalformed ほか | 検証の失敗を表すエラー。ErrMalformed・ErrUnknownKey・ErrSignature・ErrClaims・ErrExpired・ErrAudience(監査ログの pass_* に対応する) |
| ImplicitAssertion | パスの暗黙のアサーション "tenant-pass/v1/pass"(ジョブチケットでは JobImplicitAssertion の "tenant-pass/v1/job") |