Aller au contenu
Conteneurs & Orchestration medium

ServiceAccounts pour développeurs

35 min de lecture

logo kubernetes

Un ServiceAccount est l'identité Kubernetes de votre application. Chaque Pod est associé à un ServiceAccount. Lorsqu'un Pod doit appeler l'API Kubernetes, il peut utiliser les credentials montés pour ce ServiceAccount, sauf si ce montage est désactivé. Ce guide vous montre comment créer des ServiceAccounts dédiés avec les permissions minimales nécessaires.

  • Comprendre le rôle des ServiceAccounts comme identité
  • Créer un ServiceAccount dédié
  • Configurer les permissions avec Role et RoleBinding
  • Maîtriser les tokens projetés modernes
  • Désactiver le montage de token quand inutile
  • Appliquer les bonnes pratiques de sécurité

Avant d'entrer dans le détail, ce tableau donne la combinaison d'objets à retenir selon le besoin. La ligne la plus utile est la quatrième : un Pod qui n'appelle jamais l'API Kubernetes ne doit tout simplement pas recevoir de token, et c'est le cas de la majorité des applications web classiques.

BesoinOutil recommandé
Pod lit des ressources dans son namespaceRole + RoleBinding
Même permission réutilisée dans plusieurs namespacesClusterRole + RoleBinding (par namespace)
Accès réellement global au clusterClusterRole + ClusterRoleBinding
Pod n'appelle jamais l'API KubernetesautomountServiceAccountToken: false
Test ponctuel d'un tokenkubectl create token

Par défaut, un Pod utilise le ServiceAccount default du namespace. Ce n'est pas forcément dangereux en soi, mais c'est rarement un bon choix pour une application de production :

ProblèmeConséquence
Identité partagéePlusieurs workloads apparaissent sous la même identité vis-à-vis de l'API
Traçabilité réduiteLa corrélation d'audit devient moins fine
Permissions héritéesTout RoleBinding sur default s'applique à tous les Pods

Solution : Un ServiceAccount par application (ou groupe d'applications avec le même besoin).

L'objet est volontairement minimal : un nom et un namespace suffisent. Un ServiceAccount est local à son namespace, deux objets portant le même nom dans deux namespaces différents sont deux identités distinctes. À sa création il ne dispose d'aucun droit, il faudra un Role et un RoleBinding pour lui en accorder.

serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: mon-app

Ou en une ligne :

Fenêtre de terminal
kubectl create serviceaccount app-sa -n mon-app

RBAC (Role-Based Access Control) définit ce que le ServiceAccount peut faire. Le modèle repose sur trois éléments qu'il faut relier : un sujet (ici le ServiceAccount), un ensemble de règles décrivant des actions autorisées, et un binding qui associe les deux. Kubernetes fonctionne en refus par défaut, tout ce qui n'est pas explicitement autorisé est interdit, et il n'existe aucune règle de refus à écrire.

Quatre objets suffisent à couvrir tous les cas, et leur nom porte à confusion. Retenez que le préfixe Cluster indique la portée de l'objet, pas nécessairement celle des permissions accordées : c'est le binding choisi qui tranche.

RessourcePortéeDescription
RoleNamespacePermissions dans un namespace
ClusterRoleClusterDéfinition de permissions réutilisable
RoleBindingNamespaceAssocie Role ou ClusterRole à un sujet dans un namespace
ClusterRoleBindingClusterAssocie ClusterRole à un sujet pour tout le cluster

Un Role liste les permissions accordées dans un namespace :

role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: mon-app
rules:
- apiGroups: [""] # "" = core API group
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]

Les verbes disponibles :

VerbeAction
getLire une ressource
listLister les ressources
watchObserver les changements
createCréer une ressource
updateModifier une ressource
patchModifier partiellement
deleteSupprimer une ressource

Le RoleBinding connecte le Role au ServiceAccount :

rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: mon-app
subjects:
- kind: ServiceAccount
name: app-sa
namespace: mon-app
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io

Réutiliser un ClusterRole dans plusieurs namespaces

Section intitulée « Réutiliser un ClusterRole dans plusieurs namespaces »

Cas fréquent : vous avez besoin des mêmes permissions dans plusieurs namespaces, mais pas sur tout le cluster.

Solution : Créez un ClusterRole (définition réutilisable), puis un RoleBinding par namespace.

clusterrole-reusable.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: configmap-reader
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]

