OpenAI SPIFFE 身份联合配置

2026-11-09 15:44 更新

将 SPIFFE JWT-SVID 换为短期 OpenAI 访问令牌,可让经 SPIRE 或兼容提供商验证的工作负载无须长期 API 密钥。Idira SWA 的 Agent 与令牌交付见 Idira 指南。

仅支持能按 JWT 主体令牌验证、含 issuer、audience、到期、签发时间及 JWKS 签名的 JWT-SVID;不支持 SPIFFE X.509-SVID 作为该交换的主体令牌。JWT-SVID 规范要求 sub、aud、exp,OpenAI 还要求 iss、iat 和头部 kid。

JWT-SVID 不是 OIDC ID token。SPIRE OIDC Discovery Provider 只是提供发现元数据与 JWKS,不改变 SPIFFE 语义,也不要求 OIDC 登录。见 JWT-SVID 规范与 Workload API 规范。

设置 SPIFFE

以下采用 SPIRE 术语,也适用于能提供 OpenAI 可验证 issuer 和签名公钥的兼容提供商。需要:稳定 SPIFFE ID(如 spiffe://example.org/ns/production/sa/openai-wif);单一 OpenAI 专属 audience(如 https://api.openai.com/v1 或自定不透明值);出现在 iss 的 JWT issuer URL;经发现或上传提供的公有 JWKS;工作负载侧从 Workload API 获取新鲜令牌的方法。

Audience 是精确匹配标识,不一定是接收令牌的端点。两端值相同即可。尽可能通过 SPIRE OIDC Discovery Provider 暴露 issuer;SPIRE Server 和 Discovery Provider 的 jwt_issuer 设为同一 HTTPS URL。

Server 配置:

来源示例 1(hcl):

## [中文] SPIRE Server与发现提供商使用相同HTTPS jwt_issuer;SPIFFE信任域不是JWT issuer。
server {
  trust_domain = "example.org"
  jwt_issuer   = "https://spire-oidc.example.org"
}

独立 Discovery Provider 配置:

来源示例 2(hcl):

## [中文] SPIRE Server与发现提供商使用相同HTTPS jwt_issuer;SPIFFE信任域不是JWT issuer。
## Relevant issuer fields only
domains    = ["spire-oidc.example.org"]
jwt_issuer = "https://spire-oidc.example.org"

发现提供商还需要 server_api、workload_api 或 file 等密钥来源,以及 ACME、TLS 证书或 Unix socket 等服务机制;完整选项见 SPIRE 文档。

信任域和 JWT issuer 不同:下例主体是 example.org 信任域的 SPIFFE ID,issuer 是 HTTPS URL:

来源示例 3(json):

中文说明:来源的JSON数据或配置/输出片段,保留字段和值;令牌声明中的iss/aud/sub等须对照真实工作负载,解码本身不等于验签。

{
  "sub": "spiffe://example.org/ns/production/sa/openai-wif",
  "iss": "https://spire-oidc.example.org"
}

关闭上传 JWKS 时,OpenAI 使用发现文档与 JWKS;无法访问 issuer 时可上传完整公钥集,仍检查配置 issuer 与 iss。SPIFFE 规范虽可省略 kid,OpenAI 必须用它从 JWKS 选密钥,因此须配置提供商带上此头。

在与应用相同的工作负载上下文请求受众一致的 JWT,因为 Workload API 授权取决于调用进程身份:

来源示例 4(bash):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
TOKEN=$(spire-agent api fetch jwt \
  -socketPath /run/spire/sockets/agent.sock \
  -audience "https://api.openai.com/v1" | sed -n '2p')
export TOKEN

有多个 SPIFFE ID 时指定所需身份:

来源示例 5(bash):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
TOKEN=$(spire-agent api fetch jwt \
  -socketPath /run/spire/sockets/agent.sock \
  -spiffeID "spiffe://example.org/ns/production/sa/openai-wif" \
  -audience "https://api.openai.com/v1" | sed -n '2p')
export TOKEN

检查令牌

把 JWT-SVID 导出为 TOKEN,本地查看头与声明:

来源示例 6(javascript):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
const parts = process.env.TOKEN?.split(".") ?? [];
if (parts.length !== 3) {
  throw new Error("Expected a compact JWT with three segments");
}

const decode = (segment) => {
  if (!/^[A-Za-z0-9_-]+$/.test(segment) || segment.length % 4 === 1) {
    throw new Error("JWT segment is not valid Base64URL");
  }
  const bytes = Buffer.from(segment, "base64url");
  if (bytes.toString("base64url") !== segment) {
    throw new Error("JWT segment is not valid Base64URL");
  }
  const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
  const value = JSON.parse(decoded);
  if (value === null || Array.isArray(value) || typeof value !== "object") {
    throw new Error("JWT segment is not a JSON object");
  }
  return decoded;
};

console.log("Header:");
console.log(decode(parts[0]));
console.log("\nPayload:");
console.log(decode(parts[1]));

来源示例 7(python):

## [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
import base64
import json
import os
import re


def reject_non_json_constant(value):
    raise ValueError(f"JWT segment contains non-JSON constant: {value}")


parts = os.environ.get("TOKEN", "").split(".")
if len(parts) != 3:
    raise ValueError("Expected a compact JWT with three segments")


def decode(segment):
    if re.fullmatch(r"[A-Za-z0-9_-]+", segment) is None or len(segment) % 4 == 1:
        raise ValueError("JWT segment is not valid Base64URL")
    padded_segment = segment + "=" * (-len(segment) % 4)
    decoded = base64.b64decode(padded_segment, altchars=b"-_", validate=True)
    if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != segment:
        raise ValueError("JWT segment is not valid Base64URL")
    decoded_text = decoded.decode("utf-8")
    value = json.loads(decoded_text, parse_constant=reject_non_json_constant)
    if not isinstance(value, dict):
        raise ValueError("JWT segment is not a JSON object")
    return decoded_text


print("Header:")
print(decode(parts[0]))
print("\nPayload:")
print(decode(parts[1]))

来源示例 8(go):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
package main

import (
    "bytes"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "os"
    "strings"
    "unicode/utf8"
)

func decodeSegment(segment string) (json.RawMessage, error) {
    if !isBase64URLSegment(segment) {
        return nil, fmt.Errorf("JWT segment is not valid Base64URL")
    }
    decoded, err := base64.RawURLEncoding.DecodeString(segment)
    if err != nil {
        return nil, err
    }
    if base64.RawURLEncoding.EncodeToString(decoded) != segment {
        return nil, fmt.Errorf("JWT segment is not valid Base64URL")
    }
    if !utf8.Valid(decoded) {
        return nil, fmt.Errorf("JWT segment is not valid UTF-8")
    }

    var value json.RawMessage
    if err := json.Unmarshal(decoded, &value); err != nil {
        return nil, err
    }
    if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {
        return nil, fmt.Errorf("JWT segment is not a JSON object")
    }
    return value, nil
}

func isBase64URLSegment(segment string) bool {
    if segment == "" || len(segment)%4 == 1 {
        return false
    }
    for _, character := range segment {
        if !('A' <= character && character <= 'Z') &&
            !('a' <= character && character <= 'z') &&
            !('0' <= character && character <= '9') &&
            character != '-' &&
            character != '_' {
            return false
        }
    }
    return true
}

func printJSON(label string, value json.RawMessage) error {
    formatted, err := json.MarshalIndent(value, "", "  ")
    if err != nil {
        return err
    }
    fmt.Printf("%s:\n%s\n", label, formatted)
    return nil
}

func main() {
    parts := strings.Split(os.Getenv("TOKEN"), ".")
    if len(parts) != 3 {
        panic("Expected a compact JWT with three segments")
    }

    header, err := decodeSegment(parts[0])
    if err != nil {
        panic(err)
    }
    payload, err := decodeSegment(parts[1])
    if err != nil {
        panic(err)
    }
    if err := printJSON("Header", header); err != nil {
        panic(err)
    }
    fmt.Println()
    if err := printJSON("Payload", payload); err != nil {
        panic(err)
    }
}

来源示例 9(java):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public final class DecodeJwtExample {
  private static final ObjectMapper JSON =
      new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);

  private DecodeJwtExample() {}

  static String decodeUtf8(byte[] bytes) throws IOException {
    try {
      return StandardCharsets.UTF_8
          .newDecoder()
          .onMalformedInput(CodingErrorAction.REPORT)
          .onUnmappableCharacter(CodingErrorAction.REPORT)
          .decode(ByteBuffer.wrap(bytes))
          .toString();
    } catch (CharacterCodingException exception) {
      throw new IOException("JWT segment is not valid UTF-8", exception);
    }
  }

  static String decodeSegment(String segment) throws IOException {
    if (!isBase64UrlSegment(segment)) {
      throw new IllegalArgumentException("JWT segment is not valid Base64URL");
    }
    byte[] bytes = Base64.getUrlDecoder().decode(segment);
    if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {
      throw new IllegalArgumentException("JWT segment is not valid Base64URL");
    }
    String decoded = decodeUtf8(bytes);
    JsonNode value = JSON.readTree(decoded);
    if (value == null || value.isMissingNode() || !value.isObject()) {
      throw new IOException("JWT segment is not a JSON object");
    }
    return decoded;
  }

  static boolean isBase64UrlSegment(String segment) {
    if (segment.isEmpty() || segment.length() % 4 == 1) {
      return false;
    }
    return segment
        .chars()
        .allMatch(
            character ->
                character >= 'A' && character <= 'Z'
                    || character >= 'a' && character <= 'z'
                    || character >= '0' && character <= '9'
                    || character == '-'
                    || character == '_');
  }

  static String[] requireCompactJwt(String token) {
    if (token == null) {
      throw new IllegalArgumentException("Expected a compact JWT with three segments");
    }
    String[] parts = token.split("\\.", -1);
    if (parts.length != 3) {
      throw new IllegalArgumentException("Expected a compact JWT with three segments");
    }
    return parts;
  }

  public static void main(String[] args) throws IOException {
    String[] parts = requireCompactJwt(System.getenv("TOKEN"));
    System.out.println("Header:");
    System.out.println(decodeSegment(parts[0]));
    System.out.println("\nPayload:");
    System.out.println(decodeSegment(parts[1]));
  }
}

