Utiliser IRSA dans un cluster Kubernetes hors EKS

22 juin 2025 par Martin Catty8 minutes

AWS propose un système à la fois puissant et sécurisé pour donner des autorisations à nos pods Kubernetes sans avoir à manipuler (et assurer la rotation) de clés ou token, il s’agit d’IRSA.

À quoi ça sert ?

Imaginez que votre application ait besoin d’accéder à un bucket S3. Vous allez probablement créer un rôle qui aura les droits suffisants pour faire le nécessaire : lire, écrire…

Pour utiliser ce rôle vous pouvez créer des identifiants (key id et key secret) et les injecter dans votre pod sous forme de secrets.

Mais AWS propose un mécanisme plus élégant et sécurisé via IRSA (IAM role for Service Account).

Service account

Lorsque nous crééons un pod dans notre cluster Kubernetes, celui ci hérite (ou pas selon notre configuration) d’un token, celui du ServiceAccount configuré par notre pod.

Ce ServiceAccount donne une identité à notre pod et permet d’y associer des permissions de type RBAC (Role Based Access Control).

L’authentification du pod se fait au travers d’un jeton JWT, qui est stocké par défaut dans /var/run/secrets/kubernetes.io/serviceaccount/token.

Si on décode un jeton, par exemple celui d’un pod qui fait tourner ce site :

jq -R '.' /var/run/secrets/kubernetes.io/serviceaccount/token  | jq 'split(".")|{header: .[0]|@base64d|fromjson, payload: .[1]|@base64d|fromjson}'

on voit bien le contenu de notre JWT :

{
  "header": {
    "alg": "RS256",
    "kid": "Z_ktNvbbBECOxLclHC46p5cMlFTGxzQVtfD50S41Ld8"
  },
  "payload": {
    "aud": [
      "https://kubernetes.default.svc.cluster.local",
      "k3s"
    ],
    "exp": 1781121747,
    "iat": 1749585747,
    "iss": "https://kubernetes.default.svc.cluster.local",
    "jti": "f27e4a7b-b1c7-43a6-b327-6022edfb82fc",
    "kubernetes.io": {
      "namespace": "website",
      "node": {
        "name": "worker-3",
        "uid": "f3266943-2933-487d-9a35-e6a756a4b05e"
      },
      "pod": {
        "name": "website-b49bb9b7f-b45pc",
        "uid": "e399270b-526c-45b8-ac0a-141c85e7f2fe"
      },
      "serviceaccount": {
        "name": "default",
        "uid": "6c44f7d8-f28f-4ff0-a18d-eb1a6edb0966"
      },
      "warnafter": 1749589354
    },
    "nbf": 1749585747,
    "sub": "system:serviceaccount:website:default"
  }
}

Notre pod utilise le ServiceAccount default, dans le namespace website.

Si on fait la même chose pour un pod lambda qui tourne dans un cluster EKS:

{
  "header": {
    "alg": "RS256",
    "kid": "a715867f84eace896d9a43b605e29ba93b3d9f09"
  },
  "payload": {
    "aud": [
      "https://kubernetes.default.svc"
    ],
    "exp": 1782141601,
    "iat": 1750605601,
    "iss": "https://oidc.eks.ca-central-1.amazonaws.com/id/76DA39F82A5363AA579A40F5296B8E2C",
    "jti": "bae90f82-c1d0-489b-80a8-4460b2e39053",
    "kubernetes.io": {
      "namespace": "incredible",
      "node": {
        "name": "ip-10-0-3-214.ca-central-1.compute.internal",
        "uid": "cf60907d-369c-42fd-9e70-f642465de26a"
      },
      "pod": {
        "name": "incredible-client-6b647595dd-wxc6b",
        "uid": "a60b5efb-1d60-4e40-b350-664906952ddf"
      },
      "serviceaccount": {
        "name": "incredible-client",
        "uid": "6afe8c21-70bc-4787-8b2c-2a04750a4996"
      },
      "warnafter": 1750609208
    },
    "nbf": 1750605601,
    "sub": "system:serviceaccount:incredible:incredible-client"
  }
}

On a globalement la même chose, hormis pour la partie iss (issuer). En effet notre cluster dispose par défaut d’une URL OpenID Connect.

C’est l’URL de notre fournisseur d’identité (idp), propre à chaque cluster EKS. Nous ne pouvons pas directement voir comment cela fonctionne car c’est géré par le control plane de Kubernetes auquel nous n’avons pas accès.

Mais c’est sa responsabilité de fournir un token JWT valide.

Pour chacun des appels du pod à l’API server ce token va être repassé en bearer et validé par l’api server (signature, subject) afin de déterminer si le ServiceAccount a les permissions suivantes pour faire ce qu’il demande.

Attacher un rôle à un service account avec AWS

Imaginons que nous ayons créé un rôle s3-role dans AWS permettant d’accéder à notre bucket.

