← Blog
AI13 min czytania

OpenAI Python SDK — uwierzytelnianie workload identity w Kubernetesie

Zamiast trzymać stały klucz API w sekrecie Kubernetesa, SDK OpenAI potrafi wymieniać krótkotrwałe tokeny tożsamości platformy. Opisujemy pięć wariantów konfiguracji, ograniczenia trybu mTLS i opcję rezydencji danych w UE.

Wdrożenie aplikacji korzystającej z modeli językowych na Kubernetesie prawie zawsze zaczyna się tak samo: klucz API trafia do sekretu, sekret montuje się jako zmienna środowiskowa i sprawa wydaje się zamknięta. Problem zaczyna się później, przy pytaniach, które zada audytor albo klient.

Kto ma dostęp do tego sekretu? Ile ten klucz żyje? Kiedy był rotowany ostatni raz? Co się stanie, gdy wycieknie w dzienniku, w zrzucie pamięci albo w repozytorium, do którego ktoś wrzucił plik konfiguracyjny? Jak odróżnić w rachunku wywołania z produkcji od wywołań z laptopa developera, który używa tego samego klucza?

Stały klucz API jest hasłem bez terminu ważności i bez tożsamości. Odpowiedzią na to w nowoczesnej infrastrukturze jest workload identity — mechanizm, w którym aplikacja nie ma własnego sekretu, a udowadnia swoją tożsamość tokenem wystawionym przez platformę, na której działa. SDK OpenAI dla Pythona obsługuje to od strony klienta i o tym jest ten wpis.

Stan projektu

Repozytorium: openai/openai-python. 31 608 gwiazdek, licencja Apache-2.0, start w październiku 2020. Aktualna wersja na PyPI to 3.11.0 z 9 września 2026, przy 422 wydaniach w historii — czyli tempo jest wysokie. Commity wpadają codziennie, w tym w dniu powstania tego wpisu. Wymagany Python to 3.10 lub nowszy. Otwartych zgłoszeń jest 465.

Jedna rzecz, o której warto wiedzieć, bo wyjaśnia i tempo wydań, i charakter kodu: znaczna część tego SDK jest generowana ze specyfikacji OpenAPI. Widać to w nagłówkach plików źródłowych, opatrzonych adnotacją o generowaniu, a wydania publikuje bot. Praktyczna konsekwencja: zgłaszanie poprawek do wygenerowanych plików nie ma sensu, bo zostaną nadpisane — zmiany trzeba proponować w warstwie, która nie jest generowana, albo w samej specyfikacji.

Jak to działa

Zamiast klucza podaje się klientowi słownik workload_identity z trzema polami: identyfikatorem dostawcy tożsamości, identyfikatorem konta usługi po stronie OpenAI oraz dostawcą tokenu, czyli mechanizmem, który dostarczy krótkotrwały token z twojej platformy. SDK wymienia ten token na poświadczenie do API, buforuje wynik i odświeża go automatycznie.

Kształt konfiguracji jest wspólny dla wszystkich wariantów, a różni je wyłącznie dostawca. To dobra decyzja projektowa — przejście z Kubernetesa na Azure to zmiana jednej linii, nie przepisywanie warstwy uwierzytelniania.

Kubernetes

Wariant, od którego bierze się tytuł tego wpisu, jest zaskakująco prosty. Kubernetes potrafi wstrzyknąć do poda token konta usługi jako plik, a SDK wystarczy wskazać jego ścieżkę:

from openai import OpenAI
from openai.auth import k8s_service_account_token_provider

client = OpenAI(
    workload_identity={
        "identity_provider_id": "idp-123",
        "service_account_id": "sa-456",
        "provider": k8s_service_account_token_provider(
            "/var/run/secrets/kubernetes.io/serviceaccount/token"
        ),
    },
)

Ta ścieżka to standardowe miejsce, w którym Kubernetes udostępnia token konta usługi przypisanego do poda. Token jest krótkotrwały i odnawiany przez samą platformę — nie trzeba nim zarządzać, rotować go ani nigdzie zapisywać. Nie ma sekretu do wycieknięcia, bo nie ma sekretu.

