Blog
Self-hosted GitLab zu Azure deployen - ohne Secret, aber die Wildcard-Federation greift nicht
Ein Deploy-Secret in den CI/CD-Variablen ist bequem und langlebig - und genau das ist das Problem. Es liegt Monate herum, wird selten gewechselt, und wer Lesezugriff auf die Pipeline-Variablen hat, hat faktisch das Deploy-Recht. Workload Identity Federation (OIDC) macht damit Schluss: Die Pipeline bekommt bei jedem Lauf ein kurzlebiges, signiertes Token, tauscht es gegen ein Azure-Zugriffstoken und deployt damit. Kein gespeichertes Secret, das gewechselt werden muss.
Drei Wege, einen Deploy zu authentifizieren
Bevor es an OIDC geht, der Überblick - so kommt ein GitLab-Deploy an eine Azure-App-Service-Instanz:
| Methode | Vorteil | Nachteil |
|---|---|---|
| Basic Auth (Kudu-Publishing-Credentials) | Einfach und überall verfügbar, ganz ohne Entra-Setup | Gilt als überholt und wird von Admins gemieden. Langlebiges Passwort in den CI-Variablen, oft per Policy komplett abgeschaltet. Wirklich nur ein Notnagel, wenn nichts anderes geht |
| Service Principal (mit Client-Secret) | Zentrale Identität mit feingranularem RBAC über viele Ressourcen | Das client_secret ist ein langlebiges Geheimnis - läuft ab, muss erneuert und sicher verwahrt werden |
| OIDC (Workload Identity Federation) | Kein Secret in CI, nichts läuft ab. Kurzlebige, an das Projekt gebundene Token, zentrales RBAC | Aufwendigeres Setup (FIC pro Repo, sub-Patch, Token-Exchange). Dazu das 20-FIC-Limit und unbequemere Fehlersuche |
Meine Wahl: OIDC als Standard. Service Principal mit Secret ist der Mittelweg, wenn OIDC (noch) nicht geht - Basic Auth wirklich nur als Notnagel. Der Rest des Artikels zeigt, warum OIDC bei self-hosted GitLab drei zusätzliche Handgriffe kostet.
Wie OIDC-Deploy im Prinzip funktioniert
Drei Bausteine spielen zusammen:
- GitLab stellt der Pipeline ein OIDC-
id_tokenaus. Das Token trägt Claims wieiss(die GitLab-Instanz) undsub(welches Projekt/welcher Branch). - Azure vertraut diesem Token, wenn eine Federated Identity Credential (FIC) auf einer Managed Identity oder App-Registrierung genau zu
iss,subundauddes Tokens passt. - Die Pipeline tauscht das GitLab-Token bei Entra ID gegen ein Azure-Zugriffstoken und nutzt dieses als Bearer für den eigentlichen Deploy (msdeploy, Kudu-REST, ARM).
Der bequeme Weg bei GitLab.com sind flexible FICs mit Wildcards im Subject - eine Regel deckt viele Branches oder Projekte ab. Damit fängt das Problem an.
Warum die Wildcard-Federation bei self-hosted nicht greift
Bei GitLab.com ist OIDC fast ein Selbstläufer. Bei einer selbst gehosteten Instanz - also einer, deren Host nicht gitlab.com heißt - bin ich bei meiner Umsetzung dagegen auf zwei Azure-Grenzen gestoßen:
- Die Issuer-Allowlist der flexiblen FIC verlangt einen
gitlab.*-Host. Der Issuer meiner Instanz war aber die eigene Domain (im Folgendengitlab.example.com) - der Wildcard-Weg fiel damit aus. - Flexible FICs ließen sich zudem nur auf App-Registrierungen anlegen, nicht auf Managed Identities. Wer wie ich secretlos über eine Managed Identity deployen will, ist auf exact-match FICs angewiesen: eine feste FIC pro Projekt, ohne Platzhalter.
Kurz: kein Wildcard, keine Sammel-Regel. Stattdessen pro Repo eine FIC - und einen Token-Exchange, den man selbst schreibt. (Flexible FICs sind ein Preview-Feature, ihre Grenzen können sich ändern - der Blick in die Doku unten lohnt sich für die eigene Tenant-Konfiguration.)
Die Lösung in drei Teilen
1. Den GitLab-sub branch-frei machen
Standardmäßig setzt GitLab den sub-Claim aus project_path, ref_type und ref zusammen - er ändert sich also mit jedem Branch. Eine exact-match FIC würde dann bei jedem neuen Branch brechen. Lösung: den sub projektweit auf nur project_path reduzieren (per GitLab Projects-API). Das Token wird damit branch-frei und überlebt jeden Branch-Wechsel.
# sub-Claim projektweit auf project_path reduzieren (branch-frei)
curl --request PUT \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--url "https://gitlab.example.com/api/v4/projects/1234" \
--data "ci_id_token_sub_claim_components[]=project_path"
Danach lautet der sub schlicht project_path:myorg/myproject.
2. Pro Projekt eine exact-match FIC anlegen
Auf der Managed Identity kommt für jedes Repo eine FIC mit exakt passendem Issuer, Subject und Audience:
az identity federated-credential create \
--name gitlab-myproject \
--identity-name my-deploy-mi \
--resource-group my-rg \
--issuer "https://gitlab.example.com" \
--subject "project_path:myorg/myproject" \
--audiences "api://AzureADTokenExchange"
Wichtig: Der Subject muss zeichengenau zum GitLab-sub passen (Groß-/Kleinschreibung inklusive). Ein Tippfehler fällt nicht beim Anlegen auf, sondern erst beim Token-Exchange - mit AADSTS70021 (no matching federated identity record).
3. Den Token-Exchange selbst bauen - der “curl-Patch”
Für GitLab-Self-hosted gibt es keinen fertigen Azure-Login-Baustein, der einem den Exchange abnimmt. Also holt sich die Pipeline das id_token und tauscht es von Hand:
deploy:
id_tokens:
AZURE_OIDC_TOKEN:
aud: api://AzureADTokenExchange
script:
- ./deploy.sh # macht den Exchange und deployt
# id_token gegen ein Azure-ARM-Token tauschen (der eigentliche "curl-Patch")
RESPONSE=$(curl --silent --request POST \
--url "https://login.microsoftonline.com/$TENANT/oauth2/v2.0/token" \
--data "grant_type=client_credentials" \
--data "client_id=$CLIENT_ID" \
--data "scope=https://management.azure.com/.default" \
--data "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
--data "client_assertion=$AZURE_OIDC_TOKEN")
ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)
# ACCESS_TOKEN danach als "Authorization: Bearer" fuer msdeploy / Kudu-REST / ARM
Der Kern ist die client_assertion: Statt eines Client-Secrets legt die Pipeline das GitLab-id_token als Nachweis vor. Entra ID prüft es gegen die FIC und gibt ein Azure-Token zurück. Ab da ist es ein normaler Bearer-Deploy - msdeploy mit authType=Bearer, Kudu-REST mit Authorization: Bearer, oder direkt gegen die ARM-API.
Ehrlichkeit bei den Grenzen
Der Weg funktioniert, hat aber Kanten, die man vorher kennen sollte:
- Eine FIC pro Repo, kein Wildcard. Das skaliert mit der Repo-Zahl. Eine Managed Identity hat ein Limit von 20 FICs - bei mehr Projekten muss man gruppieren oder auf mehrere Identitäten verteilen.
- FICs sequenziell anlegen. Werden mehrere parallel erstellt, kann Azure mit
409 Conflictantworten. - Falscher Subject bleibt lange unsichtbar. Das Anlegen schlägt nicht fehl - erst der erste echte Token-Exchange zeigt den Vertipper.
- Fallback behalten. Ich habe den alten Secret- bzw. Basic-Auth-Weg als wählbaren Fallback in der Pipeline gelassen, bis OIDC über alle Umgebungen bewiesen war. Ein Deploy-Weg, den man im Ernstfall nicht mehr umschalten kann, ist ein schlechter Tausch.
Fazit
Secretloses Deployen ist die richtige Richtung, und OIDC/Workload Identity Federation ist dafür das saubere Werkzeug. Bei GitLab.com bekommt man es geschenkt. Bei einer selbst gehosteten Instanz kostet es drei zusätzliche Handgriffe: den sub branch-frei patchen, pro Repo eine exact-match FIC anlegen und den Token-Exchange selbst schreiben. Danach liegt kein Deploy-Secret mehr in den CI-Variablen - und das ist die Mühe wert.
Quellen: GitLab: OIDC mit ID Tokens · GitLab: OpenID Connect in Azure konfigurieren · Microsoft: Workload Identity Federation · Microsoft: Considerations (20-FIC-Limit) · Microsoft: Flexible federated identity credentials