Neu veröffentlicht: E-Commerce mit Power Pages, Stripe & Analytics

· David Göschel · DevOps  · 5 minuten Lesezeit

Zitadel hinter Traefik richtig verdrahten mit Issuer, JWKS und Login V2

Der 401 Fehler beim Ingest kam nicht vom JWT selbst, sondern von einem kleinen Architekturbruch. Issuer, JWKS und Host Header mussten dieselbe öffentliche Sicht auf Zitadel haben.

Der 401 Fehler beim Ingest kam nicht vom JWT selbst, sondern von einem kleinen Architekturbruch. Issuer, JWKS und Host Header mussten dieselbe öffentliche Sicht auf Zitadel haben.

Inhalt

Zitadel hinter Traefik mit Issuer, JWKS und Login V2 richtig konfigurieren

Der Backend Request sah zuerst aus wie ein klassischer Token Fehler.

401 Invalid or expired token

So eine Meldung ist tückisch, weil sie fast alles bedeuten kann. Abgelaufenes Token. Falscher Client. Falsche Scopes. Falsche Signatur. In meinem Fall lag die Ursache aber tiefer.

Der Token selbst war nicht das Problem. Das Problem war die Sicht auf Zitadel.

Was iss und JWKS wirklich bedeuten

Ein JWT trägt einen Issuer Claim. Bei Zitadel sieht der in meinem Setup so aus:

http://auth.localhost

Das ist wichtig. Der Backend Code prüft nicht nur die Signatur. Er prüft auch, ob der Issuer exakt zur konfigurierten Zitadel Domain passt.

const { payload } = await jwtVerify(token, JWKS, {
  issuer: ZITADEL_DOMAIN,
});

Zusätzlich braucht das Backend die öffentlichen Schlüssel von Zitadel über das JWKS Endpoint.

http://auth.localhost/oauth/v2/keys

Beides muss zusammenpassen. Der Issuer und die JWKS Quelle müssen dieselbe öffentliche URL meinen.

Warum http://zitadel:8080 falsch war

Ich hatte den Backend Wert zunächst auf die interne Docker Adresse gesetzt.

ZITADEL_DOMAIN=http://zitadel:8080

Das sieht auf den ersten Blick logisch aus, weil Container intern natürlich über den Servicenamen sprechen können. Für OIDC ist das aber die falsche Sicht.

Zitadel schreibt Tokens immer mit der öffentlichen Domain. Wenn der Backend Code dann auf die interne URL prüft, passt der Issuer nicht mehr. Gleichzeitig liefert Zitadel für zitadel:8080 keine brauchbaren JWKS Antworten, weil der Host Header nicht zu seiner Instanz passt.

Das Ergebnis ist derselbe Fehler auf zwei Ebenen.

  1. JWKS Fetch schlägt fehl.
  2. Der Issuer Vergleich schlägt fehl.

Die saubere Lösung

Ich habe den Backend Wert auf die öffentliche Domain gesetzt.

ZITADEL_DOMAIN=http://auth.localhost

Damit ist die Logik wieder konsistent. Der Token kommt von auth.localhost. Die Signaturprüfung fragt auth.localhost/oauth/v2/keys ab. Der Issuer Vergleich passt.

Damit der Backend Container diese Domain trotzdem intern auflösen kann, bekommt Traefik einen Docker Netzwerk Alias.

traefik:
  networks:
    traefik_net:
      aliases:
        - auth.localhost

Das ist der wichtige Teil. Der Backend Container spricht auth.localhost an. Docker löst den Namen auf Traefik auf. Traefik leitet die Anfrage weiter. Für den Backend Code bleibt es aber trotzdem die öffentliche Zitadel Domain.

Login V2 und die Base URI

Der zweite Punkt war die Login V2 Route. Zitadel baut die Login URL aus der Base URI und dem festen Pfad von Login V2 zusammen.

Wenn die Base URI auf http://localhost:9000 zeigt, landet man bei einer Route, die Login V2 gar nicht kennt.

Die richtige Basis ist:

ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI=http://auth.localhost/ui/v2/login

Dann ergibt sich die korrekte Login Route:

http://auth.localhost/ui/v2/login/login?authRequest=...

Das ist die Route, die Traefik an den Login Container weiterleitet.

Web oder Native App

Die Extension App ist im Portal aktuell als Web App angelegt. Das funktioniert.

Für eine Chrome Extension ist Native typischer. In meinem Fall ist das aber kein funktionaler Blocker, solange der Redirect über chromiumapp.org läuft und die Token Settings sauber gesetzt sind.

Ich würde es deshalb so einordnen.

  1. Funktional ist die aktuelle Konfiguration in Ordnung.
  2. Sauberer wäre später ein Native App Typ.
  3. Wichtig ist zuerst der korrekte Redirect und der JWT Token Typ.

Was ich aus Issuer, JWKS und Login V2 gelernt habe

OIDC ist nicht nur Authentifizierung. Es ist auch eine Frage der Perspektive.

Der Browser sieht die öffentliche Domain. Der Backend Container braucht intern eine andere Netzsicht. Beide Sichtweisen müssen trotzdem auf denselben Issuer zeigen. Genau das war der Fehler.

Sobald man diese Trennung sauber denkt, wird der Rest einfacher.

Zitadel Login V2 in Docker Compose mit Traefik und Host Header Routing

Das Bild zeigt den Authentifizierungsweg mit Login V2, Zitadel und Traefik. Entscheidend ist nicht nur der Container Name, sondern die öffentliche Domain, die am Ende im Token steht.