Zysk operacyjny jest przy tym większy, niż widać na pierwszy rzut oka. Tożsamość aplikacji przestaje być wspólnym hasłem, a staje się kontem usługi konkretnego wdrożenia — więc uprawnienia można nadawać per usługa, a w rachunku i dziennikach widać, która z nich wykonała wywołanie. To jest dokładnie ta różnica, o którą pyta audytor.

Azure i Google Cloud

Dla dwóch największych platform chmurowych SDK dowozi gotowe dostawce, korzystające z ich własnych mechanizmów tożsamości.

Na Azure to tożsamość zarządzana, gdzie podaje się zasób, dla którego token ma być wystawiony:

from openai.auth import azure_managed_identity_token_provider

client = OpenAI(
    workload_identity={
        "identity_provider_id": "idp-123",
        "service_account_id": "sa-456",
        "provider": azure_managed_identity_token_provider(
            resource="https://management.azure.com/",
        ),
    },
)

Na Google Cloud token pobierany jest z serwera metadanych instancji, a parametrem jest odbiorca tokenu:

from openai.auth import gcp_id_token_provider

client = OpenAI(
    workload_identity={
        "identity_provider_id": "idp-123",
        "service_account_id": "sa-456",
        "provider": gcp_id_token_provider(audience="https://api.openai.com/v1"),
    },
)

Własny dostawca tokenu

Jeśli twoja tożsamość pochodzi skądś indziej — z własnego dostawcy OIDC, z Vaulta, z firmowego systemu wystawiającego tokeny — nie trzeba czekać na wsparcie. Dostawcę podaje się jako zwykły słownik z typem tokenu i funkcją, która go zwraca:

def get_custom_token() -> str:
    return "twoj-token-jwt"

client = OpenAI(
    workload_identity={
        "identity_provider_id": "idp-123",
        "service_account_id": "sa-456",
        "provider": {
            "token_type": "jwt",
            "get_token": get_custom_token,
        },
    }
)

To dobrze zaprojektowany punkt rozszerzenia: gotowe dostawce dla trzech platform to wygoda, ale mechanizm nie jest do nich przywiązany.

Odświeżanie tokenu

Wartość, którą trzeba znać, bo wpływa na zachowanie w produkcji. Bufor odświeżania wynosi domyślnie 1200 sekund, czyli dwadzieścia minut przed wygaśnięciem tokenu. SDK odnawia go więc z dużym wyprzedzeniem, co przy tokenach o godzinnej ważności jest rozsądne. Wartość można zmienić:

client = OpenAI(
    workload_identity={
        "identity_provider_id": "idp-123",
        "service_account_id": "sa-456",
        "provider": k8s_service_account_token_provider("/var/token"),
        "refresh_buffer_seconds": 120.0,
    }
)

Kiedy warto ją ruszać? Skrócenie buforu ma sens, jeśli twój dostawca wystawia tokeny o krótkiej ważności — przy tokenie żyjącym dziesięć minut domyślny bufor dwudziestu minut oznaczałby próbę odświeżania przy każdym wywołaniu. W drugą stronę: wydłużenie buforu przy niestabilnym dostawcy daje więcej okazji na ponowienie, zanim token faktycznie wygaśnie.

X.509 i wzajemny TLS

Najmocniejszy wariant, a jednocześnie ten z największą liczbą warunków. Przy federacji tożsamości opartej o certyfikaty X.509 konfigurujesz certyfikat klienta i zaufanie do serwera na kliencie HTTP, a SDK dostaje wyłącznie identyfikatory:

import os, ssl
from openai import OpenAI, DefaultHttpx2Client
from openai.auth import x509_workload_identity

tls_context = ssl.create_default_context(
    cafile=os.getenv("OPENAI_MTLS_CA_BUNDLE"),
)
tls_context.load_cert_chain(
    certfile=os.environ["OPENAI_MTLS_CERTIFICATE_CHAIN"],
    keyfile=os.environ["OPENAI_MTLS_PRIVATE_KEY"],
    password=os.getenv("OPENAI_MTLS_PRIVATE_KEY_PASSWORD"),
)