来源示例 10(csharp):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
using System.Text;
using System.Text.Json;

static string DecodeSegment(string segment)
{
    if (
        segment.Length % 4 == 1 ||
        segment.Any(
            character =>
                !(
                    character is >= 'A' and <= 'Z' ||
                    character is >= 'a' and <= 'z' ||
                    character is >= '0' and <= '9' ||
                    character is '-' or '_'
                )
        )
    )
    {
        throw new FormatException("JWT segment is not valid Base64URL");
    }

    byte[] decoded = Convert.FromBase64String(
        segment.Replace('-', '+').Replace('_', '/') +
        new string('=', (4 - segment.Length % 4) % 4)
    );
    string canonicalSegment = Convert
        .ToBase64String(decoded)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');
    if (canonicalSegment != segment)
    {
        throw new FormatException("JWT segment is not valid Base64URL");
    }
    string decodedJson = new UTF8Encoding(false, true).GetString(decoded);
    using JsonDocument document = JsonDocument.Parse(decodedJson);
    if (document.RootElement.ValueKind is not JsonValueKind.Object)
    {
        throw new FormatException("JWT segment is not a JSON object");
    }
    return decodedJson;
}

string? token = Environment.GetEnvironmentVariable("TOKEN");
if (token is null)
{
    throw new InvalidOperationException(
        "Expected a compact JWT with three segments"
    );
}
string[] parts = token.Split('.');
if (parts.Length != 3)
{
    throw new InvalidOperationException(
        "Expected a compact JWT with three segments"
    );
}

