Claude Media
ClaudeのWIFをSPIFFE/SPIREと連携する手順

ClaudeのWIFをSPIFFE/SPIREと連携する手順

SPIREが発行するJWT-SVIDをClaude APIの認証に使う設定を、OIDC Discovery ProviderとKubernetesの構成で説明する。

SPIFFE対応が持つ意味

Workload Identity Federation(WIF)が対応するIdPは、AWS・Google Cloud・GitHub Actionsのような単一クラウドのネイティブIDだけではありません。SPIFFEはCNCFが定めるワークロードID発行の標準規格で、SPIREはそのオープンソース実装です。ChatGPTやGeminiなど他社の主要なLLM APIでSPIFFE連携を明示的に文書化しているものは見当たらず、この対応はマルチクラウド・オンプレ混在環境を持つ組織にとって珍しい選択肢になります。

SPIFFEはワークロードごとにspiffe://<trust-domain>/<path>という安定したID URIを割り当て、SPIREはそれをJWT-SVID(JWT形式のSVID)として必要な時に発行します。JWT-SVIDは普通の署名付きJWTで、subクレームがSPIFFE ID、audクレームは取得時にワークロード側が指定します。AnthropicはOIDC互換のJWT-SVIDを発行するSPIFFE実装であれば、SPIREに限らずどれでも連携できます。

前提条件

  • ワークロードIDが発行済みのSPIFFE環境(この記事の例はSPIRE Server/Agent)と、Claude APIを呼ぶワークロード向けの登録エントリ
  • trust domain用のOIDC discoveryエンドポイント(SPIREではOIDC Discovery Provider)が公開HTTPSで到達可能であること、または登録用にエクスポート済みのJWKS
  • JWT-SVIDのissクレームを、federation issuerのissuer_urlとして登録する値に一致させる設定(discoveryモードならdiscoveryエンドポイントの公開URL、SPIREではjwt_issuerサーバー設定)
  • JWT-SVID(X.509-SVIDではない)がワークロードから取得可能であること
  • Claude Consoleでサービスアカウント・federation issuer・federation ruleを作成できる権限

JWT-SVID取得時にリクエストするaudience値は常にhttps://api.anthropic.comです。spiffe-helperのjwt_audience、Workload APIのFetchJWTSVID呼び出し、federation ruleのaudienceマッチャーのすべてでこの値を使います。

SPIREを構成する

このセクションはSPIRE固有です。別のSPIFFE実装を使う場合は、そのプロバイダーのOIDC discoveryとJWT-SVID取得の手順に従い、Anthropic側の設定セクションから読み進めてください。

JWT issuerを揃える

AnthropicはJWT-SVIDのissクレームを登録済みのfederation issuerと突き合わせ、そのissuerのdiscoveryドキュメントからJWKSを取得して署名を検証します。ここで2つのSPIRE設定が同じURLを指している必要があります。SPIRE Serverのjwt_issuer(発行されるすべてのJWT-SVIDのissクレームになる値)と、OIDC Discovery Providerのdomains(discoveryドキュメントとJWKSを配信するホスト)です。

server {
    trust_domain         = "prod.example.com"
    jwt_issuer           = "https://oidc-discovery.prod.example.com"
    default_jwt_svid_ttl = "5m"
    # ...
}

SPIREの既定のJWT-SVID寿命は5分と短く、継続的なローテーションが前提です。Anthropicのトークン交換エンドポイントは、identity tokenの寿命がfederation issuerに設定した上限(既定1時間)を超えると拒否します。これはSPIRE固有ではなく、すべてのSPIFFE実装に共通するルールなので、default_jwt_svid_ttlやエントリ単位の上書き値をこの上限以下に保ちます。

OIDC Discovery Provider側の設定にも同じホスト名をdomainsに含め、SPIRE ServerのAPIソケットに到達できるようにします。

domains = ["oidc-discovery.prod.example.com"]
 
server_api {
    address = "unix:///run/spire/sockets/private/api.sock"
}

ワークロードを登録する

Claude APIを呼ぶ各ワークロードには、実行時のセレクタをSPIFFE IDにマッピングするSPIRE登録エントリが必要です。すでに登録済みならそのSPIFFE IDをfederation ruleのsubject_prefixに使い、未登録なら新規作成します。Kubernetes Podの場合、セレクタは通常namespaceとKubernetesサービスアカウントです。

# NODE_UIDはノードのUIDに置き換える
#   kubectl get node <node-name> -o jsonpath='{.metadata.uid}'
spire-server entry create \
    -spiffeID spiffe://prod.example.com/ns/inference/sa/worker \
    -parentID spiffe://prod.example.com/spire/agent/k8s_psat/prod-cluster/NODE_UID \
    -selector k8s:ns:inference \
    -selector k8s:sa:worker

spiffe-helperを動かす

spiffe-helperは、SPIRE Agentのソケットに接続してJWT-SVIDを取得し、ファイルに書き出して期限前に再取得するサイドカーです。

agent_address = "/run/spire/sockets/agent.sock"
cert_dir      = "/var/run/secrets/anthropic.com"
daemon_mode   = true
 
jwt_svids = [{
    jwt_audience       = "https://api.anthropic.com"
    jwt_svid_file_name = "token"
}]

Kubernetesでは、spiffe-helperをアプリケーションコンテナとemptyDir(medium: Memory)を共有するサイドカーとして動かし、ANTHROPIC_IDENTITY_TOKEN_FILE=/var/run/secrets/anthropic.com/tokenをアプリ側にセットします。SVIDがノードのディスクに書かれないよう、共有ボリュームはメモリ上に置きます。

Anthropic側でissuerとruleを作る

Claude ConsoleでSettings → Workload identityからConnect workloadCustom OIDCを選びます。ウィザードが作るリソースは、Admin APIにコードから送っても同じ結果になります。