Puis, dans chaque namespace où c'est nécessaire :

rolebinding-namespace-a.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-configmap-reader
namespace: namespace-a
subjects:
- kind: ServiceAccount
name: app-sa
namespace: namespace-a
roleRef:
kind: ClusterRole # Référence le ClusterRole
name: configmap-reader
apiGroup: rbac.authorization.k8s.io

Certaines charges de travail ont besoin de voir l'ensemble du cluster : un agent de supervision qui collecte les métriques de tous les namespaces, un opérateur qui surveille ses ressources personnalisées partout. Dans ce cas seulement, le ClusterRoleBinding accorde les permissions du ClusterRole sur la totalité du cluster, y compris les namespaces créés après le binding.

clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: namespace-reader
rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: app-namespace-reader
subjects:
- kind: ServiceAccount
name: app-sa
namespace: mon-app
roleRef:
kind: ClusterRole
name: namespace-reader
apiGroup: rbac.authorization.k8s.io

Le champ serviceAccountName se déclare dans la spécification du Pod. Sur un Deployment, il se place donc dans spec.template.spec et non directement sous le spec du Deployment, confusion fréquente qui fait échouer l'application du manifeste. Si le champ est omis, Kubernetes attribue le ServiceAccount default du namespace, et le ServiceAccount d'un Pod existant ne peut pas être modifié après création.

pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: mon-pod
namespace: mon-app
spec:
serviceAccountName: app-sa
containers:
- name: app
image: mon-image:1.0.0

Pour un Deployment :

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mon-app
spec:
replicas: 3
template:
spec:
serviceAccountName: app-sa
containers:
- name: app
image: mon-image:1.0.0

Le token est la preuve d'identité que le Pod présente à l'API server pour être reconnu comme « ce ServiceAccount ». Sa gestion a profondément changé au fil des versions de Kubernetes : les jetons permanents stockés dans des Secrets ont cédé la place à des jetons projetés, de courte durée et renouvelés automatiquement. Comprendre cette différence évite de reproduire des pratiques devenues risquées.