Console.WriteLine("Header:");
Console.WriteLine(DecodeSegment(parts[0]));
Console.WriteLine("\nPayload:");
Console.WriteLine(DecodeSegment(parts[1]));

来源示例 11(ruby):

## [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
require "base64"
require "json"

parts = ENV.fetch("TOKEN", "").split(".", -1)
raise "Expected a compact JWT with three segments" unless parts.length == 3

decode = lambda do |segment|
  unless segment.match?(/\A[A-Za-z0-9_-]+\z/) && segment.length % 4 != 1
    raise "JWT segment is not valid Base64URL"
  end

  padded = segment.ljust((segment.length + 3) & ~3, "=")
  begin
    decoded = Base64.urlsafe_decode64(padded)
  rescue ArgumentError
    raise "JWT segment is not valid Base64URL"
  end
  unless Base64.urlsafe_encode64(decoded, padding: false) == segment
    raise "JWT segment is not valid Base64URL"
  end

  decoded.force_encoding(Encoding::UTF_8)
  raise "JWT segment is not valid UTF-8" unless decoded.valid_encoding?

  value = JSON.parse(decoded)
  raise "JWT segment is not a JSON object" unless value.is_a?(Hash)

  decoded
end

puts("Header:")
puts(decode.call(parts[0]))
puts("\nPayload:")
puts(decode.call(parts[1]))

