30. Juli 2026 · 8 Min. Lesezeit
Der EKS-401, der nichts mit Credentials zu tun hatte
Das erste Prod-Rollout unserer neuen Deploy-Pipeline lief in jedem Schritt grün durch, bis auf den letzten. Identity-Check, Chart-Pull, update-kubeconfig; alles erfolgreich. Dann schlug helm upgrade mit einer einzigen Zeile fehl:
Error: Kubernetes cluster unreachable: the server has asked for the client to provide credentials
Die erste Reaktion aller Beteiligten ist: „AWS Access Key rotieren.” Dieser Instinkt ist falsch. Der Fehler kommt nicht von AWS. Das Token wurde ausgestellt, signiert und an den kube-apiserver präsentiert; der apiserver hat es abgewiesen. Der Cluster hatte keinerlei RBAC für den Principal, der da angefragt hat.
So sieht der Fehler tatsächlich aus, end-to-end, in einer gesunden Pipeline, in der nichts rotiert wurde, nichts abgelaufen ist und auf der Credential-Ebene alles in Ordnung war.
Warum der Instinkt falsch ist
Das Wort „Credentials” in dieser Fehlermeldung ist irreführend. Die Meldung kommt aus client-go, der Kubernetes-Clientbibliothek, nicht von AWS STS. AWS-Credential-Fehler tauchen sehr viel früher auf und haben eine andere Meldung, typischerweise so etwas wie exec: ... could not get token, und sie schlagen beim aws eks get-token-Schritt fehl, nicht beim helm upgrade.
In unserem Trace hat alles vor dem helm-Schritt funktioniert:
- Die IRSA-Rolle des Runners hat die Cross-Account-Deploy-Rolle per
AssumeRoleWithWebIdentityangenommen. - Die Deploy-Rolle hat das Chart aus dem Operations-ECR gezogen.
update-kubeconfighat ein gültiges kubeconfig geschrieben, das auf den Prod-Cluster zeigt.aws eks get-tokenhat ein Token zurückgegeben.
Das Token war erzeugbar. Das Token war präsentabel. Das Token wurde dann vom Cluster abgewiesen. Das ist ein 401, und ein 401 ist ein Autorisierungsproblem, kein Authentifizierungsproblem im AWS-Sinne.
Das Drei-Schichten-Modell
„Credentials” können in einer EKS-Deploy-Pipeline auf drei verschiedenen Schichten fehlschlagen, und nur eine davon erzeugt die obige Wortwahl. Eine schnelle Möglichkeit, sie zu trennen:
| Schicht | Was sie tut | Fehlermeldung | Erster Check |
|---|---|---|---|
| AWS STS | Stellt die Session für Runner- oder Deploy-Rolle aus | could not get token, ExpiredToken, AccessDenied | aws sts get-caller-identity --profile <profile> |
| EKS-Zugriffssteuerung | Mappt den IAM-Principal auf eine Kubernetes-Identität | server has asked for the client to provide credentials (bei gültigem Token) | aws eks list-access-entries --cluster-name <c> |
| Kubernetes RBAC | Erlaubt der gemappten Identität die konkrete Aktion | Forbidden, cannot ... | kubectl auth can-i --list |
Ein 401 vom apiserver sitzt in der Mitte. Ein 403 sitzt ganz unten. Connection refused oder ein Timeout ist ein Netzwerk- bzw. kubeconfig-Problem und steht gar nicht in dieser Tabelle. Sobald man weiß, auf welcher Schicht man ist, ist der Rest der Diagnose mechanisch.
Zwei Bestätigungen in dreißig Sekunden
Bevor man irgendeinen Fix angeht, sollte man die AWS-Seite als gesund verifizieren, damit man nicht am Ende einen Key rotiert, der nie das Problem war:
# Seite A: die AWS-Identität, die deine Deploy-Rolle annimmt
aws sts get-caller-identity --profile <spoke-account>
# Seite B: der Cluster selbst kann für diese Identität ein Token ausstellen
aws eks get-token --cluster-name <cluster> --region <region> --profile <spoke-account>
Wenn beide erfolgreich sind, hast du es nicht mit einem Credential-Problem zu tun. Du hast es mit einem Access-Entry-Problem zu tun. Das Token in Seite B ist exakt das Token, das der kube-apiserver sehen wird; wenn Seite B ein Token zurückgibt, sieht der kube-apiserver auch eines. Das Token ist nicht das Problem.
Dann prüft man, welche Principals der Cluster tatsächlich kennt:
aws eks list-access-entries \
--cluster-name <cluster> \
--region <region> \
--profile <admin-profile>
Wenn der principalArn der Deploy-Rolle nicht in dieser Liste steht, hat der Cluster keinen Eintrag für sie. Jede Anfrage dieser Rolle wird auf der Access-Control-Schicht abgewiesen, egal wie gültig das Token ist.
Was EKS-Access-Entries tatsächlich sind
EKS-Access-Entries sind der API-seitige Ersatz für die aws-auth ConfigMap. Sie wurden eingeführt, als AWS das aws-auth-Pattern abkündigte, bei dem IAM-Rollen zu Kubernetes-Usern gemappt wurden, indem man eine YAML-Datei in kube-system von Hand editierte. Diese Migration läuft seit Jahren; viele Cluster laufen weiterhin mit authenticationMode: CONFIG_MAP und vertrauen auf aws-auth, manche mit API_AND_CONFIG_MAP während des Übergangs, neuere rein mit API.
In jedem Modus, der die API einschließt, ist die Kette:
- Der IAM-Principal (deine Rolle) präsentiert ein presigniertes EKS-Token.
- Der apiserver validiert das Token gegen STS.
- Der apiserver schlägt den Principal in der Access-Entry-Tabelle nach.
- Wird der Principal gefunden, werden die zugeordneten Access-Policies auf Kubernetes-Gruppen oder -User gemappt.
- Diese Gruppen und User unterliegen dann dem normalen Kubernetes-RBAC.
Ein 401 in Schritt 3 heißt, dass der Lookup leer zurückgekommen ist. Der Principal ist AWS-seitig gültig und das Token ist STS-seitig gültig; der Cluster hat schlicht keine Zeile dafür. Das Token wird präsentiert, der Lookup schlägt fehl, der apiserver fragt nach Credentials (weil er keine zum Mappen hat), und client-go meldet das dem Nutzer als „server has asked for the client to provide credentials”.
Genau dieselbe Wortwahl taucht auf, wenn eine menschliche Rolle einen frisch neu erstellten Cluster benutzen will. Die Creator-Identität des Clusters bekommt implizit Admin-Zugriff und funktioniert; alle anderen brauchen einen Eintrag.
Woher die Lücke in unserem Fall kam
Unser EKS-Modul nimmt eine access_entries-Map entgegen. Jeder Eintrag ist { principal_arn = "..." }, und das Modul erzeugt das aws_eks_access_entry plus eine standardmäßige AmazonEKSClusterAdminPolicy-Association auf Cluster-Scope. Einen Key zu dieser Map hinzuzufügen ist die einzige Code-seitige Änderung, die nötig ist, um einer Deploy-Rolle vollen Zugriff auf einen Cluster zu geben.
Der Eintrag für die CI-Deploy-Rolle wurde während des Prod-EKS-Rollouts zur Map hinzugefügt. Der benachbarte dashboard-admin-Eintrag im selben Block war bereits auf dem Cluster, was bewies, dass der Block mindestens einmal angewendet worden war. Der Deploy-Eintrag wurde nach diesem letzten Apply hinzugefügt. „Edit ohne Apply” ist einer der leichteren Fehler, die man mit Terragrunt machen kann, weil die Plan-Ausgabe für einen einzelnen neuen Eintrag klein und sauber ist und die Kosten, das Apply „nur dieses eine Mal” zu überspringen, unsichtbar bleiben, bis das erste Deploy sie braucht.
Sandbox und pre haben funktioniert, weil ihre Einträge lange vor dieser Lücke angewendet worden waren. Prod war das erste Deploy durch die neue Pipeline, also trat die Lücke dort zum ersten Mal auf. Die früheren Envs waren nicht betroffen.
Der Fix
terragrunt apply --all --target <env>/eks
Das Apply ist nur dann streng additiv, wenn kein anderer deklarierter Principal bereits per Click-Ops existiert. In unserem Fall waren die benachbarten Dashboard-Admin- und Viewer-Rollen-Einträge bereits live auf dem Cluster, sodass das Apply mit ResourceInUseException fehlgeschlagen wäre, wenn wir es kalt ausgeführt hätten. Der erste Schritt ist, den Live-Zustand des Clusters zu verifizieren:
aws eks list-access-entries \
--cluster-name <cluster> \
--region <region> \
--profile <admin-profile>
Wenn die Einträge, die das Modul verwalten soll, bereits auf dem Cluster sind, importiert man sie vor dem Apply:
terragrunt import 'aws_eks_access_entry.this["dashboard-admin"]' \
'<cluster>:<dashboard-admin-role-arn>'
Sobald der Import durch ist, ist ein apply auf demselben Modul ein sauberer Create für den neuen Eintrag und ein No-Op für die bestehenden. Der End-to-End-Beweis ist der nächste Deploy-Lauf. Wenn das Apply erfolgreich war und das Deploy trotzdem fehlschlägt, ist der Eintrag nicht tatsächlich auf eine brauchbare Policy-Association gemappt, und list-associated-access-policies ist der nächste Halt.
Ein Diagramm des Pfads
Die gesamte Pipeline, mit der Fehlerstelle hervorgehoben:
┌───────────────┐ ┌────────────────────┐ ┌───────────────┐
│ Runner-IRSA │ ──▶ │ Cross-Account- │ ──▶ │ aws eks │
│ -Rolle │ │ Deploy-Rolle │ │ get-token │
└───────────────┘ └────────────────────┘ └───────────────┘
│
▼
┌───────────────┐ ┌────────────────────┐ ┌───────────────┐
│ Kubernetes │ ◀── │ Access-Entry- │ ◀── │ kube- │
│ RBAC │ │ Lookup ❌ kein │ │ apiserver │
│ │ │ Eintrag │ │ │
└───────────────┘ └────────────────────┘ └───────────────┘
Die IRSA-Rolle des Runners nimmt die Cross-Account-Rolle an, die Cross-Account-Rolle stellt ein Token aus, der apiserver empfängt das Token, der apiserver schlägt den Principal in der Access-Entry-Tabelle nach, und der Lookup liefert nichts. Alles vor dem Lookup ist in Ordnung. Alles danach ist unerreichbar.
Das größere Muster
„Credentials” ist ein Wort, das in einer EKS-Deploy-Pipeline drei verschiedene Schichten abdeckt: AWS-Credentials (hast du einen gültigen Principal?), EKS-Zugriffssteuerung (kennt der Cluster diesen Principal?) und Kubernetes-RBAC (ist dieser Principal berechtigt, das zu tun, was er anfragt?). Nur die zweite erzeugt die obige Meldung. AWS-Keys zu rotieren hilft bei der zweiten nicht. Das kubeconfig zu aktualisieren hilft bei der zweiten nicht. Das einzige, was bei der zweiten hilft, ist ein aws_eks_access_entry für den anfragenden Principal.
Wenn die Meldung „Credentials” sagt, prüfe zuerst, welche Credentials sie meint, bevor du eine davon rotierst. Die Fehlermeldung ist der schlechteste Teil der Diagnose; sie überlebt quer durch alle drei Schichten und zeigt in zwei von drei Fällen auf die falsche. Die Kette aws sts get-caller-identity → aws eks get-token → aws eks list-access-entries löst die Mehrdeutigkeit in unter einer Minute und sagt dir, welche Schicht du fixen musst.
Dasselbe Muster taucht in verwandten Fehlermodi auf. Eine menschliche Admin-Rolle, die auf dem ursprünglichen Cluster funktioniert und nach einem Destroy/Recreate nicht mehr, ist dieselbe Lücke mit einem anderen Auslöser. Eine CI-Rolle, die auf der vorherigen Env funktioniert hat und auf der neuen nicht mehr, ist dieselbe Lücke mit einer anderen Apply-Historie. Das Token ist in Ordnung. Der Cluster ist in Ordnung. Der Eintrag fehlt.