Les clusters Kubernetes modernes montent des tokens projetés et liés au Pod. Ces tokens sont :

  • À durée limitée, renouvelés automatiquement avant expiration
  • Audience-bound, portent une audience contrôlée (typiquement l'API server)
  • Liés au Pod, invalidés si le Pod est supprimé

Structure du répertoire monté :

/var/run/secrets/kubernetes.io/serviceaccount/
├── ca.crt # Certificat CA du cluster
├── namespace # Namespace du Pod
└── token # Token JWT (renouvelé automatiquement)

Pour tester une identité ou intégrer un composant externe :

Fenêtre de terminal
# Token avec durée par défaut (1h)
kubectl create token app-sa -n mon-app
# Token avec durée personnalisée
kubectl create token app-sa -n mon-app --duration=24h

La sortie est un JWT signé par l'API server. En décodant son header avec base64, vous obtenez l'algorithme et l'identifiant de clé :

Fenêtre de terminal
kubectl create token app-sa -n mon-app | cut -d. -f1 | base64 -d
# {"alg":"RS256","kid":"LxkUHA9ITF5w3mg80D102dT48j1E4k1DsNIgUcHOrRg"}

Le payload (deuxième segment) contient les claims standard : aud (audience), exp (expiration), iss (issuer), sub (sujet) et un bloc kubernetes.io qui rattache le token au namespace et au ServiceAccount.

Par défaut, l'API server signe les tokens ServiceAccount avec une clé privée stockée localement (--service-account-signing-key-file). Cette clé ne quitte jamais le control plane, mais elle reste un secret hautement sensible : sa compromission permettrait de forger n'importe quelle identité de pod.

Kubernetes 1.36 fait passer en GA la signature externe (KEP-5066) : l'API server délègue l'opération de signature à un service externe via un endpoint Unix domain socket. La clé privée vit alors dans un KMS (HashiCorp Vault, AWS KMS, GCP KMS, Azure Key Vault…) et n'est jamais exposée au control plane.

L'API server ne fabrique plus la signature lui-même : il transmet la demande au service externe par une socket Unix locale et reçoit le jeton signé en retour. La clé privée reste donc chez le fournisseur de signature, et le control plane n'en détient jamais de copie.

Le kube-apiserver, configuré par --service-account-signing-endpoint, envoie la demande de signature au service externe (Vault, KMS) par socket Unix et reçoit le token signé en retour ; la clé privée reste chez le service de signature et n'est jamais exposée au control plane

Sur kubeadm, ajouter dans /etc/kubernetes/manifests/kube-apiserver.yaml :

spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
- --service-account-signing-endpoint=unix:///var/run/k8s-signer/signer.sock
# Garder --service-account-issuer pour la cohérence des claims
- --service-account-issuer=https://kubernetes.default.svc.cluster.local
volumeMounts:
- name: signer-socket
mountPath: /var/run/k8s-signer
volumes:
- name: signer-socket
hostPath:
path: /var/run/k8s-signer
type: Directory

Le service de signature (Vault avec le plugin vault-plugin-secrets-kubernetes ou un binaire maison conforme au protocole gRPC signing.k8s.io/v1) écoute sur la même socket Unix.

L'activation de la signature externe est transparente côté workloads : les tokens projetés montés dans les pods continuent d'avoir le même format JWT RS256, le même issuer et la même structure de claims. Seul le porteur de la clé privée change. Aucune adaptation des SDK Kubernetes (client-go, fabric8, kubernetes-python) n'est nécessaire.

Les versions antérieures de Kubernetes créaient des tokens statiques dans des Secrets de type kubernetes.io/service-account-token. Ces tokens :

  • N'expirent jamais
  • Restent valides même après suppression du Pod
  • Sont plus difficiles à révoquer
❌ À éviter - token statique legacy
apiVersion: v1
kind: Secret
metadata:
name: app-sa-token
annotations:
kubernetes.io/service-account.name: app-sa
type: kubernetes.io/service-account-token

Par défaut, Kubernetes monte le token du ServiceAccount dans chaque conteneur du Pod, qu'il en ait besoin ou non. Un attaquant qui obtient l'exécution de code dans le conteneur récupère donc immédiatement une identité valide auprès de l'API server. Retirer ce montage quand il est inutile est l'une des mesures les plus rentables du durcissement Kubernetes : coût nul, surface d'attaque réduite.

La checklist sécurité Kubernetes recommande de ne pas monter de token dans les Pods qui n'en ont pas besoin. Exemples :

Type de workloadBesoin de token ?
Frontend statique (nginx, Caddy)❌ Non
Worker applicatif pur (calcul, ML)❌ Non
Job batch sans interaction API❌ Non
Application qui lit des ConfigMaps via API✅ Oui
Opérateur Kubernetes✅ Oui
Sidecar qui contacte l'API✅ Oui

Le champ automountServiceAccountToken: false supprime le montage pour ce Pod uniquement. Le répertoire /var/run/secrets/kubernetes.io/serviceaccount/ disparaît alors du conteneur : c'est le contrôle à faire pour valider la configuration.

pod-sans-token.yaml
apiVersion: v1
kind: Pod
metadata:
name: frontend
spec:
serviceAccountName: frontend-sa
automountServiceAccountToken: false
containers:
- name: nginx
image: nginx:1.25.4

Pour définir un comportement par défaut pour tous les Pods utilisant ce ServiceAccount :

sa-sans-automount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: frontend-sa
namespace: mon-app
automountServiceAccountToken: false

Cette séquence assemble tout ce qui précède sur un cas réaliste : une application qui doit lire les Pods de son propre namespace. Les trois étapes se déroulent dans l'ordre, la vérification finale avec kubectl auth can-i étant la seule qui prouve réellement que le RBAC est correct. Testez toujours l'autorisation attendue et une opération qui doit échouer.

  1. Créez les ressources RBAC

    rbac-complet.yaml
    apiVersion: v1
    kind: ServiceAccount
    metadata:
    name: app-sa
    namespace: mon-app
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
    name: pod-reader
    namespace: mon-app
    rules:
    - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
    name: read-pods
    namespace: mon-app
    subjects:
    - kind: ServiceAccount
    name: app-sa
    namespace: mon-app
    roleRef:
    kind: Role
    name: pod-reader
    apiGroup: rbac.authorization.k8s.io
    Fenêtre de terminal
    kubectl apply -f rbac-complet.yaml
  2. Déployez votre application

    deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: mon-app
    namespace: mon-app
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: mon-app
    template:
    metadata:
    labels:
    app: mon-app
    spec:
    serviceAccountName: app-sa
    containers:
    - name: app
    image: bitnamilegacy/kubectl:1.29.0
    command: ["sleep", "3600"]
  3. Vérifiez les permissions

    Fenêtre de terminal
    # Ce que le ServiceAccount peut faire
    kubectl auth can-i list pods \
    --as=system:serviceaccount:mon-app:app-sa \
    -n mon-app
    # Résultat: yes
    # Ce qu'il ne peut PAS faire
    kubectl auth can-i delete pods \
    --as=system:serviceaccount:mon-app:app-sa \
    -n mon-app
    # Résultat: no

Ces trois blocs de règles couvrent la majorité des besoins rencontrés en développement. Ils sont classés du plus restreint au plus ciblé, et se copient tels quels dans un Role. Rappel de lecture : apiGroups: [""] désigne le groupe d'API core, celui des Pods, Services, ConfigMaps et Secrets.

Le trio get, list, watch correspond au besoin d'un client qui recharge sa configuration à chaud. watch est indispensable dès que l'application s'appuie sur un mécanisme d'écoute des changements, faute de quoi elle repasse en interrogation périodique.

rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]