仅解码,不验证签名,不向第三方提交生产令牌。头部和载荷示例:

来源示例 12(json):

中文说明:来源的JSON数据或配置/输出片段,保留字段和值;令牌声明中的iss/aud/sub等须对照真实工作负载,解码本身不等于验签。

{
  "alg": "ES256",
  "kid": "jwt-svid-key-1"
}

来源示例 13(json):

中文说明:来源的JSON数据或配置/输出片段,保留字段和值;令牌声明中的iss/aud/sub等须对照真实工作负载,解码本身不等于验签。

{
  "iss": "https://spire-oidc.example.org",
  "aud": ["https://api.openai.com/v1"],
  "sub": "spiffe://example.org/ns/production/sa/openai-wif",
  "iat": 1716235422,
  "exp": 1716235722
}

对照头部 alg、kid,载荷 iss、aud、sub、iat、exp;alg 取决于真实 SPIRE 签名密钥设置。

设置身份联合

为 JWT-SVID issuer 创建提供商,再映射可信 SPIFFE ID。

设置工作负载身份提供商

  1. 唯一 Name 如 spiffe-prod,Description 标明生产工作负载。
  2. Issuer 取实际 iss,如 https://spire-oidc.example.org;Audience 与 Workload API 请求相同,示例 https://api.openai.com/v1。
  3. 可访问发现提供商时关闭上传 JWKS,使用发现到的公钥;不可达时启用上传,将完整公有 JWKS 对象及 keys 数组保存,不包含私钥。
  4. 直接匹配 sub 无须转换,仅按需派生,见共享 CEL 行为。

设置服务账户映射

  1. Name 唯一,如 production-openai-wif,Description 描述用途。
  2. Key sub,Value 如 spiffe://example.org/ns/production/sa/openai-wif。高权限优先精确 ID;只有前缀下全部身份都应访问时才用末尾通配,例如 spiffe://example.org/ns/production/sa/*。
  3. 选择目标 Project 与 Service account,如 spiffe-prod-openai-wif;可勾选创建新服务账户。
  4. Permissions 用 api.model.request、api.vector_store.read 等收窄。留空不附加 WIF scope 限制,仍按服务账户授权。

在代码中使用令牌

自定义主体提供函数应返回新鲜 JWT-SVID。示例假设集成在到期前刷新 /var/run/spiffe/openai.jwt,该文件仅工作负载可读;可用语言库在函数内部直接取 Workload API,避免陈旧文件。

