Zum Inhalt springen
Michael Blaess
IT-Consulting
Michael Blaess
EN

Self-hosted GitLab zu Azure deployen - ohne Secret, aber die Wildcard-Federation greift nicht

GitLab CIAzureOIDCDevOps

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:

MethodeVorteilNachteil
Basic Auth (Kudu-Publishing-Credentials)Einfach und überall verfügbar, ganz ohne Entra-SetupGilt 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 RessourcenDas 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 RBACAufwendigeres 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:

  1. GitLab stellt der Pipeline ein OIDC-id_token aus. Das Token trägt Claims wie iss (die GitLab-Instanz) und sub (welches Projekt/welcher Branch).
  2. Azure vertraut diesem Token, wenn eine Federated Identity Credential (FIC) auf einer Managed Identity oder App-Registrierung genau zu iss, sub und aud des Tokens passt.
  3. 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 Folgenden gitlab.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 Conflict antworten.
  • 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

“The best thing about a boolean is even if you are wrong, you are only off by a bit.”

- Unbekannt