Un opérateur écrit vraiment dans le cluster, ses permissions sont donc bien plus larges. La seconde règle est facile à oublier et pourtant nécessaire : sans le droit de créer des events, l'opérateur fonctionne mais ne peut plus rendre compte de ses actions dans kubectl describe.

rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]

C'est la formulation à privilégier dès qu'il s'agit de Secrets : resourceNames restreint la règle à deux objets nommés, au lieu d'ouvrir la lecture de tous les Secrets du namespace. Le verbe get seul, sans list, empêche également d'énumérer ce qui existe.

rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["app-credentials", "db-password"]
verbs: ["get"]

Les recommandations ci-dessous se regroupent en trois temps : nommer et organiser les identités, restreindre ce qu'elles peuvent faire, puis vérifier dans la durée que ces droits restent justifiés. Le troisième volet est le plus négligé, alors que les permissions RBAC ne font que s'accumuler si personne ne les revoit.

Une convention de nommage n'est pas cosmétique : elle rend lisible la sortie de kubectl get rolebindings -o wide au moment où vous cherchez qui a le droit de faire quoi.

  1. Un ServiceAccount par application, Pas de partage entre applications différentes
  2. Convention de nommage, <app>-sa pour identifier facilement
  3. Documentation, Annotez vos ServiceAccounts avec leur usage

Ces six règles appliquent toutes le même principe : n'accorder que ce qui est démontré nécessaire. Le joker * dans les verbes mérite une vigilance particulière, il accorde silencieusement les nouveaux verbes introduits par les versions ultérieures de Kubernetes.

  1. Principe du moindre privilège, N'accordez que les permissions strictement nécessaires
  2. Évitez * dans les verbes, Listez explicitement les actions autorisées
  3. Utilisez resourceNames quand possible, Limitez l'accès à des ressources spécifiques
  4. Préférez Role à ClusterRole, Scope namespace sauf besoin cluster-wide
  5. Désactivez automount si inutile, Réduisez la surface d'attaque
  6. Préférez les tokens projetés, Évitez les tokens statiques en Secret

Les permissions sont accordées au moment où un besoin apparaît, presque jamais retirées quand il disparaît. Une revue périodique, appuyée sur kubectl auth can-i --list, est le seul moyen de repérer ces droits devenus orphelins.

  1. Revoyez régulièrement les permissions, Les besoins évoluent
  2. Utilisez kubectl auth can-i, Testez ce qu'un ServiceAccount peut faire
  3. Activez l'audit logging, Tracez les actions sur l'API

Un message Forbidden indique toujours quel sujet, quel verbe et quelle ressource ont été refusés : lisez-le en entier avant de modifier quoi que ce soit. L'option --as=system:serviceaccount:<namespace>:<nom> permet ensuite de rejouer la question à la place du ServiceAccount, sans déployer le moindre Pod. C'est l'outil de diagnostic principal, à condition de disposer vous-même du droit d'usurpation.

Fenêtre de terminal
# Lister toutes les permissions d'un ServiceAccount
kubectl auth can-i --list \
--as=system:serviceaccount:mon-app:app-sa \
-n mon-app
# Tester une permission spécifique
kubectl auth can-i create deployments \
--as=system:serviceaccount:mon-app:app-sa \
-n mon-app
# Voir les RoleBindings d'un namespace
kubectl get rolebindings -n mon-app -o wide
# Voir les ClusterRoleBindings qui concernent un ServiceAccount
kubectl get clusterrolebindings -o wide | grep app-sa
# Créer un token pour tester
kubectl create token app-sa -n mon-app