client = OpenAI(
    workload_identity=x509_workload_identity(
        identity_provider_id=os.environ["OPENAI_IDENTITY_PROVIDER_ID"],
        service_account_id=os.environ["OPENAI_SERVICE_ACCOUNT_ID"],
    ),
    http_client=DefaultHttpx2Client(
        verify=tls_context,
        follow_redirects=False,
    ),
)

Kilka rzeczy w tym fragmencie warto zauważyć. Tryb X.509 domyślnie celuje w inny adres — https://mtls.api.openai.com/v1 — o ile nie ustawisz base_url ani odpowiedniej zmiennej środowiskowej. Ten sam skonfigurowany klient HTTP przedstawia swój certyfikat zarówno stałemu punktowi wymiany tokenów, jak i samemu API. Tokeny wymieniane są leniwie, buforowane i odświeżane automatycznie. I zwróć uwagę na follow_redirects=False w przykładzie — przy uwierzytelnianiu certyfikatem podążanie za przekierowaniami jest dokładnie tym, czego nie chcesz.

Ograniczenia trybu X.509, które trzeba znać przed wdrożeniem

Dokumentacja wylicza je zwięźle i bez upiększania, co należy docenić — bo każde z tych zdań to potencjalny incydent, gdyby trzeba było odkrywać je samemu.

  • Żądania wymagają HTTPS i muszą pozostać na skonfigurowanym pochodzeniu API. Efektywna wartość nagłówka Host musi się z tym pochodzeniem zgadzać — czyli sztuczki z przepisywaniem hosta w warstwie proxy zerwą uwierzytelnianie.
  • Nagłówków z kluczem API dostawcy ani nagłówków przeznaczonych wyłącznie dla proxy nie można wysyłać razem z uwierzytelnianiem X.509. Nie da się więc mieszać dwóch metod „na wszelki wypadek".
  • Wymiany tokenów nie dziedziczą zaczepów żądań API, uwierzytelniania ani ciasteczek. To dobra własność izolacyjna, ale znaczy też, że twój globalny mechanizm dopisywania nagłówków nie zadziała dla wymiany tokenu.
  • Ustawienia tożsamości są przechwytywane w momencie konstruowania klienta. Zmiana tożsamości wymaga utworzenia nowego klienta — nie ma sensu mutować obiektu w miejscu.
  • Klienci Azure nie obsługują X.509 workload identity. To wykluczenie podane wprost.
  • Tryb X.509 obsługuje na razie API po HTTP; Realtime i WebSockets nie są objęte. Jeśli budujesz aplikację głosową na strumieniowym połączeniu, ten wariant uwierzytelniania jej nie obsłuży.

Dokumentacja mówi też jasno, co zostaje po twojej stronie, i jest to lista, którą warto przepisać do własnej dokumentacji wdrożenia: pliki certyfikatów, klucze prywatne, hasła do nich, zaufanie do serwera, proxy oraz rotacja pozostają sprawą aplikacji i warstwy transportowej. SDK nie zarządza cyklem życia twoich certyfikatów. Kto wdroży X.509 bez zaplanowanej rotacji, ten wymieni jeden problem operacyjny na inny, tylko z gorszym momentem wykrycia.

Co siedzi pod spodem

Zajrzenie do pakietu openai.auth potwierdza, że to nie jest doklejona funkcja, i wyjaśnia kilka zachowań, których nie widać z zewnątrz.

Po pierwsze i najważniejsze: mechanizm opiera się na standardzie, nie na własnym wynalazku. W kodzie widnieje typ przyznania urn:ietf:params:oauth:grant-type:token-exchange, czyli wymiana tokenów opisana w RFC 8693, a typy tokenu podmiotu to standardowe identyfikatory urn:ietf:params:oauth:token-type:jwt i urn:ietf:params:oauth:token-type:id_token. Wymiana idzie na https://auth.openai.com/oauth/token. Dla kogoś, kto już ma federację tożsamości w firmie, to dobra wiadomość — pojęcia są te same, które zna z innych integracji.