设置 OPENAI_IDENTITY_PROVIDER_ID 和 OPENAI_SERVICE_ACCOUNT_ID。文件包含外部令牌,两 ID 分别指定提供商与目标账户,OpenAI 基于声明查找对应映射。

来源示例 14(javascript):

// [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
import { readFile } from "node:fs/promises";
import OpenAI from "openai";

const tokenPath = "/var/run/spiffe/openai.jwt";
const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;
const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;

if (!identityProviderId || !serviceAccountId) {
  throw new Error(
    "Set OPENAI_IDENTITY_PROVIDER_ID and OPENAI_SERVICE_ACCOUNT_ID"
  );
}

function spiffeJwtSvidProvider(path) {
  return {
    tokenType: "jwt",
    getToken: async () => {
      const token = (await readFile(path, "utf8")).trim();
      if (!token) {
        throw new Error("The SPIFFE JWT-SVID file is empty.");
      }
      return token;
    },
  };
}

const client = new OpenAI({
  workloadIdentity: {
    identityProviderId,
    serviceAccountId,
    provider: spiffeJwtSvidProvider(tokenPath),
  },
});

const response = await client.responses.create({
  model: "gpt-5.6-terra",
  input: "Say hello from SPIFFE workload identity federation.",
});

console.log(response.output_text);

来源示例 15(python):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
import os
from pathlib import Path

from openai import OpenAI
from openai.auth import SubjectTokenProvider

TOKEN_PATH = "/var/run/spiffe/openai.jwt"


def spiffe_jwt_svid_provider(token_path: str) -> SubjectTokenProvider:
    def get_token() -> str:
        token = Path(token_path).read_text().strip()
        if not token:
            raise RuntimeError("The SPIFFE JWT-SVID file is empty.")
        return token

    return {"token_type": "jwt", "get_token": get_token}


client = OpenAI(
    workload_identity={
        "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],
        "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],
        "provider": spiffe_jwt_svid_provider(TOKEN_PATH),
    },
)

response = client.responses.create(
    model="gpt-5.6-terra",
    input="Say hello from SPIFFE workload identity federation.",
)

print(response.output_text)

来源示例 16(go):

// [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "strings"

    "github.com/openai/openai-go/v3"
    "github.com/openai/openai-go/v3/auth"
    "github.com/openai/openai-go/v3/option"
    "github.com/openai/openai-go/v3/responses"
)

const tokenPath = "/var/run/spiffe/openai.jwt"

type spiffeJWTSVIDProvider struct {
    path string
}

func (p spiffeJWTSVIDProvider) TokenType() auth.SubjectTokenType {
    return auth.SubjectTokenTypeJWT
}

func (p spiffeJWTSVIDProvider) GetToken(ctx context.Context, _ auth.HTTPDoer) (string, error) {
    data, err := os.ReadFile(p.path)
    if err != nil {
        return "", &auth.SubjectTokenProviderError{
            Provider: "spiffe",
            Message:  "failed to read SPIFFE JWT-SVID",
            Cause:    err,
        }
    }

    token := strings.TrimSpace(string(data))
    if token == "" {
        return "", &auth.SubjectTokenProviderError{
            Provider: "spiffe",
            Message:  "SPIFFE JWT-SVID file is empty",
        }
    }

    return token, nil
}

func main() {
    client := openai.NewClient(
        option.WithWorkloadIdentity(auth.WorkloadIdentity{
            IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),
            ServiceAccountID:   os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),
            Provider: spiffeJWTSVIDProvider{
                path: tokenPath,
            },
        }),
    )

    response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
        Model: openai.ChatModelGPT4_1Mini,
        Input: responses.ResponseNewParamsInputUnion{
            OfString: openai.String("Say hello from SPIFFE workload identity federation."),
        },
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(response.OutputText())
}

来源示例 17(java):

// [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.openai.auth.SubjectTokenProvider;
import com.openai.auth.SubjectTokenType;
import com.openai.auth.WorkloadIdentity;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.http.HttpClient;
import com.openai.errors.SubjectTokenProviderException;
import com.openai.models.responses.ResponseCreateParams;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;

