tenant-pass

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}
フィールド型必須説明
passstringpass か ticket のどちらか一方ゲートウェイから受け取ったパス(パスの形式)。
ticketstringpass か ticket のどちらか一方ジョブチケット。アプリケーションに jobs の設定が必要です(ジョブチケット)。
repairboolいいえ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 ヘッダーからは判断しないでください。
storepostgres・mysql・redis のいずれか。
host・port・database接続先(設定の nodes[].connect)。database は、設定していない場合と Redis の場合は省略されます。
usernameアカウント名。
passwordbase64url の 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\"}"
フィールド型必須説明
passstringはい呼び出したアプリケーション宛ての、有効なパス。
ttlstringいいえチケットに求める有効期間。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意味扱い
400invalid_requestリクエストボディが正しくありません。JSON のオブジェクトでない、pass と ticket の両方がある(またはどちらもない)、未知のフィールドや重複したフィールドがある、後ろに余分なデータが続く、などです。/v1/job-tickets では、pass がない場合や ttl が正しくない場合も含みます。リトライしない
400invalid_passパスを受け付けませんでした。形式が正しくない、鍵が未知、署名が正しくない、クレームが正しくない、期限切れ、aud が呼び出したアプリケーションでない、のいずれかです。リトライしない
400invalid_ticketジョブチケットを受け付けませんでした。理由はパスと同じで、有効期間がアプリケーションの jobs.max_ttl を超える場合も含みます。リトライしない
401unauthorizedアプリケーションの認証に失敗しました。応答に WWW-Authenticate: Bearer realm="tenant-pass" が付きます。リトライしない
403deniedテナントが存在しない、そのアプリケーションに許可されていない、テナントが無効、ストアがアカウントを拒否した、のいずれかです。チケットの場合は、そのアプリケーションでジョブチケットが有効になっていない、発行者が許可されていない、そのパスがすでにチケットと交換済み、も含みます。リトライしない
404not_found存在しない URL です。リトライしない
405method_not_allowedメソッドが POST ではありません(応答に Allow: POST が付きます)。/healthz と /readyz は GET と HEAD を受け付けます。リトライしない
413request_too_largeリクエストボディが 4096 バイトを超えています。リトライしない
415unsupported_media_typeContent-Type が application/json ではありません(charset などのパラメーターは付けてかまいません)。リトライしない
429rate_limited直近 1 分間に求めた異なるテナントの数が、このレプリカで limits.max_tenants_per_minute を超えました。チケットの場合は jobs.max_tenants_per_minute です。すでに求めたテナントを繰り返し求める分は制限しません。リトライ可
503busy同時に処理しているリクエスト数が limits.max_concurrent_requests に達しました。リトライ可
503unavailableディレクトリやストアのエラー、タイムアウト、このレプリカとストアのサーバーの時計のずれ、(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
viapass か ticket
issuerチケットの場合に、署名した鍵の ID
jtiパスまたはチケットの ID(ゲートウェイのログと突き合わせるのに使う)
resultok か、返したエラーコード
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意味パス
200okパスを発行しました。プロキシは、応答の Tenant-Pass ヘッダー(と forward_claims のヘッダー)を、アプリケーションへ転送するリクエストにコピーします。あり
200no_routeホストがどのルートにも一致しません(テナントのアプリケーションではない)。なし
200unauthenticatedセッションがない、無効、または期限切れです(unauthenticated: "pass_through" の場合)。なし
401unauthenticated上と同じ状況です(unauthenticated: "deny" の場合)。なし
401unauthorized_callercaller_secret_file を設定しているのに、Tenant-Pass-Gateway-Secret ヘッダーがないか、値が違います。なし
400bad_hostホストのヘッダーがない、正しくない、またはあいまいです(複数の値、リスト、IP アドレス、ASCII 以外の文字)。なし
403forbiddenセッションは有効ですが、別のテナントのものです。または、single_tenant_tokens / tenant_from_token の設定があるのに、トークンが 1 つのテナントを指していません。なし
503busy同時に処理しているリクエスト数が max_concurrent_requests(既定 256)に達しました。なし
503unavailable署名に失敗しました(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 FILEgen-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")