Po drugie, buforowanie tokenu jest zrobione starannie. Czasy wygaśnięcia i odświeżenia trzymane są na zegarze monotonicznym, a nie na czasie ściennym. To detal, który ratuje przed klasą błędów bardzo trudnych do zdiagnozowania: korekta czasu przez NTP albo wybudzenie maszyny wirtualnej z uśpienia nie sprawi, że token zostanie uznany za ważny lub nieważny wbrew faktom.

Po trzecie, odświeżanie jest serializowane blokadą wątkową. Bez tego nagły przypływ równoległych żądań po wygaśnięciu tokenu wywołałby stado jednoczesnych wymian — klasyczny problem, przez który usługa uwierzytelniająca dostaje falę zapytań dokładnie w najmniej wygodnym momencie. Tutaj wymiana odbywa się raz, a pozostałe wątki czekają na wynik.

Po czwarte, dostawce dla Azure i Google Cloud odpytują serwer metadanych z dziesięciosekundowym limitem czasu, konfigurowalnym parametrem. Wysyłają przy tym nagłówki wymagane przez te platformy — Metadata: true na Azure i Metadata-Flavor: Google na GCP — które są tam po to, by żądanie do serwera metadanych nie mogło zostać sfabrykowane przez przypadkowe przekierowanie.

Po piąte, błędy mają własny typ SubjectTokenProviderError, a komunikaty są konkretne: osobny dla pustego pliku z tokenem, osobny dla nieudanego odczytu tego pliku, osobne dla nieudanego i dla pustego odczytu z serwera metadanych — przy czym te ostatnie przenoszą ze sobą odpowiedź HTTP. Praktycznie: warto ten wyjątek przechwytywać osobno i logować, bo odróżnia problem z tożsamością od zwykłego błędu API.

I po szóste, cała konfiguracja jest opisana jako TypedDict — eksportowane są typy WorkloadIdentity, SubjectTokenProvider, X509WorkloadIdentity i pozostałe. Literówkę w nazwie klucza wyłapie więc sprawdzanie typów, a nie produkcja.

Wdrożenie stopniowe

Autorzy przewidzieli rzecz, o której przy takich migracjach zwykle się zapomina. W repozytorium leżą dwa pełne przykłady — synchroniczny i asynchroniczny — nazwane wprost jako przykłady przełączania wdrożenia. Wybierają one uwierzytelnianie kluczem API albo X.509 na podstawie zmiennej środowiskowej należącej do aplikacji.

To właściwy wzorzec: nie przełączasz całej flotylli naraz, a wypuszczasz nową metodę na część podów, obserwujesz i cofasz zmianą zmiennej środowiskowej, jeśli coś nie działa. Warto zauważyć, że ta zmienna jest własnością aplikacji, nie SDK — czyli mechanizm przełączania piszesz sam, a przykłady pokazują jak.

Dla wariantu asynchronicznego używa się AsyncOpenAI wraz z asynchroniczną odmianą klienta HTTP.

Rezydencja danych, czyli druga rzecz warta znalezienia

Przy okazji przeglądania kodu natrafiłem na opcję, która dla polskich firm bywa ważniejsza niż sam sposób uwierzytelniania. SDK ma parametr data_residency przyjmujący cztery wartości — global, us, eu i ae — i przekładający je na osobne adresy bazowe. Dla Unii Europejskiej jest to https://eu.api.openai.com/v1.

Praktycznie znaczy to, że kierowanie żądań do punktu końcowego w Unii jest kwestią jednego argumentu przy tworzeniu klienta, a nie ręcznego składania adresu. Nie zastępuje to oczywiście analizy prawnej ani umowy z dostawcą — o tym, co faktycznie dzieje się z danymi, decydują warunki usługi, a nie nazwa adresu — ale usuwa całą klasę pomyłek konfiguracyjnych. Przy przetwarzaniu danych klientów w modelu językowym to jest ta opcja, którą chcesz ustawić świadomie i mieć zapisaną w kodzie.