federation issuer: discoveryモードでOIDC Discovery Providerの公開URLを登録します。

{
  "name": "spire-prod",
  "issuer_url": "https://oidc-discovery.prod.example.com",
  "jwks": { "type": "discovery" }
}

discovery providerが外部から到達できない場合は、自分でJWKSを取得し(curl https://oidc-discovery.prod.example.com/keys)、inlineモードで登録します。inlineモードではissuer_urlはJWT-SVIDのissクレームとの文字列比較にのみ使われ、Anthropicはそこへ接続しに行きません。

federation rule: JWT-SVIDのsub(SPIFFE ID)とspiffe-helperに要求させたaudを照合します。

{
  "name": "spire-inference-worker",
  "issuer_id": "fdis_...",
  "match": {
    "subject_prefix": "spiffe://prod.example.com/ns/inference/sa/worker",
    "audience": "https://api.anthropic.com"
  },
  "target": { "type": "service_account", "service_account_id": "svac_..." },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}

token_lifetime_secondsはAnthropicアクセストークンの寿命であり、JWT-SVID自体の寿命ではありません。アクセストークンの更新はSDKが自動で処理します。

トークンを取得してClaude APIを呼ぶ

Anthropic SDKは、spiffe-helperが維持するファイルからJWT-SVIDを読む方法と、SPIFFE Workload APIを直接呼ぶcallableを渡す方法のどちらにも対応します。ファイル経由が最もシンプルで、どのSDK言語でも動きます。

JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE")
 
ACCESS_TOKEN=$(curl -sS https://api.anthropic.com/v1/oauth/token \
  -H "content-type: application/json" \
  --data @- <<JSON | jq -r .access_token
{
  "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
  "assertion": "$JWT",
  "federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
  "organization_id": "$ANTHROPIC_ORGANIZATION_ID",
  "service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
  "workspace_id": "$ANTHROPIC_WORKSPACE_ID"
}
JSON
)
 
curl https://api.anthropic.com/v1/messages \
  -H "authorization: Bearer $ACCESS_TOKEN" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"model": "claude-opus-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello, Claude"}]}' \
  | jq -r '.content[] | select(.type == "text") | .text'

SPIRE Agentのソケットに直接つなぐSPIFFE Workload APIクライアントを持つ言語(Pythonのpy-spiffe、Goのgo-spiffe)では、spiffe-helperを省いてSDKにcallableを渡す構成も選べます。SDKはトークン交換のたびにこのcallableを呼ぶため、常に有効期限内のSVIDを提示できます。

検証方法

SDKを組み込む前に、SPIRE AgentからJWT-SVIDを直接取得し、federation ruleが期待するクレームと一致しているかを確認します。

spire-agent api fetch jwt \
    -audience https://api.anthropic.com \
    -socketPath /run/spire/sockets/agent.sock \
    -output json \
  | jq -r '.[0].svids[0].svid' \
  | jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson'

Workload APIは呼び出し元のプロセスをattestationで確認するため、Kubernetesの登録エントリなら該当Podの中(kubectl exec経由など)で、VM・ベアメタルならunix:セレクタに一致するユーザー・プロセスとして実行します。attestation対象外のホストシェルから実行するとno identity issuedになるのが、検証時に最も多い失敗です。issがdiscovery providerのURL、subがワークロードのSPIFFE ID、audhttps://api.anthropic.comが含まれることを確認したら、上のcURL例を実行します。成功時はsk-ant-oat01-で始まるaccess_tokenが返り、失敗時は401authentication_error(Authentication failed)が返るので、Claude Consoleの認証履歴ページで拒否理由を確認します。SPIRE側で最も多い原因は、SPIRE Serverのjwt_issuerと登録したfederation issuerのURLの不一致です。

ルールのスコープを絞る

SPIFFE IDのパス規則は運用者が自由に決めるため、subject_prefixは登録エントリで使っているパス構成に合わせます。よくある構成は、Kubernetesのspire-controller-managerがClusterSPIFFEIDリソースで自動生成するspiffe://<trust-domain>/ns/<namespace>/sa/<service-account>と、VM・ベアメタル向けのspiffe://<trust-domain>/host/<hostname>/<service>です。

  • 1ワークロードに固定: subject_prefixを末尾*なしの完全なSPIFFE IDにする
  • audienceを必ず設定: ルールにaudienceを必須にし、spiffe-helper(またはWorkload API呼び出し)にも同じ値を要求させ、別の依拠者向けに発行されたSVIDを拒否する
  • パスセグメントで絞る: spiffe://prod.example.com/ns/inference/*のようにnamespace単位で許可し、1つのルールを広げるのではなくnamespaceごとに別ルール・別サービスアカウントを作る
  • trust domainごとに1 issuer: SPIREのtrust domainごとに署名鍵とOIDC Discovery Providerが異なるため、それぞれ別のfederation issuerとして登録し、対応するSPIFFE IDにルールを紐づける

まとめ

SPIFFE/SPIRE連携の要点は、SPIRE Serverのjwt_issuerとOIDC Discovery Providerの公開URLを一致させること、JWT-SVIDの短い寿命に合わせてspiffe-helperで継続的にローテーションすること、そしてsubject_prefixをSPIFFE IDのパス構成に合わせて絞ることの3つです。マルチクラウド・オンプレ混在の環境で単一のワークロードID基盤をすでに運用しているなら、クラウドごとに別々のWIF設定を組むよりSPIFFE経由で統一した方が、federation issuerの数を減らせます。OktaのようなIdPと役割を切り分けたい場合は、ClaudeのWIFをOktaと連携する手順も合わせて確認してください。

この記事を共有:XはてブLinkedIn