public final class SpiffeWorkloadIdentityExample {
  private static final String TOKEN_PATH = "/var/run/spiffe/openai.jwt";

  private SpiffeWorkloadIdentityExample() {}

  static final class SpiffeJwtSvidProvider implements SubjectTokenProvider {
    private final Path tokenPath;

    SpiffeJwtSvidProvider(String tokenPath) {
      this.tokenPath = Path.of(tokenPath);
    }

    @Override
    public SubjectTokenType tokenType() {
      return SubjectTokenType.JWT;
    }

    @Override
    public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {
      String token;
      try {
        token = Files.readString(tokenPath).trim();
      } catch (Exception e) {
        throw new SubjectTokenProviderException("spiffe", "failed to read SPIFFE JWT-SVID", e);
      }

      if (token.isEmpty()) {
        throw new SubjectTokenProviderException("spiffe", "SPIFFE JWT-SVID file is empty", null);
      }

      return token;
    }

    @Override
    public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {
      return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));
    }
  }

  public static void main(String[] args) {
    WorkloadIdentity workloadIdentity =
        WorkloadIdentity.builder()
            .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))
            .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))
            .provider(new SpiffeJwtSvidProvider(TOKEN_PATH))
            .build();

    OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();

    ResponseCreateParams params =
        ResponseCreateParams.builder()
            .model("gpt-5.6-terra")
            .input("Say hello from SPIFFE workload identity federation.")
            .build();

    client.responses().create(params).output().stream()
        .flatMap(item -> item.message().stream())
        .flatMap(message -> message.content().stream())
        .flatMap(content -> content.outputText().stream())
        .forEach(outputText -> System.out.println(outputText.text()));
  }
}

来源示例 18(ruby):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
require "openai"

TOKEN_PATH = "/var/run/spiffe/openai.jwt"

class SpiffeJWTSVIDProvider
  include OpenAI::Auth::SubjectTokenProvider

  def initialize(token_path:)
    @token_path = token_path
  end

  def token_type
    OpenAI::Auth::TokenType::JWT
  end

  def get_token
    token = File.read(@token_path).strip
    if token.empty?
      raise OpenAI::Errors::SubjectTokenProviderError.new(
        message: "SPIFFE JWT-SVID file is empty",
        provider: "spiffe"
      )
    end
    token
  rescue SystemCallError => e
    raise OpenAI::Errors::SubjectTokenProviderError.new(
      message: "Failed to read SPIFFE JWT-SVID: #{e.message}",
      provider: "spiffe",
      cause: e
    )
  end
end

provider = SpiffeJWTSVIDProvider.new(token_path: TOKEN_PATH)

workload_identity = OpenAI::Auth::WorkloadIdentity.new(
  identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),
  service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),
  provider: provider
)

client = OpenAI::Client.new(workload_identity: workload_identity)

response = client.responses.create(
  model: "gpt-5.6-terra",
  input: "Say hello from SPIFFE workload identity federation."
)

puts(response.output_text)

SPIFFE 最佳实践

  • 使用 JWT-SVID;X.509-SVID 可用于 mTLS,但不受此交换端点支持。
  • 使用单一专属 audience,避免整个信任域/环境的宽泛受众。
  • 尽量精确 SPIFFE ID,仅有意共享边界时通配。
  • 短寿命降低 bearer 重放风险,OpenAI 令牌不会比外部主体令牌活得更久。
  • 轮换时同时发布新旧公钥,或在新 kid 签发前更新上传 JWKS。
  • 同步 SPIRE 与工作负载时钟,避免未到生效时间、过旧或到期误拒绝。
  • 保护 Workload API socket,能取 JWT 的进程可尝试交换访问权。
  • OpenAI 账户按应用/环境隔离,不跨无关工作负载共享高权限账户。
  • 监控 issuer、audience、签名与映射不匹配。
以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号