Implementacja jest przy tym staranna: data_residency wyklucza się wzajemnie z base_url, z adresem bazowym dla WebSocketów oraz z parametrem dostawcy, a próba podania obu naraz kończy się jawnym błędem z czytelnym komunikatem. Nieprawidłowa wartość również — z wypisaniem listy dopuszczalnych. To dokładnie takie zachowanie, jakiego się chce od opcji, której pomylenie miałoby konsekwencje.

Pułapki w jednym miejscu

  • Znaczna część SDK jest generowana ze specyfikacji. Poprawki w wygenerowanych plikach zostaną nadpisane.
  • Bufor odświeżania to domyślnie 20 minut. Przy tokenach o krótszej ważności trzeba go skrócić, inaczej odświeżanie będzie próbowane bez przerwy.
  • Tryb X.509 domyślnie celuje w mtls.api.openai.com, a nie w zwykły adres API.
  • Nagłówek Host musi zgadzać się z pochodzeniem API. Przepisywanie hosta w proxy zerwie uwierzytelnianie X.509.
  • Nie można łączyć X.509 z nagłówkiem klucza API ani z nagłówkami wyłącznie dla proxy.
  • Wymiana tokenu nie dziedziczy zaczepów, uwierzytelniania ani ciasteczek z żądań API.
  • Tożsamość jest ustalana przy konstruowaniu klienta. Zmiana wymaga nowego obiektu.
  • Klienci Azure nie obsługują X.509, a sam tryb X.509 nie obejmuje Realtime i WebSocketów.
  • Rotacja certyfikatów jest po twojej stronie. SDK nie zarządza ich cyklem życia.
  • data_residency wyklucza się z base_url — podanie obu kończy się błędem.
  • Przechwytuj SubjectTokenProviderError osobno — odróżnia problem z tożsamością od błędu samego API.
  • 465 otwartych zgłoszeń. Przy tej skali nie licz na szybką odpowiedź na swoje.

Podsumowanie

Uwierzytelnianie workload identity to jedna z tych zmian, które nie dodają żadnej funkcji użytkownikowi, a bardzo dużo zmieniają w rozmowie z audytorem i w rachunku ryzyka. Zamiast hasła bez terminu ważności, wspólnego dla produkcji i laptopów, dostajesz tożsamość konkretnego wdrożenia i tokeny, którymi nie musisz zarządzać.

  • Brak sekretu do wycieknięcia — w Kubernetesie wystarcza ścieżka do tokenu, który platforma odnawia sama.
  • Jeden kształt konfiguracji dla wszystkich platform — przejście między Kubernetesem, Azure i Google Cloud to zmiana jednej linii.
  • Punkt rozszerzenia na własnego dostawcę — słownik z funkcją zwracającą token, bez czekania na wsparcie producenta.
  • Wariant z wzajemnym TLS dla środowisk, w których certyfikat jest podstawą tożsamości maszyny.
  • Gotowe przykłady stopniowego wdrożenia z przełącznikiem po stronie aplikacji.
  • Rezydencja danych jednym argumentem, z punktem końcowym w Unii Europejskiej i jawnym wykluczaniem sprzecznych ustawień.

SDK jest wydane na licencji Apache-2.0 — permisywnej, z jawnym udzieleniem licencji patentowej i wymogiem odnotowania zmian w modyfikowanych plikach. Przy zwykłym użyciu jako zależności nie ma to żadnych konsekwencji dla twojego kodu. Warto natomiast pamiętać o rozgraniczeniu, które przy tego rodzaju bibliotekach bywa mylące: licencja dotyczy biblioteki klienckiej, nie usługi. Kod, który wywołuje API, jest otwarty i możesz z nim zrobić co chcesz; to, co dzieje się po drugiej stronie połączenia, reguluje umowa z dostawcą, a nie Apache-2.0. Przy przetwarzaniu danych klientów to jest właśnie ten dokument, który trzeba przeczytać — obok tego wpisu.