Para usar o Kubernetes como provedor de identidade de carga de trabalho, troque um token projetado de uma conta de serviço do Kubernetes por um token de acesso da OpenAI de curta duração.
Este guia pressupõe que a projeção de tokens de contas de serviço esteja habilitada no Kubernetes. Esse recurso está disponível por padrão nas versões recentes do Kubernetes. A federação de identidades de carga de trabalho da OpenAI exige que os tokens projetados de contas de serviço sejam compatíveis com OIDC. A OpenAI não aceita tokens legados de contas de serviço do Kubernetes armazenados em Secrets.
Use um ServiceAccount do Kubernetes para a carga de trabalho que precisa chamar a API da OpenAI. Se você ainda não tiver um, crie-o com o comando a seguir.
kubectl create serviceaccount openai-wif --namespace default
Obtenha o emissor OIDC do cluster Kubernetes com o seguinte comando.
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
Mesmo que você envie o JWKS e a OpenAI não consulte o emissor OIDC para descobrir o JWKS, esse emissor deve corresponder ao configurado no provedor de identidade de carga de trabalho.
Obtenha o JWKS do cluster e salve o conjunto de chaves retornado. Você precisará dele para configurar o provedor de identidade de carga de trabalho.
kubectl get --raw /openid/v1/jwks
Configure o token projetado da conta de serviço com o público esperado pela OpenAI e um prazo de validade adequado à carga de trabalho. A OpenAI valida o emissor, a assinatura, o público e a expiração do token. Neste exemplo, o arquivo do token é montado em /var/run/secrets/tokens/token. O token tem como público https://api.openai.com/v1 e expira após 3600 segundos. Você pode usar outro público, desde que o público do token projetado corresponda ao configurado no provedor de identidade de carga de trabalho da OpenAI.
12345678910111213141516171819202122apiVersion: v1
kind: Pod
metadata:
name: openai-wif-app
namespace: default
spec:
serviceAccountName: openai-wif
containers:
- name: app
image: my-image
volumeMounts:
- name: ksa-token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: ksa-token
projected:
sources:
- serviceAccountToken:
path: token
audience: "https://api.openai.com/v1"
expirationSeconds: 3600
Antes de configurar a federação de identidades de carga de trabalho, decodifique localmente um token projetado de uma conta de serviço e examine suas declarações. Obtenha o token de um pod em execução no qual o arquivo do token projetado esteja montado e exporte-o como TOKEN.
TOKEN=$(kubectl exec -n default openai-wif-app -- cat /var/run/secrets/tokens/token)
export TOKEN
Em seguida, execute este script.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const parts = process.env.TOKEN?.split(".") ?? [];
if (parts.length !== 3) {
throw new Error("Expected a compact JWT with three segments");
}
if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {
throw new Error("JWT payload is not valid Base64URL");
}
const bytes = Buffer.from(parts[1], "base64url");
if (bytes.toString("base64url") !== parts[1]) {
throw new Error("JWT payload is not valid Base64URL");
}
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const claims = JSON.parse(decoded);
if (claims === null || Array.isArray(claims) || typeof claims !== "object") {
throw new Error("JWT payload is not a JSON object");
}
console.log(decoded);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import base64
import json
import os
import re
def reject_non_json_constant(value):
raise ValueError(f"JWT payload contains non-JSON constant: {value}")
parts = os.environ.get("TOKEN", "").split(".")
if len(parts) != 3:
raise ValueError("Expected a compact JWT with three segments")
payload = parts[1]
if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:
raise ValueError("JWT payload is not valid Base64URL")
padded_payload = payload + "=" * (-len(payload) % 4)
decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)
if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:
raise ValueError("JWT payload is not valid Base64URL")
decoded_text = decoded.decode("utf-8")
claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)
if not isinstance(claims, dict):
raise ValueError("JWT payload is not a JSON object")
print(decoded_text)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69package 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 main() {
parts := strings.Split(os.Getenv("TOKEN"), ".")
if len(parts) != 3 {
panic("Expected a compact JWT with three segments")
}
payload, err := decodeSegment(parts[1])
if err != nil {
panic(err)
}
formatted, err := json.MarshalIndent(payload, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(formatted))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77// 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 DecodeJwtPayloadExample {
private static final ObjectMapper JSON =
new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
private DecodeJwtPayloadExample() {}
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(decodeSegment(parts[1]));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59using 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(DecodeSegment(parts[1]));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26require "base64"
require "json"
parts = ENV.fetch("TOKEN", "").split(".", -1)
raise "Expected a compact JWT with three segments" unless parts.length == 3
unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1
raise "JWT payload is not valid Base64URL"
end
begin
payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))
rescue ArgumentError
raise "JWT payload is not valid Base64URL"
end
unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]
raise "JWT payload is not valid Base64URL"
end
payload.force_encoding(Encoding::UTF_8)
raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?
claims = JSON.parse(payload)
raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)
puts(payload)
Esse comando decodifica o conteúdo do JWT sem verificar a assinatura do token. Para tokens de produção, use um decodificador local e evite colar esses tokens em ferramentas de terceiros.
Depois de decodificado, um token projetado de uma conta de serviço do Kubernetes terá uma estrutura semelhante à do exemplo a seguir.
1234567891011121314{
"iss": "https://kubernetes.example.com",
"aud": ["https://api.openai.com/v1"],
"sub": "system:serviceaccount:default:openai-wif",
"iat": 1716235422,
"exp": 1716239022,
"kubernetes.io": {
"namespace": "default",
"serviceaccount": {
"name": "openai-wif",
"uid": "11111111-2222-3333-4444-555555555555"
}
}
}
Compare o conteúdo decodificado do token recebido com o emissor, o público e os valores de mapeamento configurados na OpenAI. É possível identificar a maioria dos problemas de configuração nas declarações iss, aud e sub antes de trocar o token.
Crie um provedor de identidade de carga de trabalho na OpenAI para o emissor do Kubernetes. Em seguida, adicione um mapeamento de conta de serviço que corresponda aos atributos do token projetado.
Configure primeiro o provedor de identidade de carga de trabalho e depois crie o mapeamento de conta de serviço.
-
Crie o provedor de identidade de carga de trabalho. Em Nome, insira um valor exclusivo, como kubernetes-prod. Em Descrição, insira um texto como Production Kubernetes cluster para ajudar os administradores a identificar o cluster.
-
Defina o emissor e o público. Em URL do emissor OIDC, insira o emissor retornado por kubectl get --raw /.well-known/openid-configuration | jq -r .issuer. Esse valor deve corresponder à declaração iss do token projetado. Em Público, insira a mesma string opaca configurada como público no volume do token projetado da conta de serviço. Neste exemplo, o valor é https://api.openai.com/v1.
-
Envie o JWKS do Kubernetes. Habilite Usar o JWKS enviado para verificar tokens. Em JSON do JWKS, insira a saída de kubectl get --raw /openid/v1/jwks. A OpenAI usa esse conjunto de chaves públicas para verificar os tokens projetados das contas de serviço do Kubernetes. Envie o conjunto completo, incluindo o campo keys que contém as chaves.
Observação: Para clusters Kubernetes auto-hospedados, a OpenAI oferece suporte apenas ao modo JWKS local. Envie o JWKS retornado pelo cluster. A OpenAI não consulta o emissor configurado para realizar a descoberta OIDC, mas ainda compara esse emissor com o campo iss do token.
Se o cluster fizer a rotação das chaves de assinatura das contas de serviço, atualize o JWKS enviado na configuração do provedor de identidade de carga de trabalho. A OpenAI rejeita tokens assinados com chaves ausentes do JWKS configurado. Se o JWKS contiver várias chaves públicas ativas, inclua o array keys completo.
-
Adicione transformações de atributos somente se precisar de atributos derivados para o mapeamento. Você pode usar declarações originais do token como sub, aud e iss diretamente nas asserções de mapeamento. Se quiser usar atributos transformados como critérios de correspondência em vez das declarações originais do token, o painel adicionará o prefixo openai. automaticamente. Por exemplo, insira workload_subject com a expressão assertion.sub para criar openai.workload_subject. A OpenAI ignora as declarações originais do token que já começam com openai. ao avaliar chaves de mapeamento com o prefixo openai., a menos que você configure uma transformação correspondente.
-
Crie um mapeamento de conta de serviço. Em Nome, insira um valor exclusivo para esse provedor de identidade de carga de trabalho, como openai-mapping-kubernetes. Em Descrição, insira um texto como Workload Identity Provider Mapping for Kubernetes Workloads para indicar qual carga de trabalho pode usar o mapeamento.
-
Use o identificador de sujeito da conta de serviço do Kubernetes como critério de correspondência. Em Chave, insira sub. Em Valor, insira system:serviceaccount:default:openai-wif. Para contas de serviço do Kubernetes, o identificador de sujeito tem o formato system:serviceaccount:<namespace>:<service-account-name>.
-
Escolha a conta de destino na OpenAI. Em Projeto, selecione o projeto da OpenAI ao qual pertence a conta de serviço de destino. Em Conta de serviço, selecione a conta de serviço da OpenAI que a carga de trabalho do Kubernetes pode usar, como kubernetes-prod-openai-wif. Marque Create a new service account in this project se quiser criar uma conta de serviço para esse mapeamento em vez de reutilizar uma existente.
-
Restrinja as permissões da API, se necessário. Em Permissões, selecione as permissões adequadas, como api.model.request e api.vector_store.read, para restringir ainda mais o acesso concedido pelos tokens emitidos com base nesse mapeamento. Deixe as permissões em branco para evitar uma restrição de escopo específica da federação de identidades de carga de trabalho. O token continua autorizando o acesso com as permissões da conta de serviço mapeada.
Configure o cliente do SDK da OpenAI para ler o token projetado do Kubernetes e trocá-lo por um token de acesso emitido pela OpenAI.
Use o caminho do arquivo montado, como /var/run/secrets/tokens/token, como origem do token de sujeito para o provedor de federação de identidades de carga de trabalho do SDK. O SDK troca esse token do Kubernetes por um token de acesso emitido pela OpenAI e usa o token da OpenAI para autenticar as requisições à API.
Os exemplos a seguir inicializam um cliente da OpenAI com um provedor personalizado de tokens de sujeito. O provedor lê o token projetado da conta de serviço do Kubernetes no caminho em que o arquivo foi montado e o usa como token de sujeito na federação de identidades de carga de trabalho.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40import { readFile } from "node:fs/promises";
import OpenAI from "openai";
const tokenPath = "/var/run/secrets/tokens/token";
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 mountedServiceAccountTokenProvider(path) {
return {
tokenType: "jwt",
getToken: async () => {
const token = (await readFile(path, "utf8")).trim();
if (!token) {
throw new Error("The mounted service account token file is empty.");
}
return token;
},
};
}
const client = new OpenAI({
workloadIdentity: {
identityProviderId,
serviceAccountId,
provider: mountedServiceAccountTokenProvider(tokenPath),
},
});
const response = await client.responses.create({
model: "gpt-5.6-terra",
input: "Say hello from Kubernetes workload identity federation.",
});
console.log(response.output_text);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import os
from pathlib import Path
from openai import OpenAI
from openai.auth import SubjectTokenProvider
TOKEN_PATH = "/var/run/secrets/tokens/token"
def mounted_service_account_token_provider(token_path: str) -> SubjectTokenProvider:
def get_token() -> str:
token = Path(token_path).read_text().strip()
if not token:
raise RuntimeError("The mounted service account token 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": mounted_service_account_token_provider(TOKEN_PATH),
},
)
response = client.responses.create(
model="gpt-5.6-terra",
input="Say hello from Kubernetes workload identity federation.",
)
print(response.output_text)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69package 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/secrets/tokens/token"
type mountedServiceAccountTokenProvider struct {
path string
}
func (p mountedServiceAccountTokenProvider) TokenType() auth.SubjectTokenType {
return auth.SubjectTokenTypeJWT
}
func (p mountedServiceAccountTokenProvider) GetToken(ctx context.Context, _ auth.HTTPDoer) (string, error) {
data, err := os.ReadFile(p.path)
if err != nil {
return "", &auth.SubjectTokenProviderError{
Provider: "kubernetes",
Message: "failed to read mounted service account token",
Cause: err,
}
}
token := strings.TrimSpace(string(data))
if token == "" {
return "", &auth.SubjectTokenProviderError{
Provider: "kubernetes",
Message: "mounted service account token 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: mountedServiceAccountTokenProvider{
path: tokenPath,
},
}),
)
response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: openai.ChatModelGPT4_1Mini,
Input: responses.ResponseNewParamsInputUnion{
OfString: openai.String("Say hello from Kubernetes workload identity federation."),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response.OutputText())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77import 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 KubernetesWorkloadIdentityExample {
private static final String TOKEN_PATH = "/var/run/secrets/tokens/token";
private KubernetesWorkloadIdentityExample() {}
static final class MountedServiceAccountTokenProvider implements SubjectTokenProvider {
private final Path tokenPath;
MountedServiceAccountTokenProvider(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(
"kubernetes", "failed to read mounted service account token", e);
}
if (token.isEmpty()) {
throw new SubjectTokenProviderException(
"kubernetes", "mounted service account token 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 MountedServiceAccountTokenProvider(TOKEN_PATH))
.build();
OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-5.6-terra")
.input("Say hello from Kubernetes 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()));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49require "openai"
TOKEN_PATH = "/var/run/secrets/tokens/token"
class MountedServiceAccountTokenProvider
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: "Mounted service account token is empty",
provider: "kubernetes"
)
end
token
rescue SystemCallError => e
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "Failed to read mounted service account token: #{e.message}",
provider: "kubernetes",
cause: e
)
end
end
provider = MountedServiceAccountTokenProvider.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 Kubernetes workload identity federation."
)
puts(response.output_text)
- Use um emissor OIDC estável. A URL do emissor deve corresponder à declaração
iss do token projetado da conta de serviço. É recomendável manter essa URL estável durante atualizações e operações de manutenção do cluster.
- Proteja as chaves de assinatura com cuidado. Qualquer pessoa com acesso às chaves de assinatura das contas de serviço do cluster pode emitir tokens que a OpenAI poderá aceitar.
- Use contas de serviço dedicadas às integrações com a OpenAI. Evite reutilizar contas de serviço que também dão acesso a infraestrutura ou aplicativos sem relação com essas integrações.
- Mantenha o JWKS enviado atualizado. No modo JWKS local, a OpenAI usa o JWKS configurado para validar tokens de identidade de carga de trabalho. Atualize o provedor de identidade de carga de trabalho antes de passar a usar novas chaves de assinatura.
- Mantenha as declarações personalizadas simples. Prefira usar como critérios de correspondência declarações padrão, como
sub e aud, ou atributos transformados derivados diretamente dessas declarações.
- Leve em conta quem controla cada namespace no seu modelo de segurança. Se os administradores de namespaces puderem criar contas de serviço, verifique se o escopo dos mapeamentos está delimitado corretamente para evitar elevações de privilégio indesejadas.
- Monitore as alterações no emissor e nas chaves de assinatura. Fazer a rotação das chaves de assinatura sem atualizar o JWKS do provedor de identidade de carga de trabalho pode causar falhas na troca de tokens.