SPB Git

Repository Public

kubernetes.md 24.9 KB 718 lines markdown

1# Configuring workload identity federation for Kubernetes23> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.45Use Kubernetes as a Workload Identity Provider by exchanging a projected Kubernetes service account token for a short-lived OpenAI access token.67## Setting up Kubernetes89This guide assumes Kubernetes service account token projection is enabled, which is available by default in modern Kubernetes releases. OpenAI workload identity federation requires OIDC-compatible projected service account tokens. Legacy Kubernetes service account tokens stored in Secrets are not supported.1011Use a Kubernetes `ServiceAccount` for the workload that needs to call the OpenAI API. If you do not already have one, create it:1213```bash14kubectl create serviceaccount openai-wif --namespace default15```1617Get the OIDC issuer for your Kubernetes cluster:1819```bash20kubectl get --raw /.well-known/openid-configuration | jq -r .issuer21```2223Even if you upload the JWKS and OpenAI does not perform JWKS discovery against the OIDC issuer, this issuer must match the issuer configured in the Workload Identity Provider.2425Get the cluster JWKS and save the returned key set. You will need it when configuring the Workload Identity Provider:2627```bash28kubectl get --raw /openid/v1/jwks29```3031Configure the projected service account token with the audience OpenAI expects and an expiration suitable for your workload. OpenAI validates the token's issuer, signature, audience, and expiration. In this example, the token file is mounted at `/var/run/secrets/tokens/token`, uses the audience `https://api.openai.com/v1`, and expires after 3600 seconds. You may use a different audience if the projected token audience and OpenAI Workload Identity Provider audience match:3233```yaml34apiVersion: v135kind: Pod36metadata:37  name: openai-wif-app38  namespace: default39spec:40  serviceAccountName: openai-wif41  containers:42    - name: app43      image: my-image44      volumeMounts:45        - name: ksa-token46          mountPath: /var/run/secrets/tokens47          readOnly: true48  volumes:49    - name: ksa-token50      projected:51        sources:52          - serviceAccountToken:53              path: token54              audience: "https://api.openai.com/v1"55              expirationSeconds: 360056```5758## Verify the token5960Before configuring workload identity federation, decode a sample projected service account token locally and inspect its claims. From a running pod with the projected token mounted, retrieve the token and export it as `TOKEN`:6162```bash63TOKEN=$(kubectl exec -n default openai-wif-app -- cat /var/run/secrets/tokens/token)64export TOKEN65```6667Then run this script:6869```javascript70const parts = process.env.TOKEN?.split(".") ?? [];71if (parts.length !== 3) {72  throw new Error("Expected a compact JWT with three segments");73}74if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {75  throw new Error("JWT payload is not valid Base64URL");76}7778const bytes = Buffer.from(parts[1], "base64url");79if (bytes.toString("base64url") !== parts[1]) {80  throw new Error("JWT payload is not valid Base64URL");81}82const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);83const claims = JSON.parse(decoded);84if (claims === null || Array.isArray(claims) || typeof claims !== "object") {85  throw new Error("JWT payload is not a JSON object");86}87console.log(decoded);88```8990```python91import base6492import json93import os94import re959697def reject_non_json_constant(value):98    raise ValueError(f"JWT payload contains non-JSON constant: {value}")99100101parts = os.environ.get("TOKEN", "").split(".")102if len(parts) != 3:103    raise ValueError("Expected a compact JWT with three segments")104105payload = parts[1]106if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:107    raise ValueError("JWT payload is not valid Base64URL")108padded_payload = payload + "=" * (-len(payload) % 4)109decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)110if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:111    raise ValueError("JWT payload is not valid Base64URL")112decoded_text = decoded.decode("utf-8")113claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)114if not isinstance(claims, dict):115    raise ValueError("JWT payload is not a JSON object")116print(decoded_text)117```118119```go120package main121122import (123	"bytes"124	"encoding/base64"125	"encoding/json"126	"fmt"127	"os"128	"strings"129	"unicode/utf8"130)131132func decodeSegment(segment string) (json.RawMessage, error) {133	if !isBase64URLSegment(segment) {134		return nil, fmt.Errorf("JWT segment is not valid Base64URL")135	}136	decoded, err := base64.RawURLEncoding.DecodeString(segment)137	if err != nil {138		return nil, err139	}140	if base64.RawURLEncoding.EncodeToString(decoded) != segment {141		return nil, fmt.Errorf("JWT segment is not valid Base64URL")142	}143	if !utf8.Valid(decoded) {144		return nil, fmt.Errorf("JWT segment is not valid UTF-8")145	}146147	var value json.RawMessage148	if err := json.Unmarshal(decoded, &value); err != nil {149		return nil, err150	}151	if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {152		return nil, fmt.Errorf("JWT segment is not a JSON object")153	}154	return value, nil155}156157func isBase64URLSegment(segment string) bool {158	if segment == "" || len(segment)%4 == 1 {159		return false160	}161	for _, character := range segment {162		if !('A' <= character && character <= 'Z') &&163			!('a' <= character && character <= 'z') &&164			!('0' <= character && character <= '9') &&165			character != '-' &&166			character != '_' {167			return false168		}169	}170	return true171}172173func main() {174	parts := strings.Split(os.Getenv("TOKEN"), ".")175	if len(parts) != 3 {176		panic("Expected a compact JWT with three segments")177	}178179	payload, err := decodeSegment(parts[1])180	if err != nil {181		panic(err)182	}183	formatted, err := json.MarshalIndent(payload, "", "  ")184	if err != nil {185		panic(err)186	}187	fmt.Println(string(formatted))188}189```190191```java192// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.193import com.fasterxml.jackson.databind.DeserializationFeature;194import com.fasterxml.jackson.databind.JsonNode;195import com.fasterxml.jackson.databind.ObjectMapper;196import java.io.IOException;197import java.nio.ByteBuffer;198import java.nio.charset.CharacterCodingException;199import java.nio.charset.CodingErrorAction;200import java.nio.charset.StandardCharsets;201import java.util.Base64;202203public final class DecodeJwtPayloadExample {204  private static final ObjectMapper JSON =205      new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);206207  private DecodeJwtPayloadExample() {}208209  static String decodeUtf8(byte[] bytes) throws IOException {210    try {211      return StandardCharsets.UTF_8212          .newDecoder()213          .onMalformedInput(CodingErrorAction.REPORT)214          .onUnmappableCharacter(CodingErrorAction.REPORT)215          .decode(ByteBuffer.wrap(bytes))216          .toString();217    } catch (CharacterCodingException exception) {218      throw new IOException("JWT segment is not valid UTF-8", exception);219    }220  }221222  static String decodeSegment(String segment) throws IOException {223    if (!isBase64UrlSegment(segment)) {224      throw new IllegalArgumentException("JWT segment is not valid Base64URL");225    }226    byte[] bytes = Base64.getUrlDecoder().decode(segment);227    if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {228      throw new IllegalArgumentException("JWT segment is not valid Base64URL");229    }230    String decoded = decodeUtf8(bytes);231    JsonNode value = JSON.readTree(decoded);232    if (value == null || value.isMissingNode() || !value.isObject()) {233      throw new IOException("JWT segment is not a JSON object");234    }235    return decoded;236  }237238  static boolean isBase64UrlSegment(String segment) {239    if (segment.isEmpty() || segment.length() % 4 == 1) {240      return false;241    }242    return segment243        .chars()244        .allMatch(245            character ->246                character >= 'A' && character <= 'Z'247                    || character >= 'a' && character <= 'z'248                    || character >= '0' && character <= '9'249                    || character == '-'250                    || character == '_');251  }252253  static String[] requireCompactJwt(String token) {254    if (token == null) {255      throw new IllegalArgumentException("Expected a compact JWT with three segments");256    }257    String[] parts = token.split("\\.", -1);258    if (parts.length != 3) {259      throw new IllegalArgumentException("Expected a compact JWT with three segments");260    }261    return parts;262  }263264  public static void main(String[] args) throws IOException {265    String[] parts = requireCompactJwt(System.getenv("TOKEN"));266    System.out.println(decodeSegment(parts[1]));267  }268}269```270271```csharp272using System.Text;273using System.Text.Json;274275static string DecodeSegment(string segment)276{277    if (278        segment.Length % 4 == 1 ||279        segment.Any(280            character =>281                !(282                    character is >= 'A' and <= 'Z' ||283                    character is >= 'a' and <= 'z' ||284                    character is >= '0' and <= '9' ||285                    character is '-' or '_'286                )287        )288    )289    {290        throw new FormatException("JWT segment is not valid Base64URL");291    }292293    byte[] decoded = Convert.FromBase64String(294        segment.Replace('-', '+').Replace('_', '/') +295        new string('=', (4 - segment.Length % 4) % 4)296    );297    string canonicalSegment = Convert298        .ToBase64String(decoded)299        .TrimEnd('=')300        .Replace('+', '-')301        .Replace('/', '_');302    if (canonicalSegment != segment)303    {304        throw new FormatException("JWT segment is not valid Base64URL");305    }306    string decodedJson = new UTF8Encoding(false, true).GetString(decoded);307    using JsonDocument document = JsonDocument.Parse(decodedJson);308    if (document.RootElement.ValueKind is not JsonValueKind.Object)309    {310        throw new FormatException("JWT segment is not a JSON object");311    }312    return decodedJson;313}314315string? token = Environment.GetEnvironmentVariable("TOKEN");316if (token is null)317{318    throw new InvalidOperationException(319        "Expected a compact JWT with three segments"320    );321}322string[] parts = token.Split('.');323if (parts.Length != 3)324{325    throw new InvalidOperationException(326        "Expected a compact JWT with three segments"327    );328}329330Console.WriteLine(DecodeSegment(parts[1]));331```332333```ruby334require "base64"335require "json"336337parts = ENV.fetch("TOKEN", "").split(".", -1)338raise "Expected a compact JWT with three segments" unless parts.length == 3339340unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1341  raise "JWT payload is not valid Base64URL"342end343344begin345  payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))346rescue ArgumentError347  raise "JWT payload is not valid Base64URL"348end349unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]350  raise "JWT payload is not valid Base64URL"351end352353payload.force_encoding(Encoding::UTF_8)354raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?355356claims = JSON.parse(payload)357raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)358359puts(payload)360```361362363This command decodes the JWT payload without verifying the token signature. Use a local decoder for production tokens, and avoid pasting production tokens into third-party tools.364365A decoded Kubernetes projected service account token will look similar to:366367```json368{369  "iss": "https://kubernetes.example.com",370  "aud": ["https://api.openai.com/v1"],371  "sub": "system:serviceaccount:default:openai-wif",372  "iat": 1716235422,373  "exp": 1716239022,374  "kubernetes.io": {375    "namespace": "default",376    "serviceaccount": {377      "name": "openai-wif",378      "uid": "11111111-2222-3333-4444-555555555555"379    }380  }381}382```383384Use the decoded payload to compare the token you received with the issuer, audience, and mapping values configured in OpenAI. Most configuration issues are visible in the `iss`, `aud`, and `sub` claims before you exchange the token.385386## Setting up workload identity federation387388Create a Workload Identity Provider in OpenAI for the Kubernetes issuer, then add a service account mapping that matches attributes from the projected token.389390Configure the Workload Identity Provider first, then create the service account mapping.391392### Set up the Workload Identity Provider3933941. **Create the Workload Identity Provider.** Set **Name** to a unique value, such as `kubernetes-prod`. Use **Description**, such as `Production Kubernetes cluster`, to help admins identify the cluster.3953962. **Set the issuer and audience.** Set **OIDC Issuer URL** to the issuer returned by `kubectl get --raw /.well-known/openid-configuration | jq -r .issuer`. This value must match the `iss` claim in the projected token. Set **Audience** to the same opaque audience string configured on the projected service account token volume. In this example, that value is `https://api.openai.com/v1`.3973983. **Upload the Kubernetes JWKS.** Enable **Use uploaded JWKS for token verification**, then set **JWKS JSON** to the output from `kubectl get --raw /openid/v1/jwks`. OpenAI uses this public key set to verify projected Kubernetes service account tokens. Upload the full key set including the surrounding `keys`.399400   > **Note:** For self-hosted Kubernetes clusters, OpenAI supports only local JWKS mode. Upload the JWKS returned by your cluster; OpenAI does not perform OIDC discovery against the configured issuer. OpenAI still compares the configured issuer with the `iss` field in the token.401402   If your cluster rotates service account signing keys, update the uploaded JWKS in the Workload Identity Provider configuration. Tokens signed by keys that are not present in the configured JWKS are rejected. If the JWKS contains multiple active public keys, include the full `keys` array.4034044. **Add attribute transformations only if you need derived mapping attributes.** Raw token claims such as `sub`, `aud`, and `iss` can be used directly in mapping assertions. If you plan to match on transformed attributes rather than raw token claims, the dashboard applies the `openai.` prefix automatically; for example, enter `workload_subject` with expression `assertion.sub` to create `openai.workload_subject`. Raw token claims that already start with `openai.` are ignored for `openai.` mapping keys unless a matching transformation is configured.405406### Set up the service account mapping4074081. **Create a service account mapping.** Set **Name** to a unique value within the Workload Identity Provider, such as `openai-mapping-kubernetes`. Use **Description**, such as `Workload Identity Provider Mapping for Kubernetes Workloads`, to explain which workload can use the mapping.4094102. **Match the Kubernetes service account subject.** Set **Key** to `sub` and **Value** to `system:serviceaccount:default:openai-wif`. For Kubernetes service accounts, the subject format is `system:serviceaccount:<namespace>:<service-account-name>`.4114123. **Choose the OpenAI target.** Set **Project** to the OpenAI project that owns the target service account. Set **Service account** to the OpenAI service account the Kubernetes workload can use, such as `kubernetes-prod-openai-wif`. Check `Create a new service account in this project` if you wish to create a new service account for this mapping rather than reuse an existing one.4134144. **Narrow API permissions if needed.** Select appropriate **Permissions** such as `api.model.request` and `api.vector_store.read` to further narrow access tokens minted from this mapping. Leave permissions blank to avoid adding a WIF-specific scope restriction; the token still authorizes as the mapped service account.415416## Using the token in code417418Configure your OpenAI SDK client to read the projected Kubernetes token and exchange it for an OpenAI-issued access token.419420Use the mounted token path, such as `/var/run/secrets/tokens/token`, as the subject token source for the SDK workload identity federation provider. The SDK exchanges that Kubernetes token for an OpenAI-issued access token and uses the OpenAI token to authenticate API requests.421422The following examples initialize an OpenAI client with a custom subject token provider. The provider reads the projected Kubernetes service account token from the mounted file path and uses it as the subject token for workload identity federation.423424Authenticate from a Kubernetes projected service account token425426```javascript427import { readFile } from "node:fs/promises";428import OpenAI from "openai";429430const tokenPath = "/var/run/secrets/tokens/token";431const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;432const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;433434if (!identityProviderId || !serviceAccountId) {435  throw new Error(436    "Set OPENAI_IDENTITY_PROVIDER_ID and OPENAI_SERVICE_ACCOUNT_ID"437  );438}439440function mountedServiceAccountTokenProvider(path) {441  return {442    tokenType: "jwt",443    getToken: async () => {444      const token = (await readFile(path, "utf8")).trim();445      if (!token) {446        throw new Error("The mounted service account token file is empty.");447      }448      return token;449    },450  };451}452453const client = new OpenAI({454  workloadIdentity: {455    identityProviderId,456    serviceAccountId,457    provider: mountedServiceAccountTokenProvider(tokenPath),458  },459});460461const response = await client.responses.create({462  model: "gpt-5.6-terra",463  input: "Say hello from Kubernetes workload identity federation.",464});465466console.log(response.output_text);467```468469```python470import os471from pathlib import Path472473from openai import OpenAI474from openai.auth import SubjectTokenProvider475476TOKEN_PATH = "/var/run/secrets/tokens/token"477478479def mounted_service_account_token_provider(token_path: str) -> SubjectTokenProvider:480    def get_token() -> str:481        token = Path(token_path).read_text().strip()482        if not token:483            raise RuntimeError("The mounted service account token file is empty.")484        return token485486    return {"token_type": "jwt", "get_token": get_token}487488489client = OpenAI(490    workload_identity={491        "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],492        "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],493        "provider": mounted_service_account_token_provider(TOKEN_PATH),494    },495)496497response = client.responses.create(498    model="gpt-5.6-terra",499    input="Say hello from Kubernetes workload identity federation.",500)501502print(response.output_text)503```504505```go506package main507508import (509	"context"510	"fmt"511	"log"512	"os"513	"strings"514515	"github.com/openai/openai-go/v3"516	"github.com/openai/openai-go/v3/auth"517	"github.com/openai/openai-go/v3/option"518	"github.com/openai/openai-go/v3/responses"519)520521const tokenPath = "/var/run/secrets/tokens/token"522523type mountedServiceAccountTokenProvider struct {524	path string525}526527func (p mountedServiceAccountTokenProvider) TokenType() auth.SubjectTokenType {528	return auth.SubjectTokenTypeJWT529}530531func (p mountedServiceAccountTokenProvider) GetToken(ctx context.Context, _ auth.HTTPDoer) (string, error) {532	data, err := os.ReadFile(p.path)533	if err != nil {534		return "", &auth.SubjectTokenProviderError{535			Provider: "kubernetes",536			Message:  "failed to read mounted service account token",537			Cause:    err,538		}539	}540541	token := strings.TrimSpace(string(data))542	if token == "" {543		return "", &auth.SubjectTokenProviderError{544			Provider: "kubernetes",545			Message:  "mounted service account token is empty",546		}547	}548549	return token, nil550}551552func main() {553	client := openai.NewClient(554		option.WithWorkloadIdentity(auth.WorkloadIdentity{555			IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),556			ServiceAccountID:   os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),557			Provider: mountedServiceAccountTokenProvider{558				path: tokenPath,559			},560		}),561	)562563	response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{564		Model: openai.ChatModelGPT4_1Mini,565		Input: responses.ResponseNewParamsInputUnion{566			OfString: openai.String("Say hello from Kubernetes workload identity federation."),567		},568	})569	if err != nil {570		log.Fatal(err)571	}572573	fmt.Println(response.OutputText())574}575```576577```java578import com.fasterxml.jackson.databind.json.JsonMapper;579import com.openai.auth.SubjectTokenProvider;580import com.openai.auth.SubjectTokenType;581import com.openai.auth.WorkloadIdentity;582import com.openai.client.OpenAIClient;583import com.openai.client.okhttp.OpenAIOkHttpClient;584import com.openai.core.http.HttpClient;585import com.openai.errors.SubjectTokenProviderException;586import com.openai.models.responses.ResponseCreateParams;587import java.nio.file.Files;588import java.nio.file.Path;589import java.util.concurrent.CompletableFuture;590591public final class KubernetesWorkloadIdentityExample {592  private static final String TOKEN_PATH = "/var/run/secrets/tokens/token";593594  private KubernetesWorkloadIdentityExample() {}595596  static final class MountedServiceAccountTokenProvider implements SubjectTokenProvider {597    private final Path tokenPath;598599    MountedServiceAccountTokenProvider(String tokenPath) {600      this.tokenPath = Path.of(tokenPath);601    }602603    @Override604    public SubjectTokenType tokenType() {605      return SubjectTokenType.JWT;606    }607608    @Override609    public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {610      String token;611      try {612        token = Files.readString(tokenPath).trim();613      } catch (Exception e) {614        throw new SubjectTokenProviderException(615            "kubernetes", "failed to read mounted service account token", e);616      }617618      if (token.isEmpty()) {619        throw new SubjectTokenProviderException(620            "kubernetes", "mounted service account token is empty", null);621      }622623      return token;624    }625626    @Override627    public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {628      return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));629    }630  }631632  public static void main(String[] args) {633    WorkloadIdentity workloadIdentity =634        WorkloadIdentity.builder()635            .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))636            .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))637            .provider(new MountedServiceAccountTokenProvider(TOKEN_PATH))638            .build();639640    OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();641642    ResponseCreateParams params =643        ResponseCreateParams.builder()644            .model("gpt-5.6-terra")645            .input("Say hello from Kubernetes workload identity federation.")646            .build();647648    client.responses().create(params).output().stream()649        .flatMap(item -> item.message().stream())650        .flatMap(message -> message.content().stream())651        .flatMap(content -> content.outputText().stream())652        .forEach(outputText -> System.out.println(outputText.text()));653  }654}655```656657```ruby658require "openai"659660TOKEN_PATH = "/var/run/secrets/tokens/token"661662class MountedServiceAccountTokenProvider663  include OpenAI::Auth::SubjectTokenProvider664665  def initialize(token_path:)666    @token_path = token_path667  end668669  def token_type670    OpenAI::Auth::TokenType::JWT671  end672673  def get_token674    token = File.read(@token_path).strip675    if token.empty?676      raise OpenAI::Errors::SubjectTokenProviderError.new(677        message: "Mounted service account token is empty",678        provider: "kubernetes"679      )680    end681    token682  rescue SystemCallError => e683    raise OpenAI::Errors::SubjectTokenProviderError.new(684      message: "Failed to read mounted service account token: #{e.message}",685      provider: "kubernetes",686      cause: e687    )688  end689end690691provider = MountedServiceAccountTokenProvider.new(token_path: TOKEN_PATH)692693workload_identity = OpenAI::Auth::WorkloadIdentity.new(694  identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),695  service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),696  provider: provider697)698699client = OpenAI::Client.new(workload_identity: workload_identity)700701response = client.responses.create(702  model: "gpt-5.6-terra",703  input: "Say hello from Kubernetes workload identity federation."704)705706puts(response.output_text)707```708709710## Kubernetes best practices711712- Use a stable OIDC issuer. The issuer URL must match the projected service account token `iss` claim and should remain stable across cluster upgrades and maintenance operations.713- Protect signing keys carefully. Anyone with access to the cluster's service account signing keys can mint tokens that may be accepted by OpenAI.714- Use dedicated service accounts for OpenAI integrations. Avoid reusing service accounts that are also used for unrelated infrastructure or application access.715- Keep the uploaded JWKS current. OpenAI uses the configured JWKS to validate workload identity tokens in local JWKS mode, so update the Workload Identity Provider before rotating to new signing keys.716- Minimize custom claim complexity. Prefer matching on standard claims such as `sub` and `aud`, or transformed attributes derived directly from those claims.717- Treat namespace ownership as part of your security model. If namespace administrators can create service accounts, ensure mappings are scoped appropriately to prevent unintended privilege escalation.718- Monitor issuer and signing key changes. Rotating signing keys without updating the Workload Identity Provider JWKS can cause token exchange failures.
Files main
Menu

Theme