Nous pouvons, dans un contexte EKS, créer un ServiceAccount en l’annotant du rôle qu’il pourra endosser.

apiVersion: v1
automountServiceAccountToken: true
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::AWS_ACCOUNT_ID:role/s3-role

Si un pod utilise ce ServiceAccount, il pourra automatiquement assumer le rôle IAM associé et accéder à notre bucket sans fournir explicitement de clés d’accès ou secret.

Derrière le rideau, EKS fournit au pod un token JWT signé, qu’il peut présenter à AWS STS pour obtenir des identifiants temporaires (access key, secret key, session token).

Attacher un rôle à un service account d’un autre compte AWS

La force du système réside dans le fait qu’il s’appuie sur des standards ouverts, notamment OpenID Connect (OIDC).

AWS permet de fédérer plusieurs fournisseurs d’identité (IdP), y compris de clusters EKS issus d’autres comptes AWS, via l’ajout de leurs URL OIDC.

Cette URL devient alors une source de confiance : AWS est capable de vérifier les tokens JWT émis par ce cluster et de permettre l’accès à ses services via des rôles IAM conditionnés au sub (subject) ou à d’autres claims du token.

Fédérer un cluster non AWS dans AWS

Après cette longue introduction, venant en aux faits. À partir du moment ou AWS s’appuie sur des standards je me suis mis en tête de fédérer un cluster non EKS dans mon tenant AWS.

Le but est de bénéficier du même mécanisme IRSA dans des clusters externes, ici avec un cluster k3s.

Dans mon cas cela me sert à « redescendre » des secrets gérés dans AWS secrets manager avec external secrets operator. Mais nous aurons l’occasion d’en reparler dans un autre article 😊.

Exposer la configuration

Pour fonctionner, OpenID Connect s’appuie sur un standard qui nécessite l’exposition d’informations de configuration sur un endpoint bien défini.

Cette approche permet à AWS (ou tout autre service) de découvrir automatiquement comment interagir avec notre fournisseur d’identité.

Le point de découverte OpenID Connect

Le standard OpenID Connect définit un endpoint de découverte situé à l’adresse :

https://url/.well-known/openid-configuration

Cette URL va exposer un fichier JSON de configuration qui décrit les capacités de notre fournisseur d’identité. Par exemple :

{
  "issuer": "https://url-de-mon-cluster.tld/",
  "jwks_uri": "https://url-de-mon-cluster.tld/keys.json",
  "authorization_endpoint": "urn:kubernetes:programmatic_authorization",
  "response_types_supported": [
      "id_token"
  ],
  "subject_types_supported": [
      "public"
  ],
  "id_token_signing_alg_values_supported": [
      "RS256"
  ],
  "claims_supported": [
      "sub",
      "iss"
  ]
}
  • issuer : L’URL qui identifie de manière unique notre fournisseur d’identité. C’est cette valeur qui sera utilisée dans le champ iss des tokens JWT émis par notre cluster.
  • jwks_uri : L’URL où AWS pourra récupérer les clés publiques utilisées pour vérifier la signature des tokens JWT.
  • authorization_endpoint : Ce champ est requis par la spécification OIDC, mais Kubernetes ne propose pas de login interactif. Nous utilisons donc une valeur symbolique (urn:kubernetes:programmatic_authorization) pour indiquer que l’authentification est entièrement non interactive (par jetons JWT générés automatiquement pour les pods).
  • id_token_signing_alg_values_supported : Les algorithmes de signature supportés (RS256 est l’algorithme standard recommandé).
  • claims_supported : Les claims (revendications) que notre fournisseur d’identité peut inclure dans les tokens.

AWS a besoin de ces informations pour :

  • Vérifier que les tokens proviennent bien de notre cluster
  • Vérifier les signatures : utiliser les clés publiques pour valider la signature des tokens JWT
  • Savoir quelles informations sont disponibles dans les tokens pour les utiliser dans les politiques IAM

Sans cette exposition, AWS ne pourrait pas faire confiance aux tokens émis par notre cluster k3s.

Dans mon cas j’ai simplement exposé ce endpoint de manière statique via mon ingress. Les JSON openid-configuration et keys.json sont stockés dans des configmap.

Générer des clés de signature pour les ServiceAccount

Pour que notre cluster k3s puisse émettre des tokens JWT valides et sécurisés, nous devons configurer des clés de signature permettent de signer ces tokens.

Pourquoi des clés de signature ?

Les tokens JWT contiennent des informations sensibles (namespace, service account, etc.).

Sans signature cryptographique, n’importe qui pourrait créer des tokens frauduleux.

Les clés de signature permettent à AWS de vérifier que les tokens proviennent bien de notre cluster.

Génération de la paire de clés RSA

Nous utilisons RSA-2048, un algorithme de cryptographie asymétrique standard et sécurisé :

ssh-keygen -t rsa -b 2048 -f signing.key -m pemls

Cette commande génère :

  • signing.key : La clé privée (à garder secrète)
  • signing.key.pub : La clé publique (que nous exposerons plus tard via l’endpoint JWKS)
chmod 600 signing.key*

Conversion au format PKCS8

ssh-keygen -e -m PKCS8 -f signing.key.pub > signing.key.pub.pkcs8

Cette conversion transforme la clé publique au format PKCS8, qui est le standard attendu par les systèmes OpenID Connect et AWS pour la validation des signatures JWT.

Utilisation dans k3s

Une fois ces clés générées, elles doivent être configurées dans notre service k3s pour signer les tokens des ServiceAccount.

La clé privée reste dans le cluster pour signer les tokens, tandis que la clé publique est exposée via l’endpoint JWKS pour permettre la vérification par AWS.

On dépose nos fichiers précédemment générés (signing.key, signing.key.pub.pkcs8) dans le dossier /etc/rancher/k3s/tls/ sur les master qui gèrent le control plane de notre cluster.

Nous allons maintenant reconfigurer le service k3s pour utiliser ces clés. Pour cela on met à jour le fichier du service /etc/systemd/system/k3s.service, pour y ajouter les paramètres de démarrage suivant :

ExecStart=/usr/local/bin/k3s \
    server \
    '--kube-apiserver-arg=api-audiences=sts.amazonaws.com' \
    '--kube-apiserver-arg=service-account-key-file=/etc/rancher/k3s/tls/signing.key.pub.pkcs8' \
    '--kube-apiserver-arg=service-account-key-file=/var/lib/rancher/k3s/server/tls/service.key' \
    '--kube-apiserver-arg=service-account-signing-key-file=/etc/rancher/k3s/tls/signing.key' \
    '--kube-apiserver-arg=service-account-issuer=https://oidc-url.tld' \
    '--kube-apiserver-arg=service-account-issuer=k3s'

Les paramètres --kube-apiserver-arg=service-account-key-file et --kube-apiserver-arg=service-account-issuer peuvent être passés plusieurs fois, ce qui n’oblige pas à invalider les token précédemment émis.

Les tokens nouvellement émis auront donc l’audience sts.amazonaws.com.

Nous pouvons maintenant redémarrer notre k3s.

systemctl restart k3s

Récupération et exposition des clés

Depuis une des machines du cluster k3s :

kubectl get --raw /openid/v1/jwks | jq > keys.json

Il reste à récupérer ce fichier et à l’exposer au même titre que .well-known/openid-configuration.

Injecter automatiquement les credentials

Comme nous ne sommes pas dans un cluster EKS, nous ne disposons pas automatiquement d’une solution permettant de dynamiquement injecter des credentials dans le pod.

En effet, comme expliqué plus tôt, AWS le gère automatiquement pour nous en coulisse en s’occupant de fournir et gérer la rotation des tokens dans un cluster EKS.

Toutefois Amazon met à disposition le code d’Amazon EKS Pod Identity Webhook que l’on va pouvoir installer et utiliser dans notre cluster k3s.

Un pré-requis est d’avoir un cert manager :

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.18.0/cert-manager.yaml

Et après avoir cloné le dépôt :

make cluster-up IMAGE=amazon/amazon-eks-pod-identity-webhook:latest

Nous voyons les objets Kubernetes suivants :

NAME                                        READY   STATUS    RESTARTS   AGE
pod/pod-identity-webhook-764c6c7bdc-6chtr   1/1     Running   0          11d

NAME                           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/kubernetes             ClusterIP   10.43.0.1       <none>        443/TCP   15d
service/pod-identity-webhook   ClusterIP   10.43.135.168   <none>        443/TCP   11d

NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/pod-identity-webhook   1/1     1            1           11d

NAME                                              DESIRED   CURRENT   READY   AGE
replicaset.apps/pod-identity-webhook-764c6c7bdc   1         1         1       11d

Il ne nous reste qu’à ajouter l’URL de notre IDP dans la console AWS pour qu’elle soit fédérée.

Tester IRSA avec le cli AWS

Si vous voulez vérifier la capacité de vos tokens à assumer un rôle IAM, vous pouvez récupérer le token JWT d’un de vos pods, le stocker par exemple dans un fichier token.jwt et jouer une commande de ce type :

aws sts assume-role-with-web-identity \
  --role-arn arn-du-role \
  --role-session-name test \
  --web-identity-token file://token.jwt

Conclusion

Nous avons vu comment faire bénéficier à un cluster tiers du mécanisme IRSA pour interagir avec des services AWS, ce qui nous offre le meilleur des deux mondes.

Nous pouvons ainsi bénéficier d’une intégration sécurisée de certains services d’AWS pour nos applications mais sans nécessairement déployer de clusters Kubernetes avec EKS.

ⓕ ⓤ ⓢ ⓔ 👊🏼