Alle Artikel der Serie

  1. Vision und Systemübersicht: Chrome Extension, RAG-Architektur, Projekthintergrund: Artikel lesen
  2. RAG-System Aufbau: Qdrant, Embeddings, Cosine-Ähnlichkeit in TypeScript: Artikel lesen
  3. AI Provider Abstraktion: Ollama vs. OpenAI, Interface-Design, kein Vendor-Lock-in: Artikel lesen
  4. Chrome Extension MV3: Drei isolierte Laufzeitkontexte, Message Passing, Strategy Pattern: Artikel lesen
  5. Docker Compose Strategie: Override-Pattern, von lokal zu Azure: Artikel lesen
  6. Ollama lokal vs. Docker: Die Entscheidung und ihre Konsequenzen: Artikel lesen
  7. Ollama Auto-Pull Entrypoint: Automatisiertes Modell-Setup beim Container-Start: Artikel lesen
  8. tsconfig und Vite: Node16 vs. bundler, warum Vite eigene Regeln hat: Artikel lesen
  9. Instagram Caption mit MutationObserver vollständig laden: Artikel lesen
  10. Chrome Extension Foundation mit Health-Dot und Retry-Queue: Artikel lesen
  11. Phase 2 Features: Shadow DOM Overlay, Tailwind v4, Duplicate Detection: Artikel lesen
  12. Race Condition bei der Plattformerkennung: Wie ein UI-Event die Instagram-Erkennung bricht: Artikel lesen
  13. PostId-Extraktion in zwei Instagram-Layouts: querySelector vs. Ancestor-Traversal: Artikel lesen
  14. Instagram Karussell vollständig erfassen mit MutationObserver: Lazy-Loading, Observer-before-click, Timeout-Fallback: Artikel lesen
  15. Notiz und Tags beim Screenshot-Speichern: Artikel lesen
  16. Instagram Tastatur-Shortcuts blockieren Chrome Extension Eingaben: Artikel lesen
  17. Lowercase-Normalisierung und Duplikat-Erkennung im Tag-Input: Artikel lesen
  18. Zitadel Login V2 in Docker Compose: drei versteckte Fehler: Artikel lesen
  19. PKCE OAuth in einer Chrome MV3 Extension: Artikel lesen
  20. React Frontend mit react-oidc-context und Zitadel: Artikel lesen
  21. Vite Build-Time-Umgebungsvariablen in Docker: Artikel lesen
  22. Event-Driven Ingestion mit BullMQ und Redis: Artikel lesen
  23. MinIO statt Azurite: S3-kompatible Objektspeicherung lokal und auf Hetzner: Artikel lesen
  24. access_token, id_token und der Userinfo-Endpoint: was wohin gehört: Artikel lesen
  25. Qdrant Multi-Tenancy: Pro Nutzer eine eigene Collection: Artikel lesen
  26. Wenn Backend und Frontend unterschiedliche Typen kennen: Artikel lesen
  27. Zitadel Bootstrap entfernt: Host-Header-Bug und manuelles Setup: Artikel lesen
  28. Backend Code Review: sechs Probleme vor dem Launch behoben: Artikel lesen
  29. Traefik statt NGINX: Reverse Proxy für einen wachsenden Docker-Compose-Stack: Artikel lesen
  30. Zweischichtiges Rate Limiting: Traefik und express-rate-limit mit Redis: Artikel lesen
  31. DSGVO Art. 17 korrekt implementieren: Promise.allSettled und Export-Batching: Artikel lesen
  32. Embedding-Modell-Lock-in: Warum mxbai-embed-large eine Produktionsentscheidung für immer ist: Artikel lesen
  33. Docker Volumes in Produktion: Named Volumes, Bind Mounts und der Hetzner-Volume-Trick: Artikel lesen
  34. Zwei Sicherheitslücken vor dem Launch: Redis ohne Auth und ein offener Qdrant-Admin-Port: Artikel lesen
  35. Traefik als einziger Einstiegspunkt im Docker Compose Stack: Artikel lesen
  36. Zitadel hinter Traefik richtig verdrahten mit Issuer, JWKS und Login V2: (dieser Artikel)
  37. Frontend reparieren wenn der nginx Healthcheck an localhost scheitert: Artikel lesen
  38. Observability für meinen Docker Compose Stack mit Bull Board und Dozzle: Artikel lesen
  39. Qdrant Dashboard sicher öffnen mit lokalem Traefik und SSH Tunnel: Artikel lesen
  40. Diagnose: Warum mein Chunking trotz Tokenisierung noch scheiterte: Artikel lesen
  41. Entscheidung: Warum ich den Chunk auf 1500 Tokens gesetzt habe: Artikel lesen
  42. Implementierung: Wie ich den Embedding Workflow in mehrere saubere Schritte zerlegt habe: Artikel lesen
  43. Validierung: Wie ich Chunking, Speicherung und Suche wieder zusammenbringe: Artikel lesen

Du baust gerade einen ähnlichen Multi-Service-Stack und fragst dich, wie du Routing und TLS sauber löst? Lass uns das gemeinsam einschätzen.

Zurück zum Blog

Ähnliche Beiträge

Alle Beiträge ansehen
Traefik statt NGINX für einen wachsenden Docker-Compose-Stack

Traefik statt NGINX für einen wachsenden Docker-Compose-Stack

Ab acht Services im Docker-Compose-Stack wird nginx.conf zur Wartungslast. Traefik liest Service-Konfiguration direkt aus Docker-Labels, terminiert TLS automatisch über ACME und braucht keine separate Konfigurationsdatei. Warum ich gewechselt habe und wie die Konfiguration aussieht.