Un admission controller est un module de l'API server qui inspecte une ressource après l'autorisation RBAC, et peut la modifier ou la rejeter. C'est lui qui explique les refus les plus déroutants : vous avez le droit de créer un Pod, la requête est bien authentifiée et autorisée, et pourtant elle échoue. Savoir distinguer un refus RBAC d'un refus d'admission fait gagner beaucoup de temps de diagnostic.

Quand vous créez un Pod, la requête passe par 3 étapes :

Traitement d'une requête par l'API server en trois étapes successives : l'authentification identifie l'appelant par token ServiceAccount, certificat ou OIDC, l'autorisation vérifie le Role ou ClusterRole via RBAC, puis le contrôle d'admission applique webhooks de validation et de mutation, Pod Security Admission et quotas

Le vocabulaire de l'erreur permet de trancher sans réfléchir : Forbidden signale un refus RBAC, tandis qu'un message mentionnant un webhook, PodSecurity ou un quota signale un refus d'admission. Le tableau ci-dessous illustre les quatre cas les plus courants.

ProblèmeRBACAdmissionRésultat
Pas de Role pour créer des Podsnon atteintForbidden
Pod privilégié dans namespace restricted❌ PSARejected
ResourceQuota dépassé❌ QuotaRejected
Image non signée❌ PolicyRejected

PSA remplace les anciennes PodSecurityPolicies. Il applique les Pod Security Standards (PSS) au niveau namespace :

NiveauCe qui est autoriséCas d'usage
privilegedToutPods système, debug
baselinePas de privileges dangereuxApplications standards
restrictedConfiguration la plus sécuriséeProduction, multi-tenant
Fenêtre de terminal
# Vérifier le niveau PSA d'un namespace
kubectl get namespace mon-app -o yaml | grep pod-security

Si votre Pod est refusé avec une erreur mentionnant pod-security.kubernetes.io, c'est PSA qui bloque.

Le message de rejet remonte directement quand vous créez le Pod à la main. Créé par un Deployment ou un Job, le Pod n'existe jamais et l'erreur se lit alors dans les événements de l'objet parent, ce qui explique l'impression trompeuse d'un déploiement « bloqué sans raison ».

Fenêtre de terminal
# L'erreur apparaît dans le message de rejet
kubectl apply -f pod.yaml
# Error: admission webhook "validate.policy" denied the request...
# Ou via describe sur le parent (Deployment, Job...)
kubectl describe deployment mon-app -n mon-namespace
# Events:
# Warning FailedCreate ... violates PodSecurity "restricted:latest"

Ces quatre contrôleurs sont configurés par l'équipe plateforme, pas par vous. Votre marge d'action consiste à connaître les contraintes qu'ils imposent et à écrire des manifestes qui les respectent dès le départ, plutôt qu'à découvrir chaque règle par un rejet.

Admission ControllerCe qu'il vérifieComment s'adapter
PSAPrivileges, capabilities, volumesRespecter les Pod Security Standards
ResourceQuotaCPU/mémoire du namespaceVérifier les quotas disponibles
LimitRangeRequests/limits par PodDéfinir requests/limits conformes
ValidatingWebhookRègles custom (Kyverno, Gatekeeper)Consulter les policies actives
ConceptUtilisation
ServiceAccountIdentité d'un workload dans Kubernetes
RolePermissions dans un namespace
ClusterRoleDéfinition de permissions réutilisable
RoleBindingAssocie un rôle à un sujet dans un namespace
ClusterRoleBindingAssocie un rôle à un sujet pour tout le cluster
serviceAccountNameSpécifie l'identité du Pod
automountServiceAccountTokenContrôle le montage du token
kubectl create tokenCrée un token ponctuel pour tests

Règle d'or : Le ServiceAccount est une identité, les permissions viennent de RBAC, et le token ne doit être monté que lorsqu'il y a un vrai besoin.

Ce quiz reprend les distinctions qui posent le plus de difficultés : identité contre permissions, ClusterRole contre ClusterRoleBinding, et les conditions dans lesquelles un token doit être monté.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

7 questions
5 min.
80% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn