Les pods Kubernetes comme identités cloud : EKS IRSA vs. GKE Workload Identity Federation
Comment un ServiceAccount Kubernetes devient une véritable identité cloud sur AWS et Google Cloud - la chaîne de confiance de fédération OIDC derrière EKS IRSA et GKE Workload Identity Federation, mise en correspondance un-à-un, avec les particularités qui bousculent vos habitudes.
AWS et Google Cloud résolvent tous deux le même problème de la même manière : un pod s'authentifie auprès des API cloud en tant que lui-même, avec un jeton de courte durée et sans secret stocké, en transformant son ServiceAccount Kubernetes en une identité cloud fédérée. Sur AWS, la fonctionnalité est IRSA (IAM Roles for Service Accounts) ; sur Google Cloud, il s'agit de Workload Identity Federation for GKE (renommée en 2024 de "Workload Identity"). En coulisses, les deux sont le même mécanisme de fédération OIDC - mais GKE masque une grande partie de la plomberie, ce qui modifie la façon dont vous le configurez, le déboguez et le comprenez.
En bref, si vous ne lisez qu'un seul paragraphe : le cluster signe un jeton de courte durée pour le ServiceAccount de chaque pod, et le cloud échange ce jeton contre de véritables identifiants associés à une seule identité. Sur AWS, vous enregistrez un fournisseur IAM OIDC par cluster, créez un rôle IAM et le protégez avec une politique de confiance ; le SDK à l'intérieur du pod appelle AssumeRoleWithWebIdentity. Sur GKE, vous n'enregistrez rien - chaque projet dispose d'un pool d'identités de charge de travail permanent, géré par Google, nommé PROJECT_ID.svc.id.goog, chaque ServiceAccount est automatiquement un principal dans celui-ci, et un serveur de métadonnées local au nœud effectue l'échange de jetons afin que votre application utilise simplement les Application Default Credentials comme si elle se trouvait sur une VM ordinaire. Trois choses diffèrent suffisamment pour vous poser problème : GKE nécessite deux bascules d'activation (cluster et pool de nœuds), GKE peut accorder des permissions cloud à un ServiceAccount Kubernetes sans aucun objet d'identité cloud, et - comme toujours - le téléchargement de votre image de conteneur est une identité différente de votre identité de charge de travail.
Le problème que les deux résolvent
Avant la fédération, accorder des permissions cloud à un pod impliquait de mauvaises options : intégrer une clé statique dans l'image ou un Secret (fuites, difficultés de rotation), ou accorder la permission à l'ensemble du nœud afin que chaque pod sur celui-ci l'hérite (pas d'isolation, portée excessive). Les solutions temporaires côté AWS étaient des intercepteurs IMDS de nœud comme kube2iam et kiam ; côté GCP, il s'agissait de monter une clé de compte de service de nœud dans les pods. Toutes étaient complexes, trop larges et sujettes aux conditions de concurrence.
La fédération remplace cela par une confiance cryptographique. Le cluster projette un jeton OIDC signé et de courte durée, associé au ServiceAccount du pod. Le cloud vérifie ce jeton par rapport aux clés publiques du cluster et, si l'identité du jeton correspond à une règle que vous avez configurée, renvoie des identifiants pour une seule identité cloud. Aucun secret n'est stocké ; le jeton s'auto-renouvelle (durée de vie par défaut d'environ une heure) et est inutile en dehors du cluster.
Le mécanisme partagé : la fédération OIDC
Chaque configuration d'identité de charge de travail, sur l'un ou l'autre cloud, est le même triangle de confiance :
- Le cluster est un émetteur OIDC. Il expose un document de découverte et un point de terminaison JWKS avec les clés publiques qu'il utilise pour signer les jetons ServiceAccount. Cet émetteur est l'ancre de confiance.
- Le cloud fait confiance à cet émetteur pour une identité ServiceAccount spécifique. AWS exprime la confiance par rôle ; GCP l'exprime une fois par projet, de manière permanente, et Google la gère pour vous.
- La charge de travail échange le jeton projeté contre des identifiants cloud et reçoit des identifiants de courte durée pour une seule identité cloud.
Tout le reste est une question de nommage - et de la quantité de l'étape 2 que vous devez construire vous-même. C'est là que les deux clouds divergent le plus.
La correspondance un-à-un
- Côté Kubernetes. Sur AWS, vous annotez le ServiceAccount avec
eks.amazonaws.com/role-arn: <role arn>. Sur GKE, dans le modèle direct moderne, vous n'annotez rien du tout sur le ServiceAccount - vous le référencez simplement comme un principal IAM. (L'ancien modèle d'impersonation utilise une annotation ; plus de détails ci-dessous.) - Le "fournisseur OIDC". Sur AWS, il s'agit de l'émetteur OIDC du cluster EKS, que vous enregistrez une fois dans IAM en tant que fournisseur d'identité OIDC - par cluster. Sur GKE, il s'agit du pool d'identités de charge de travail du projet
PROJECT_ID.svc.id.goog, créé automatiquement et géré par Google ; vous ne l'enregistrez jamais, et chaque ServiceAccount de chaque cluster du projet est déjà un principal à l'intérieur de celui-ci. - L'identité. AWS : un rôle IAM. Modèle direct GKE : aucun objet d'identité dédié - le ServiceAccount Kubernetes lui-même est le principal. Modèle d'impersonation GKE : un compte de service Google que le KSA impersonne.
- La règle de confiance. AWS : la politique de confiance du rôle IAM épingle
sub = system:serviceaccount:<ns>:<sa>etaud = sts.amazonaws.com. GKE direct : il n'y a pas de document de confiance séparé - vous accordez un rôle directement à un identifiant de principal qui encode le namespace et le ServiceAccount. GKE d'impersonation : vous accordez au KSA le rôleroles/iam.workloadIdentityUsersur le compte de service Google. - Les permissions. AWS : une politique IAM attachée au rôle. GKE : des liaisons de rôle de politique d'autorisation IAM sur la ressource cible (ou le projet) -
roles/storage.objectViewer,roles/secretmanager.secretAccessor, etc. C'est la division plus profonde : AWS attache une politique à l'identité ; GCP accorde un rôle au niveau de la portée de la ressource. - L'échange. AWS : le SDK à l'intérieur du pod appelle
sts:AssumeRoleWithWebIdentityavec le jeton projeté. GKE : le serveur de métadonnées du nœud effectue l'échange de manière transparente et votre application utilise les Application Default Credentials sans aucun code d'échange.
Le reste de cet article décrit chaque cloud de bout en bout, puis s'attarde sur les différences qui posent réellement problème.
Présentation : AWS EKS IRSA, de bout en bout
Les éléments, dans l'ordre du flux de confiance :
1. L'émetteur OIDC du cluster, à une URL comme https://oidc.eks.<region>.amazonaws.com/id/<hash>. Vous l'enregistrez une fois dans IAM en tant que fournisseur d'identité OIDC afin qu'IAM fasse confiance aux jetons qu'il signe.
2. Le rôle IAM et sa politique de confiance. La politique de confiance rend le rôle assumable par un seul ServiceAccount et rien d'autre :
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::<account>:oidc-provider/oidc.eks.<region>.amazonaws.com/id/<hash>" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.<region>.amazonaws.com/id/<hash>:sub": "system:serviceaccount:apps:checkout",
"oidc.eks.<region>.amazonaws.com/id/<hash>:aud": "sts.amazonaws.com"
}
}
}
Épinglez à la fois :sub et :aud - n'épingler que l'émetteur permettrait à n'importe quel ServiceAccount du cluster d'assumer le rôle.
3. La politique IAM attachée au rôle accorde ce dont l'application a besoin (s3:GetObject sur un bucket, et ainsi de suite).
4. Le ServiceAccount porte une annotation :
apiVersion: v1
kind: ServiceAccount
metadata:
name: checkout
namespace: apps
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account>:role/checkout
5. La projection. EKS intègre le Pod Identity Webhook dans le cluster. Lorsqu'un pod utilise le ServiceAccount annoté, le webhook injecte AWS_ROLE_ARN et AWS_WEB_IDENTITY_TOKEN_FILE et monte un jeton projeté (audience sts.amazonaws.com, auto-renouvelé).
6. L'échange. Le SDK AWS détecte AWS_WEB_IDENTITY_TOKEN_FILE, appelle sts:AssumeRoleWithWebIdentity et met en cache les identifiants de courte durée renvoyés. Votre code est simplement boto3.client("s3").
Alternative plus récente : EKS Pod Identity (2023) fait le même travail via un agent sur le cluster et une API d'association au lieu d'un fournisseur IAM OIDC par cluster et d'une politique de confiance par rôle - ce qui, notamment, le rend beaucoup plus similaire au modèle géré de GKE. IRSA et Pod Identity coexistent en production ; IRSA est celui qui correspond le mieux au cadre de fédération OIDC, c'est donc celui à retenir ici.
Présentation : GKE Workload Identity Federation, de bout en bout
Même flux de confiance, beaucoup moins à construire :
1. Deux bascules d'activation. Activez le pool d'identités de charge de travail sur le cluster et le serveur de métadonnées sur le pool de nœuds :
gcloud container clusters update <cluster> --workload-pool=<project-id>.svc.id.goog
gcloud container node-pools update <pool> --cluster=<cluster> --workload-metadata=GKE_METADATA
Les deux sont requis. L'indicateur de cluster intègre le cluster dans le pool du projet ; l'indicateur de pool de nœuds déploie le serveur de métadonnées GKE (un DaemonSet sur chaque nœud) qui interceptera les requêtes d'identifiants. Un pod sur un pool de nœuds sans GKE_METADATA obtient l'identité du nœud, pas la sienne - un repli silencieux et trop large.
2. Le pool existe déjà. <project-id>.svc.id.goog est créé automatiquement pour le projet ; vous n'enregistrez jamais de fournisseur OIDC, et il n'y a pas de limite de fournisseur par compte à atteindre. Chaque ServiceAccount dans les clusters du projet est déjà un principal.
3a. Accès direct (le défaut moderne). Accordez un rôle directement au ServiceAccount, adressé comme un principal IAM. Pas de compte de service Google, pas d'annotation :
gcloud storage buckets add-iam-policy-binding gs://<bucket> \
--role=roles/storage.objectViewer \
--member="principal://iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/<project-id>.svc.id.goog/subject/ns/apps/sa/checkout"
Le chemin du principal encode ns/apps/sa/checkout - le namespace et le ServiceAccount sont la règle de confiance ; il n'y a pas de document de confiance séparé à écrire.
3b. Impersonation (l'ancien modèle, toujours nécessaire pour quelques services). Créez un compte de service Google, laissez le KSA l'impersonner et annotez le KSA :
gcloud iam service-accounts add-iam-policy-binding checkout@<project-id>.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:<project-id>.svc.id.goog[apps/checkout]"
apiVersion: v1
kind: ServiceAccount
metadata:
name: checkout
namespace: apps
annotations:
iam.gke.io/gcp-service-account: checkout@<project-id>.iam.gserviceaccount.com
Ici, le compte de service Google détient les permissions (via des liaisons de rôle normales), et le KSA les emprunte.
4. L'échange. Lorsque votre application demande des identifiants, le serveur de métadonnées GKE intercepte la requête vers http://metadata.google.internal, récupère un JWT de ServiceAccount depuis l'API Kubernetes pour ce pod, l'échange via le Security Token Service contre un jeton d'accès fédéré de courte durée (par défaut 1 heure, rafraîchi de manière proactive avant expiration), et le renvoie. Votre application utilise les Application Default Credentials et les bibliothèques clientes Google Cloud standard - elle se comporte exactement comme si elle s'exécutait sur une VM GCE. Il n'y a pas de webhook injectant des variables d'environnement, pas de variables de style AZURE_*, et pas d'appel d'échange dans votre code : une lecture de Cloud Storage est simplement storage.Client() en Python.
Le sujet est le pivot (sur les deux clouds)
La valeur qui doit correspondre exactement, sur les deux clouds, est l'identité du ServiceAccount :
- AWS épingle le sujet du jeton
system:serviceaccount:<namespace>:<serviceaccount>dans la politique de confiance du rôle. - GKE direct encode la même paire dans le chemin du principal
.../subject/ns/<namespace>/sa/<serviceaccount>. - GKE impersonation l'encode dans le membre
serviceAccount:<project-id>.svc.id.goog[<namespace>/<serviceaccount>].
L'échec le plus courant de type "il ne s'authentifie comme personne" sur l'un ou l'autre cloud est une non-concordance ici : le chart déployé dans un namespace différent, le ServiceAccount a reçu le nom de la release au lieu du nom de l'application, ou le pod est revenu au ServiceAccount default. Lorsque la fédération échoue silencieusement, vérifiez d'abord le namespace et le nom du ServiceAccount.
Différences qui vous poseront réellement problème
1. GKE enregistre le "fournisseur OIDC" pour vous - une fois, par projet, pour toujours. Sur EKS, vous enregistrez un fournisseur IAM OIDC par cluster, et à l'échelle d'une flotte, vous pouvez atteindre la limite souple de 100 fournisseurs OIDC par compte AWS et devoir la contourner. Sur GKE, il n'y a qu'un seul pool par projet (PROJECT_ID.svc.id.goog), créé et géré par Google, partagé par chaque cluster du projet. Rien à enregistrer, rien à plafonner. L'autre face de la médaille : la confiance est par défaut à l'échelle du projet, vous la délimitez donc avec des liaisons IAM (à quel namespace et ServiceAccount vous accordez des permissions), et non en divisant les fournisseurs.
2. GKE échange le jeton sur un serveur de métadonnées local au nœud, et non via un appel SDK dans le pod. IRSA modifie le pod (un webhook injecte des variables d'environnement et un jeton projeté) et le SDK appelle STS. GKE intercepte metadata.google.internal sur le nœud et renvoie les identifiants de manière transparente, de sorte que l'application utilise les Application Default Credentials comme si elle se trouvait sur une VM ordinaire. Deux conséquences : votre application n'a pas besoin de code d'identifiants spécifique au cloud, et le pool de nœuds doit avoir le serveur de métadonnées activé (GKE_METADATA) ou le pod obtient silencieusement l'identité du nœud au lieu de la sienne.
3. GKE peut accorder des permissions cloud à un ServiceAccount sans objet d'identité cloud. Dans IRSA, il y a toujours un rôle IAM. Dans le modèle direct de GKE, il n'y a rien à créer côté cloud, à l'exception de la liaison de rôle elle-même - le ServiceAccount Kubernetes est le principal (principal://.../subject/ns/NS/sa/SA). L'ancien modèle d'impersonation introduit un compte de service Google et l'annotation iam.gke.io/gcp-service-account, et vous en avez toujours besoin pour la poignée de services qui n'acceptent pas directement un principal fédéré - mais privilégiez d'abord la liaison directe.
4. GKE nécessite deux bascules, comme Azure. Cluster (--workload-pool) plus pool de nœuds (--workload-metadata=GKE_METADATA). Oubliez la moitié du pool de nœuds et il n'y a pas d'échec, juste la mauvaise identité (celle du nœud). IRSA intègre la projection dans EKS lui-même, il n'y a donc pas de deuxième interrupteur équivalent.
5. Le téléchargement de l'image est une identité différente de l'identité de la charge de travail - sur les deux clouds. Le téléchargement d'image est le travail de l'identité du nœud : sur GKE, le compte de service Google du pool de nœuds a besoin de roles/artifactregistry.reader pour télécharger depuis Artifact Registry ; sur EKS, le rôle d'instance du groupe de nœuds a besoin de AmazonEC2ContainerRegistryReadOnly (ou vous utilisez des secrets de téléchargement). L'identité de charge de travail est uniquement destinée à l'appel d'une API cloud par le code de l'application. Un service HTTP pur qui ne communique jamais avec un SDK cloud n'a pas besoin d'identité de charge de travail du tout - et accorder une identité de charge de travail artifactregistry.reader ne fait rien pour le téléchargement d'image, car le téléchargement a lieu avant que l'application (et son jeton fédéré) ne s'exécute.
6. Les permissions s'attachent différemment. AWS attache une politique IAM au rôle - la permission voyage avec l'identité. GCP accorde une liaison de rôle IAM au niveau de la portée de la ressource cible (ou du projet) - l'octroi réside sur l'élément accédé, et non sur l'identité. Même état final, endroit différent pour l'auditer : sur AWS, lisez les politiques attachées au rôle ; sur GCP, listez la politique IAM de la ressource (ou utilisez Policy Analyzer pour savoir ce qu'un principal peut atteindre).
Ce que fait le code de l'application
Les deux clouds aboutissent à "pas de secret, pas de code d'identifiants", mais ils y parviennent différemment :
- AWS : le webhook définit
AWS_ROLE_ARNetAWS_WEB_IDENTITY_TOKEN_FILE; la chaîne par défaut du SDK appelleAssumeRoleWithWebIdentity. Votre code :boto3.client("s3"). - GKE : rien n'est injecté dans le pod ; la chaîne Application Default Credentials du SDK atteint le serveur de métadonnées du nœud, qui effectue l'échange. Votre code :
storage.Client().
Dans les deux cas, les identifiants sont de courte durée et rafraîchis de manière transparente. Il n'y a rien à faire pivoter, rien à stocker et rien à divulguer.
Une liste de contrôle de débogage qui fonctionne sur les deux clouds
Lorsqu'un pod "ne peut pas s'authentifier", parcourez la chaîne de confiance dans l'ordre :
- Fédération cluster/projet activée ? EKS : le fournisseur IAM OIDC est enregistré. GKE : le cluster a
--workload-poolet le pool de nœuds aGKE_METADATA. Sur GKE, une bascule de pool de nœuds manquante est l'échec silencieux classique - le pod obtient l'identité du nœud, pas la sienne. - Bon ServiceAccount ? Le pod utilise réellement le ServiceAccount que vous pensez (pas
default). Exécutez exec et vérifiez. - Correspondance d'identité ? Comparez la règle de confiance au namespace et au ServiceAccount réels :
system:serviceaccount:<ns>:<sa>sur AWS,.../subject/ns/<ns>/sa/<sa>(ou[ns/sa]) sur GKE. C'est l'échec le plus fréquent sur les deux. - Échange fonctionnel ? EKS : le pod a
AWS_WEB_IDENTITY_TOKEN_FILE. GKE : curl -H "Metadata-Flavor: Google" metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email depuis le pod renvoie l'identité attendue. - Permissions ? Seulement après que l'identité est résolue : la politique IAM sur le rôle (AWS) ou la liaison de rôle sur la ressource cible (GCP). Un résultat vide ici est un échec d'autorisation, pas d'authentification - ce qui indique que la fédération elle-même fonctionne.
Quelles certifications abordent ce sujet
L'identité de charge de travail se situe à l'intersection de Kubernetes, de l'IAM cloud et de la fédération OIDC, elle apparaît donc dans trois parcours d'examen. Si vous détenez déjà un identifiant cloud, le même concept sur l'autre cloud est un petit pas.
Côté AWS :
- AWS Certified Solutions Architect - Associate (SAA-C03) - Rôles IAM, EKS et comment les identités obtiennent des permissions cloud.
- AWS Certified Security - Specialty (SCS-C03) - Politiques de confiance IAM, fédération OIDC et délimitation du moindre privilège (exactement la chaîne de confiance IRSA).
Côté Google Cloud :
- Google Cloud Associate Cloud Engineer - GKE, IAM, comptes de service et liaisons de rôle.
- Google Cloud Professional Cloud Security Engineer - Identité de charge de travail, profondeur IAM, sécurité des comptes de service et moindre privilège.
Côté Kubernetes :
- CNCF Certified Kubernetes Administrator (CKA) - ServiceAccounts, jetons projetés et spécifications de pod.
- CNCF Certified Kubernetes Security Specialist (CKS) - Sécurité des jetons ServiceAccount, moindre privilège et réduction de l'exposition aux secrets statiques.
En résumé
IRSA et GKE Workload Identity Federation sont la même idée avec des implémentations différentes : le cluster signe un jeton OIDC de courte durée pour un ServiceAccount, et le cloud l'échange contre des identifiants associés à une seule identité. AWS vous fait assembler la confiance vous-même - un fournisseur OIDC par cluster, un rôle IAM, une politique de confiance, un jeton projeté que le SDK échange. GKE vous fournit un pool géré à l'échelle du projet, vous permet de lier un rôle directement au ServiceAccount Kubernetes sans aucun objet d'identité cloud, et effectue l'échange sur un serveur de métadonnées de nœud afin que votre application ne voie jamais d'identifiant. Apprenez le triangle de confiance une fois et la traduction est mécanique : le rôle devient une liaison de principal (ou un compte de service que vous impersonnez), la politique de confiance devient une chaîne de membre IAM, la politique attachée devient une liaison de rôle sur la ressource, et AssumeRoleWithWebIdentity devient un échange invisible via le serveur de métadonnées. Gardez trois choses à l'esprit - les deux bascules de GKE, son modèle direct sans objet d'identité, et le fait que le téléchargement d'image est une identité entièrement différente - et les refus qui semblaient aléatoires commencent à être interprétés comme une conception cohérente et délibérée.