Usa Kubernetes como proveedor de identidad de carga de trabajo al intercambiar un token proyectado de cuenta de servicio de Kubernetes por un token de acceso de OpenAI de corta duración.
Para Codex, usa esta página para obtener e inspeccionar el token proyectado. Luego, configura la identidad de carga de trabajo de Codex para que Codex use el archivo de token montado. La asignación de cuentas de servicio y los ejemplos del SDK de esta página se aplican a la API de OpenAI.
Esta guía supone que la proyección de tokens de cuentas de servicio de Kubernetes está habilitada, una función disponible de forma predeterminada en las versiones modernas de Kubernetes. La federación de identidades de carga de trabajo de OpenAI requiere tokens proyectados de cuentas de servicio compatibles con OIDC. No se admiten los tokens heredados de cuentas de servicio de Kubernetes almacenados en Secrets.
Usa una ServiceAccount de Kubernetes para la carga de trabajo que necesita llamar a la API de OpenAI. Si aún no tienes una, créala:
kubectl create serviceaccount openai-wif --namespace default
Obtén el emisor OIDC de tu clúster de Kubernetes:
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
Aunque cargues el JWKS y OpenAI no realice el descubrimiento de JWKS a través del emisor OIDC, este emisor debe coincidir con el configurado en el proveedor de identidad de carga de trabajo.
Obtén el JWKS del clúster y guarda el conjunto de claves devuelto. Lo necesitarás al configurar el proveedor de identidad de carga de trabajo:
kubectl get --raw /openid/v1/jwks
Configura el token proyectado de cuenta de servicio con la audiencia que espera OpenAI y un vencimiento adecuado para tu carga de trabajo. OpenAI valida el emisor, la firma, la audiencia y el vencimiento del token. En este ejemplo, el archivo de token se monta en /var/run/secrets/tokens/token, usa la audiencia https://api.openai.com/v1 y vence después de 3600 segundos. Puedes usar otra audiencia siempre que coincidan la audiencia del token proyectado y la del proveedor de identidad de carga de trabajo de 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 la federación de identidades de carga de trabajo, decodifica localmente un token proyectado de cuenta de servicio de muestra e inspecciona sus declaraciones. Desde un pod en ejecución que tenga montado el token proyectado, obtén el token y expórtalo como TOKEN:
TOKEN=$(kubectl exec -n default openai-wif-app -- cat /var/run/secrets/tokens/token)
export TOKEN
Luego, ejecuta 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)
Este comando decodifica el contenido del JWT sin verificar la firma del token. Usa un decodificador local para los tokens de producción y evita pegarlos en herramientas de terceros.
Un token proyectado de cuenta de servicio de Kubernetes decodificado tendrá un aspecto similar a este:
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"
}
}
}
Usa el contenido decodificado para comparar el token que recibiste con los valores de emisor, audiencia y asignación configurados en OpenAI. La mayoría de los problemas de configuración se pueden detectar en las declaraciones iss, aud y sub antes de intercambiar el token.
Crea un proveedor de identidad de carga de trabajo en OpenAI para el emisor de Kubernetes y luego agrega una asignación de cuenta de servicio que coincida con los atributos del token proyectado.
Primero configura el proveedor de identidad de carga de trabajo y luego crea la asignación de cuenta de servicio.
-
Crea el proveedor de identidad de carga de trabajo. Establece Nombre en un valor único, como kubernetes-prod. Usa una Descripción, como Production Kubernetes cluster, para ayudar a los administradores a identificar el clúster.
-
Establece el emisor y la audiencia. Establece URL del emisor OIDC en el emisor devuelto por kubectl get --raw /.well-known/openid-configuration | jq -r .issuer. Este valor debe coincidir con la declaración iss del token proyectado. Establece Audiencia en la misma cadena opaca de audiencia configurada en el volumen del token proyectado de cuenta de servicio. En este ejemplo, ese valor es https://api.openai.com/v1.
-
Carga el JWKS de Kubernetes. Habilita Usar el JWKS cargado para verificar tokens y luego establece JSON de JWKS en la salida de kubectl get --raw /openid/v1/jwks. OpenAI usa este conjunto de claves públicas para verificar los tokens proyectados de cuentas de servicio de Kubernetes. Carga el conjunto completo de claves, incluido el campo keys que las contiene.
Nota: para los clústeres de Kubernetes autoalojados, OpenAI solo admite el modo JWKS local. Carga el JWKS devuelto por tu clúster; OpenAI no realiza el descubrimiento OIDC a través del emisor configurado. OpenAI sigue comparando el emisor configurado con el campo iss del token.
Si tu clúster rota las claves de firma de las cuentas de servicio, actualiza el JWKS cargado en la configuración del proveedor de identidad de carga de trabajo. Los tokens firmados con claves que no estén presentes en el JWKS configurado se rechazan. Si el JWKS contiene varias claves públicas activas, incluye el arreglo keys completo.
-
Agrega transformaciones de atributos solo si necesitas atributos derivados para la asignación. Las declaraciones originales del token, como sub, aud y iss, se pueden usar directamente en las aserciones de asignación. Si planeas buscar coincidencias con atributos transformados en lugar de las declaraciones originales del token, el panel aplica el prefijo openai. automáticamente; por ejemplo, ingresa workload_subject con la expresión assertion.sub para crear openai.workload_subject. Las declaraciones originales del token que ya comienzan con openai. se ignoran para las claves de asignación openai., a menos que se configure una transformación correspondiente.
-
Crea una asignación de cuenta de servicio. Establece Nombre en un valor único dentro del proveedor de identidad de carga de trabajo, como openai-mapping-kubernetes. Usa una Descripción, como Workload Identity Provider Mapping for Kubernetes Workloads, para explicar qué carga de trabajo puede usar la asignación.
-
Configura la coincidencia con el sujeto de la cuenta de servicio de Kubernetes. Establece Clave en sub y Valor en system:serviceaccount:default:openai-wif. Para las cuentas de servicio de Kubernetes, el formato del sujeto es system:serviceaccount:<namespace>:<service-account-name>.
-
Elige el destino en OpenAI. Establece Proyecto en el proyecto de OpenAI al que pertenece la cuenta de servicio de destino. Establece Cuenta de servicio en la cuenta de servicio de OpenAI que puede usar la carga de trabajo de Kubernetes, como kubernetes-prod-openai-wif. Marca Create a new service account in this project si deseas crear una cuenta de servicio nueva para esta asignación en lugar de reutilizar una existente.
-
Restringe los permisos de la API si es necesario. Selecciona los Permisos adecuados, como api.model.request y api.vector_store.read, para restringir aún más los tokens de acceso emitidos a partir de esta asignación. Deja los permisos en blanco para evitar agregar una restricción de alcance específica de WIF; el token sigue autorizando el acceso como la cuenta de servicio asignada.
Configura tu cliente del SDK de OpenAI para que lea el token proyectado de Kubernetes y lo intercambie por un token de acceso emitido por OpenAI.
Usa la ruta del token montado, como /var/run/secrets/tokens/token, como origen del token de sujeto para el proveedor de federación de identidades de carga de trabajo del SDK. El SDK intercambia ese token de Kubernetes por un token de acceso emitido por OpenAI y usa el token de OpenAI para autenticar las solicitudes a la API.
Los siguientes ejemplos inicializan un cliente de OpenAI con un proveedor personalizado de tokens de sujeto. El proveedor lee el token proyectado de cuenta de servicio de Kubernetes desde la ruta del archivo montado y lo usa como token de sujeto para la federación de identidades de carga de trabajo.
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)
- Usa un emisor OIDC estable. La URL del emisor debe coincidir con la declaración
iss del token proyectado de cuenta de servicio y debería mantenerse estable durante las actualizaciones del clúster y las operaciones de mantenimiento.
- Protege cuidadosamente las claves de firma. Cualquier persona con acceso a las claves de firma de las cuentas de servicio del clúster puede emitir tokens que OpenAI podría aceptar.
- Usa cuentas de servicio dedicadas para las integraciones con OpenAI. Evita reutilizar cuentas de servicio que también se usen para acceder a infraestructura o aplicaciones no relacionadas.
- Mantén actualizado el JWKS cargado. OpenAI usa el JWKS configurado para validar los tokens de identidad de carga de trabajo en el modo JWKS local, así que actualiza el proveedor de identidad de carga de trabajo antes de rotar a nuevas claves de firma.
- Minimiza la complejidad de las declaraciones personalizadas. Da preferencia a las coincidencias con declaraciones estándar, como
sub y aud, o con atributos transformados derivados directamente de esas declaraciones.
- Considera la titularidad de los espacios de nombres como parte de tu modelo de seguridad. Si los administradores de los espacios de nombres pueden crear cuentas de servicio, asegúrate de que las asignaciones tengan un alcance adecuado para evitar una escalada de privilegios no intencional.
- Monitorea los cambios de emisor y de claves de firma. Rotar las claves de firma sin actualizar el JWKS del proveedor de identidad de carga de trabajo puede provocar fallas en el intercambio de tokens.