Utilisez Idira Secure Workload Access (SWA) pour appeler l’API OpenAI sans stocker de clé API OpenAI. SWA attribue un identifiant SPIFFE à votre charge de travail et émet un JWT-SVID, un jeton signé qui représente cette identité. Le SDK OpenAI échange ce jeton contre un jeton d’accès OpenAI de courte durée qui confère les autorisations du compte de service associé.
Ce guide explique comment configurer l’API OpenAI. Pour Codex, suivez les étapes de ce guide pour fournir le jeton à la charge de travail, puis suivez les instructions de la section Utilisation d’une identité de charge de travail avec Codex.
Vous devez disposer des éléments suivants :
- Un déploiement SWA avec un domaine de confiance et une politique d’attestation qui attribue l’identifiant SPIFFE attendu à votre charge de travail.
- Un accès, depuis la charge de travail, au socket de la Workload API de l’agent SWA.
- Les autorisations nécessaires pour gérer les fournisseurs d’identités de charges de travail, les associations de comptes de service, les projets et les comptes de service dans votre organisation OpenAI.
- Un SDK OpenAI qui prend en charge les informations d’identification des charges de travail.
Terminez la configuration de l’infrastructure SWA en suivant les prérequis indiqués dans le guide d’Idira sur l’intégration avec OpenAI.
Récupérez les valeurs de jwt.discovery_endpoints.oidc_discovery_url et de jwt.discovery_endpoints.jwks_uri pour votre domaine de confiance. Pour les trouver, suivez les instructions d’Idira pour la configuration OIDC.
OpenAI doit pouvoir accéder au document de découverte et au jeu de clés JWKS via HTTPS sans authentification. L’URL HTTPS figurant dans le champ iss du jeton indique l’émetteur. L’identifiant SPIFFE figurant dans sub identifie votre charge de travail.
Choisissez une audience réservée à l’accès à OpenAI, par exemple https://api.openai.com/v1. Utilisez la même valeur lorsque vous demandez le JWT-SVID et lorsque vous configurez le fournisseur dans OpenAI. L’audience indique le destinataire prévu du jeton. Elle ne définit pas le point de terminaison utilisé pour l’échange de jetons.
Par défaut, SWA signe les jetons avec RS512 et une clé RSA de 4 096 bits. Le vérificateur de JWT d’OpenAI accepte cette combinaison. Pour connaître les autres options de signature de SWA, consultez les exigences d’Idira relatives aux JWT.
Installez spiffe-helper dans l’environnement de la charge de travail pour demander un JWT-SVID à l’agent SWA et le renouveler. Enregistrez la configuration suivante dans helper.conf. L’utilitaire écrit le jeton dans /var/run/spiffe/openai.jwt, le chemin utilisé dans les exemples de code du SDK communs aux intégrations SPIFFE :
12345678agent_address = "/tmp/swa-agent/public/api.sock"
cert_dir = "/var/run/spiffe"
daemon_mode = true
jwt_svids = [{
jwt_audience = "https://api.openai.com/v1"
jwt_svid_file_name = "openai.jwt"
}]
Dans Kubernetes, exécutez l’utilitaire dans un conteneur sidecar du pod de la charge de travail. Partagez un volume emptyDir en mémoire monté sur /var/run/spiffe et montez le répertoire parent du socket SWA, /tmp/swa-agent/public, dans le conteneur de l’utilitaire. Sur une machine virtuelle, utilisez /run/swa-agent/api.sock comme valeur de agent_address et conservez le répertoire du jeton sur un système de fichiers en mémoire. Suivez les instructions de déploiement d’Idira pour l’accès au socket et l’attestation.
Avant de démarrer l’utilitaire, créez le répertoire du jeton et accordez à l’utilitaire le droit d’y écrire. Démarrez l’utilitaire avec cette configuration dans le conteneur sidecar ou dans le service de la machine virtuelle :
spiffe-helper -config helper.conf
Réservez l’accès au fichier du jeton et au socket de l’agent aux processus qui en ont besoin. Laissez l’utilitaire s’exécuter pour qu’il remplace le jeton avant son expiration.
Si votre langage dispose d’un client pour la Workload API de SPIFFE, vous pouvez aussi récupérer un nouveau JWT-SVID directement dans le fournisseur de jetons de sujet. Consultez l’option d’accès direct à la Workload API dans le guide d’intégration d’Idira.
Depuis la charge de travail, chargez le jeton dans l’environnement du décodeur local :
export TOKEN="$(cat /var/run/spiffe/openai.jwt)"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25const 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]));
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 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]))
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
78
79
80
81
82
83
84package 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)
}
}
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
78
79
80// 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]));
}
}
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
62using 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]));
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
34require "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]))
Vérifiez les champs suivants avant de créer le fournisseur dans OpenAI :
| Champ |
Valeur attendue |
Champ d’en-tête alg |
L’algorithme de signature configuré, par exemple RS512. |
Champ d’en-tête kid |
L’identifiant d’une clé de signature figurant dans le JWKS public du domaine de confiance. |
iss |
L’URL HTTPS de l’émetteur du domaine de confiance. |
sub |
L’identifiant SPIFFE attribué à cette charge de travail. |
aud |
Contient l’audience choisie pour l’accès à OpenAI. |
iat et exp |
Les horodatages d’émission et d’expiration du jeton. Utilisez un jeton qui n’a pas encore expiré. |
Le décodeur lit les champs du jeton sans en vérifier la signature. N’enregistrez pas le jeton dans les journaux et ne le collez pas dans un décodeur en ligne. Supprimez la variable d’environnement TOKEN après l’inspection.
Ouvrez Paramètres de l’organisation > Sécurité > Fournisseur d’identités de charges de travail et créez un fournisseur avec les paramètres suivants :
| Paramètre |
Valeur |
| Nom |
Un nom unique, par exemple idira-prod. |
| Description |
L’application et l’environnement qui utilisent ce fournisseur. |
| URL de l’émetteur OIDC |
La valeur du champ iss du JWT-SVID. |
| Audience |
L’audience demandée par l’utilitaire, par exemple https://api.openai.com/v1. |
| Utiliser un JWKS importé pour vérifier les jetons |
Laissez cette option désactivée pour utiliser la découverte OIDC. |
| Transformations d’attributs |
Laissez ce champ vide si vous associez le compte de service directement à partir de sub. |
Lors de l’échange de jetons, l’API OpenAI ignore les barres obliques finales lorsqu’elle compare les URL des émetteurs. Par exemple, https://issuer.example.com et https://issuer.example.com/ sont équivalentes. Tous les autres caractères doivent correspondre, y compris dans le schéma et le chemin de l’URL. Les valeurs d’audience doivent correspondre exactement. Copiez l’URL de l’émetteur telle qu’elle figure dans le jeton pour que votre configuration reste cohérente.
Si OpenAI ne peut pas accéder au document de découverte ou au JWKS, suivez les instructions communes de configuration des fournisseurs pour importer un JWKS public. Mettez à jour les clés importées lorsque SWA renouvelle ses clés de signature.
- Dans le fournisseur, créez une association de compte de service et donnez-lui un nom, par exemple
idira-production-workload.
- Dans Clé, saisissez
sub. Dans Valeur, saisissez l’identifiant SPIFFE exact figurant dans le jeton de la charge de travail, par exemple spiffe://prod.example.com/k8s-nodegroup/ns/my-namespace/sa/my-service-account.
- Sélectionnez le Projet et le Compte de service que la charge de travail peut utiliser.
- Sélectionnez les Autorisations nécessaires à la charge de travail, par exemple
api.model.request pour les requêtes adressées aux modèles. Ces autorisations peuvent restreindre les droits d’accès du compte de service, mais elles ne peuvent pas les étendre. Si vous laissez ce champ vide, la fédération d’identités de charges de travail n’ajoute aucune restriction.
Utilisez un compte de service distinct pour chaque application et chaque environnement. Privilégiez un identifiant SPIFFE exact. N’ajoutez un caractère générique à la fin d’un préfixe que si toutes les identités commençant par ce préfixe doivent bénéficier des mêmes droits d’accès. Évitez les associations qui se chevauchent. Pour en savoir plus, consultez la section Associations entre identifiants SPIFFE et comptes de service.
Dans l’environnement de la charge de travail, définissez les identifiants de votre fournisseur OpenAI et du compte de service sélectionné :
export OPENAI_IDENTITY_PROVIDER_ID="<workload identity provider ID>"
export OPENAI_SERVICE_ACCOUNT_ID="<service account ID>"
Les exemples suivants, communs aux intégrations SPIFFE, lisent le jeton dans /var/run/spiffe/openai.jwt. Le fournisseur de jetons de sujet renvoie le jeton en cours chaque fois que le SDK doit effectuer un échange pour obtenir ou renouveler un jeton d’accès OpenAI. SWA et l’utilitaire renouvellent le JWT-SVID externe. Le SDK OpenAI gère le jeton d’accès OpenAI.
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/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);
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/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)
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/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())
}
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
75import 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()));
}
}
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/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)
Exécutez l’exemple depuis la charge de travail attestée, en veillant à ce que la variable OPENAI_API_KEY ne soit pas définie. Si la requête aboutit, l’exemple affiche la réponse du modèle. Vérifiez que les requêtes continuent d’aboutir après le remplacement du JWT-SVID par l’utilitaire et le renouvellement du jeton d’accès OpenAI par le SDK.
| Symptôme |
Vérifications à effectuer |
| L’utilitaire ne reçoit aucune identité |
Vérifiez que la charge de travail répond aux critères de la politique d’attestation SWA et qu’elle peut accéder au socket de l’agent. Effectuez ces vérifications dans le contexte de la charge de travail. |
| Le fichier du jeton est absent ou vide, ou le jeton qu’il contient a expiré |
Vérifiez que l’utilitaire est en cours d’exécution, que le volume partagé et le chemin du fichier correspondent à la configuration et que la charge de travail peut lire le fichier. Maintenez les horloges des hôtes synchronisées. |
| La vérification du jeton échoue |
Vérifiez iss, aud, kid et la date d’expiration du jeton. Vérifiez aussi que le document de découverte et le JWKS sont accessibles. Appliquez la règle de comparaison des URL des émetteurs décrite plus haut. |
| Aucune association de compte de service ne correspond |
Vérifiez l’identifiant du fournisseur, l’identifiant du compte de service, l’activation de l’association et la valeur exacte de sub. |
| Plusieurs associations correspondent |
Rendez les critères des associations qui se chevauchent plus précis ou désactivez certaines de ces associations pour qu’une seule association active corresponde. |
| L’échange aboutit, mais l’API rejette la requête |
Vérifiez le projet du compte de service associé, ses autorisations et les politiques d’accès à l’API qui s’appliquent. |
Pour diagnostiquer les problèmes de déploiement de SWA, consultez les instructions de dépannage de l’intégration d’Idira.