À retenir
- Les droits d’administration permanents sur des clusters partagés sont un passif. Unit 42 constate que 99 % des identités cloud sont sur-privilégiées, et que 66 % des attaques par ingénierie sociale en 2025 visaient directement des comptes à privilèges.
- La parade tient dans une CRD
PrivilegedAccessRequest: une élévation limitée dans le temps qui nomme un humain, un rôle, un namespace, avec expiration automatique et un plafond strict de 48 heures.- Deux bugs issus d’un projet réel valent la peine d’être repris : une attribution à portée cluster qui aurait dû rester à portée namespace, et une incohérence de format sur le fil qui produisait un accès déjà expiré à l’arrivée.
- Le zéro privilège permanent intégral reste un idéal. La cible pragmatique de 2026, c’est le just-in-time partout où cela compte, en réservant le zéro privilège permanent aux segments les plus exposés.
Sur un cluster Kubernetes mutualisé, un identifiant sur-privilégié n’est pas le problème d’une seule équipe. C’est un rayon d’impact partagé par tous les tenants du cluster. Les enjeux augmentent donc avec le nombre d’équipes hébergées. Voici une architecture de référence pour éliminer les privilèges permanents sur les clusters partagés. Et elle y parvient sans transformer chaque réponse à incident en file d’attente de tickets pour l’équipe plateforme. Elle provient d’un projet réel en production : la plateforme Kubernetes multi-tenant d’un grand constructeur automobile européen, où des centaines d’équipes applicatives partagent des clusters mutualisés — le type de parc que nous exploitons depuis des années, comme le raconte notre décennie avec Kubernetes. Nous gardons les noms et les identifiants génériques à dessein. C’est le motif qui voyage.
Pourquoi les clusters partagés rendent-ils les droits d’administration permanents intenables ?
Les clusters partagés démultiplient le coût de chaque compte sur-privilégié. L’étude 2025 d’Unit 42 constate que 99 % des utilisateurs, rôles et comptes de service cloud sont sur-privilégiés (Palo Alto Networks Unit 42, 2025). La même équipe rapporte que 66 % des attaques par ingénierie sociale ont ciblé des comptes à privilèges (Unit 42 Global Incident Response Report, 2025). Sur des clusters mutualisés, ce calcul s’aggrave vite.
Alors pourquoi le partage relève-t-il autant les enjeux ? Parce que les murs entre tenants sont plus fins qu’ils n’en ont l’air. Le privilège permanent a aussi une longue traîne. GitGuardian constate que 64 % des secrets valides en 2022 étaient toujours actifs quatre ans plus tard (GitGuardian, 2026). Un identifiant accordé pour un incident ponctuel est rarement révoqué dans les temps. Il traîne. Et sur un cluster partagé, il traîne avec une portée dans des namespaces que son propriétaire a oubliés depuis longtemps.
Le coupable habituel, c’est la prolifération RBAC. En pratique, cela se traduit par des verbes en wildcard, des ClusterRoleBinding sans limite, des RoleBinding copiés-collés, et des workloads qui s’appuient sur le compte de service par défaut (vcluster.com, 2025). Chacun est minime pris isolément. Ensemble, sur un cluster partagé par des centaines d’équipes, ils deviennent la surface d’attaque.
Note de sécurité : Les bindings RBAC de Kubernetes se combinent selon une logique de OU, jamais de ET. Si un utilisateur est sujet de deux bindings, il obtient l’union des deux ensembles de permissions, jamais leur intersection. Sur un cluster multi-tenant, un
ClusterRoleBindingégaré peut silencieusement élargir un accès qu’unRoleBindingsoigneusement délimité était censé contenir.
Qu’est-ce qu’une PrivilegedAccessRequest, et comment fonctionne l’élévation limitée dans le temps ?
Une PrivilegedAccessRequest (PAR) est une ressource personnalisée qui représente une seule élévation limitée dans le temps. Elle nomme un humain, un rôle, une portée cible et une durée. Un contrôleur crée l’attribution à l’ouverture de la fenêtre. Puis il la révoque à sa fermeture. Il n’y a donc aucun nettoyage manuel ni identifiant résiduel, ce qui contre le problème des 64 % de secrets périmés évoqué plus haut.
Imaginez un incident un vendredi soir. Le tunnel de paiement est à l’arrêt, et une ingénieure a besoin d’un accès réel à la production, tout de suite. Plutôt que de réveiller l’équipe plateforme pour un RoleBinding ponctuel, elle dépose une PAR de 45 minutes. Le contrôleur l’accorde, elle résout la panne, et l’accès disparaît de lui-même avant qu’elle ne se déconnecte. Aucun ticket. Aucun droit d’administration résiduel en attente d’être détourné.
La conception maintient volontairement l’accès de secours (break-glass) séparé du RBAC de régime permanent. Le moindre privilège au quotidien reste statique : le développement obtient editor, la production obtient viewer, par convention. Deux invariants de politique rendent ce mécanisme sûr à confier aux tenants. D’abord, la requête nomme une seule adresse e-mail d’utilisateur, jamais un groupe. Ensuite, un webhook d’admission plafonne strictement la durée à 48 heures.
Voici une PAR générique, expurgée de la conception de production :
apiVersion: platform.example.io/v1alpha1
kind: PrivilegedAccessRequest
metadata:
name: par-incident-4821
namespace: team-checkout-services
spec:
environmentRef:
name: checkout-prod
namespace: team-checkout-services
user: [email protected] # exactly one human, never a group
role: tenant-admin # tenant-admin | tenant-editor | tenant-viewer
duration: 45m # Go duration string, hard-capped at 48h
status:
phase: Ready
expiryTime: "2026-07-04T14:12:00Z"
remoteRoleBindingRef:
kind: RoleBinding # namespace-scoped, see the next section
namespace: gke-checkout-prod
name: par-jordan-tenant-admin
Le contrôleur assume les parties ingrates mais critiques pour la sûreté. Il réconcilie l’attribution de façon idempotente, de sorte qu’une réconciliation répétée ne crée jamais un binding en double. Il replanifie aussi selon un rythme serré : le minimum entre « le temps restant avant expiration » et un court intervalle de sécurité. Résultat, il ne peut pas manquer une fenêtre de révocation, même si une réconciliation intermédiaire est sautée. Une fois la fenêtre fermée, il collecte l’objet PAR environ une heure plus tard. La piste d’audit vit dans les logs, pas dans l’objet Kubernetes. Les requêtes expirées ne s’accumulent donc jamais en encombrement du cluster.
La durée est modifiable, et ce détail compte. Prolonger une requête active recalcule l’expiration à partir de la date de création d’origine, et non du moment de la modification. Cela émet aussi un événement AccessExtended. Le webhook applique toujours le plafond de 48 heures sur la fenêtre recalculée. D’expérience, c’est ce qui distingue une prolongation propre d’un intervenant qui réinitialise accidentellement sa propre horloge en plein incident.
Piège : L’ordre des opérations à l’intérieur du reconciler n’est pas cosmétique. L’étape du cycle de vie qui provisionne le binding doit s’exécuter avant la vérification d’expiration, car la sortie anticipée sur expiration dépend de la référence du binding déjà renseignée. Inversez-les, et chaque requête boucle indéfiniment sur un statut non renseigné, sans jamais réellement provisionner l’accès. Cette classe de bug d’ordonnancement passe les tests unitaires et n’échoue qu’en réconciliation réelle.
RoleBinding contre ClusterRoleBinding : pourquoi la portée brise-t-elle discrètement le cloisonnement ?
La portée est le champ le plus important d’une attribution JIT, et elle reste invisible jusqu’à ce que quelqu’un l’audite. La conception prévoyait un RoleBinding à portée namespace. À la place, la première implémentation a livré un ClusterRoleBinding à portée cluster. Les deux compilent. Les deux passent un test de cas nominal où l’utilisateur fait exactement ce qu’il voulait dans exactement un namespace. Mais seul le second confère à cet utilisateur des droits sur tous les namespaces d’un cluster partagé.
Avez-vous déjà livré un binding qui fonctionnait parfaitement en test et ouvrait quand même une porte que vous n’aviez jamais voulu ouvrir ? Celui-ci a été attrapé par un relecteur pendant une revue de code, pas par un test, et c’est tout le sujet. Un ClusterRoleBinding accordant tenant-admin ressemble presque à s’y méprendre à un RoleBinding accordant le même rôle. La différence tient à un champ kind et à un namespace manquant. Sur un cluster mono-tenant, l’erreur est inoffensive. Sur un cluster mutualisé, elle devient une escalade transversale aux tenants. Et aucune équipe applicative ne s’en apercevrait jamais, parce que son propre flux de travail continue de fonctionner parfaitement.
Le contraste, écrit noir sur blanc, est délibérément anodin :
# WRONG on a shared cluster: grants the role cluster-wide, across every tenant
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: par-jordan-tenant-admin
roleRef:
kind: ClusterRole
name: tenant-admin
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: User
name: [email protected]
---
# RIGHT: same role, confined to one tenant's namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: par-jordan-tenant-admin
namespace: gke-checkout-prod # the scope that makes it safe
roleRef:
kind: ClusterRole # a ClusterRole referenced by a RoleBinding
name: tenant-admin # applies only within this namespace
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: User
name: [email protected]
La conception résolue livre la forme à portée namespace. Le contrôleur inscrit un remoteRoleBindingRef dans le statut de la PAR. Cette référence nomme le RoleBinding exact, son namespace et son nom déterministe. Un auditeur peut donc confirmer la portée depuis l’objet lui-même, plutôt que de faire confiance au code. Si vous ne retenez qu’un seul point de checklist de revue de cet article, retenez celui-ci : sur tout cluster partagé, une attribution JIT qui produit un ClusterRoleBinding est un bug jusqu’à preuve du contraire.
Le bug de format sur le fil : quand des secondes se faisaient passer pour une durée
Les incohérences de contrat entre couches sont les bugs qui survivent à la revue, parce que chaque couche paraît correcte prise isolément. Dans ce projet, la couche CRD et kubectl exprimait la durée sous forme de chaîne de durée Go, comme "45m" ou "4h0m0s". Le contrat sur le fil de l’API server exprimait le même champ en secondes entières. Les deux formes étaient valides. Elles ne s’accordaient simplement pas, et rien dans l’une ni l’autre couche ne signalait le heurt.
La défaillance était subtile et totale. Sous le capot, le message protobuf Duration porte des nanosecondes en int64. Et l’encodeur JSON de l’API server n’appelle pas le marshaler de durée personnalisé de Go comme le fait la bibliothèque standard. Une requête signifiant « 300 secondes » arrivait donc sous la forme de l’entier 300. Le serveur le lisait ensuite comme 300 nanosecondes. Chaque PrivilegedAccessRequest concernée atterrissait déjà expirée, à environ trois cent milliardièmes de seconde dans le passé. L’objet paraissait sain. L’accès n’a jamais existé.
Piège : Une attribution née expirée est pire qu’une attribution qui échoue bruyamment. L’objet se déclare créé, le statut paraît plausible, et en plein incident l’intervenant croit détenir un accès qu’il n’a pas. L’équipe a donc ajouté une conversion explicite secondes-vers-nanosecondes à la frontière de l’API server. Elle l’a gardée séparée de la forme chaîne de durée propre à la CRD, afin que les deux contrats ne se contaminent plus jamais l’un l’autre.
Un cousin plus modeste a été livré en même temps. La colonne d’impression Expiry utilisait un type date. Mais une régression de kubectl échouait à parser des timestamps RFC3339 valides sous ce type. Elle affichait <invalid> alors même que la valeur stockée était toujours correcte. Basculer la colonne d’impression vers un type chaîne a corrigé l’affichage. La donnée stockée n’a jamais été fausse. Reste que, pour un contrôle de sécurité, un opérateur qui ne peut pas se fier à la colonne d’expiration sous ses yeux est un vrai problème.
Comment un webhook d’admission doit-il autoriser les requêtes d’élévation ?
Le webhook d’admission applique l’autorisation selon deux voies indépendantes avant d’autoriser une requête. Il admet immédiatement les rôles transverses, à l’échelle de la plateforme, sans contrôle de tenant, car ils couvrent légitimement toute la plateforme. Il n’admet les rôles par asset qu’après avoir confirmé que l’appelant possède bien le tenant auquel il veut accéder. Cette scission est la frontière de sécurité porteuse de toute la conception.
Le contrôle par asset ferme une véritable voie d’escalade. Chaque rôle par asset encode un identifiant d’asset issu de la CMDB de l’entreprise. Le webhook parse donc cet identifiant depuis le groupe de l’appelant, résout le namespace cible vers son enregistrement de tenant, et exige que les deux correspondent. Par exemple, un utilisateur détenant le rôle admin pour l’asset A ne peut pas ouvrir une PrivilegedAccessRequest contre un namespace appartenant à l’asset B. La première version du parser sautait entièrement ce contrôle de propriété. L’équipe a trouvé et fermé cette faille transversale aux tenants le jour même où elle a câblé le webhook.
Qu’y a-t-il de l’autre côté de ce contrôle ? Une CRD d’inventaire de tenants synchronisée depuis la CMDB. Elle porte l’identifiant d’asset, la criticité et la propriété faisant autorité pour chaque tenant. Le webhook ne fait pas confiance à la portée revendiquée par la requête. Il fait plutôt confiance à l’inventaire, et l’inventaire reflète la source de vérité organisationnelle.
Note de sécurité : Les rôles transverses sont puissants par conception. Gardez donc la liste d’autorisation des rôles transverses courte, explicite et codée en dur plutôt que déduite par filtrage de motif. Dans ce projet, le webhook traite les rôles operator et superuser d’un asset comme transverses uniquement pour cet identifiant d’asset précis. Il rejette les rôles homonymes de tout autre asset, au lieu de les promouvoir silencieusement à l’échelle de la plateforme. Un filtrage de motif ici rouvrirait exactement la faille que le contrôle de propriété referme.
Quel rôle jouent les portes de criticité sur les clusters mutualisés ?
Les portes de criticité décident si un workload de moindre criticité peut s’exécuter sur un cluster de plus haute criticité. Par défaut, le webhook d’admission applique une égalité stricte : la criticité d’un tenant doit correspondre exactement à celle de son cluster. Ce défaut est sûr, mais trop rigide pour les clusters mutualisés. Là, un cluster classé critique héberge légitimement des tenants dont les propres workloads ne sont que standard.
L’assouplissement est opt-in et à portée du propriétaire. Une annotation allow-below-criticality: "true" sur le cluster desserre le contrôle, de l’égalité stricte vers « la criticité du tenant est inférieure ou égale à celle du cluster ». L’annotation vit sur le cluster, pas sur le tenant qui demande l’accès. C’est donc le propriétaire du cluster qui décide de la politique, pas l’équipe qui demande à déposer un workload. Le webhook valide sa valeur et n’accepte que true ou false, ce qui empêche une simple faute de frappe de modifier silencieusement l’application de la règle.
Un second correctif a rectifié l’endroit d’où la configuration lit la criticité. Pour les clusters mutualisés, un cluster peut surcharger sa propre criticité via annotation. Dans ce cas, la configuration dérivée préfère la surcharge à la valeur de l’inventaire de tenants, et ne se rabat sur l’inventaire que lorsque l’annotation est absente. L’application de la règle reste entièrement dans le webhook d’admission. L’API server ne fait aucune validation de criticité de son côté. Un seul endroit auditable décide donc de cette politique.
Piège : « En dessous de la criticité » est numériquement contre-intuitif. Strategic se classe au-dessus de critical, critical au-dessus de standard, si bien qu’un nombre plus élevé signifie moins critique. Quand vous écrivez la comparaison, un tenant est autorisé lorsque son nombre de criticité est supérieur ou égal à celui du cluster. Inversez l’inégalité et vous bloquez tout workload valide ou, pire, vous laissez passer ceux que vous vouliez justement tenir à l’écart.
Pourquoi faire de la CMDB la source de vérité du RBAC ?
L’accès doit refléter l’organisation, pas un tas de bindings édités à la main. Un contrôleur synchronise en continu la CMDB de l’entreprise, la propriété, l’appartenance aux groupes et les métadonnées d’asset, vers les enregistrements de tenants et le RBAC de Kubernetes. Ainsi, quand un responsable produit change ou qu’un membre d’équipe arrive, le binding suit automatiquement. Personne n’édite un ClusterRoleBinding à la main, et chaque binding géré reste auditable par label. C’est la même discipline de synchronisation de plan de contrôle qui alimente un plan de contrôle Kubernetes as a Service complet, appliquée au privilège plutôt qu’à l’infrastructure.
L’accès se scinde en deux voies qui se rejoignent sur le même vocabulaire de rôles. La voie des équipes applicatives gouverne les droits à l’intérieur des propres namespaces d’un tenant. Le responsable de chaque équipe délègue ces droits via l’appartenance à un groupe d’annuaire. La voie des groupes d’annuaire gouverne les rôles transverses, à l’échelle de la plateforme, déclarés une fois et appliqués à chaque tenant. Les deux se résolvent vers des sujets d’identité fédérée plutôt que vers des noms d’utilisateur bruts. Résultat, un seul changement d’appartenance de groupe côté annuaire se propage dans le RBAC de Kubernetes sans aucune modification de code.
Une protection subtile préserve les champs sur lesquels tout le reste s’appuie. Le contrôleur capture les champs d’identité comme l’identifiant d’asset, la criticité et le value stream une seule fois à la création de l’enregistrement de tenant, puis les gèle. Si une récupération CMDB ultérieure diverge, le contrôleur ne les écrase pas. Il pose plutôt un label de changement, pour qu’un humain décide de basculer ou non. D’expérience, c’est ce qui empêche une modification CMDB de routine de remodeler silencieusement le RBAC et la politique réseau de dizaines de tenants en production, en plein vol.
Architecture de référence et construire ou acheter
Assemblé, le motif est compact. Une CRD PrivilegedAccessRequest modélise l’élévation limitée dans le temps. Un webhook d’admission applique la règle de l’utilisateur unique, le plafond de 48 heures et le contrôle de propriété à deux voies. Un contrôleur crée et révoque un RoleBinding à portée namespace selon un calendrier déterministe. Et une synchronisation CMDB maintient la propriété et la criticité faisant autorité. C’est la référence neutre que la littérature des éditeurs dessine rarement : la réalité de la logique de OU du RBAC, l’accès éphémère et un flux d’approbation, reliés en une seule conception multi-tenant — la moitié sécurité du dossier plus large en faveur de l’ingénierie de plateforme pilotée par Kubernetes.
Alors, faut-il construire ceci ou l’acheter ? Tout dépend du degré de spécificité de votre modèle de cloisonnement. Des plateformes commerciales comme Teleport, Apono, P0 Security et CyberArk couvrent bien la requête, l’approbation et l’accès éphémère. Pour un parc standard, elles sont la voie la plus rapide. L’approche CRD sur mesure gagne sa place quand l’accès doit être recoupé avec une CMDB interne et un modèle de criticité qu’aucun produit sur étagère ne connaît. C’est ce couplage, et non la mécanique JIT, qui force habituellement la construction.
Fixez honnêtement les attentes sur la destination. Le zéro privilège permanent intégral reste un idéal pour la plupart des parcs. La cible pragmatique de 2026, c’est le JIT partout où cela compte, en réservant le zéro privilège permanent aux segments les plus exposés (Teleport, 2026). Cela s’aligne proprement sur les moteurs d’audit des deux côtés de l’Atlantique. En Europe, NIS2 et l’ISO 27001 attendent une minimisation des accès démontrable et une revue régulière. En Amérique du Nord, SOC 2 attend la même chose pour l’accès à privilèges. Des attributions limitées dans le temps, assorties d’une piste d’audit propre, satisfont ces contrôles bien mieux qu’un tableur d’administrateurs permanents.
Conclusion
Le privilège permanent est le réglage par défaut discret que les clusters partagés peuvent le moins se permettre. Unit 42 range 99 % des identités cloud dans le camp du sur-privilège, et les comptes à privilèges attirent les deux tiers des attaques par ingénierie sociale (Palo Alto Networks Unit 42, 2025). Le coût de laisser traîner des droits d’administration ne fait donc que croître à mesure que davantage d’équipes partagent le même plan de contrôle. Une CRD PrivilegedAccessRequest, un webhook de propriété à deux voies et une synchronisation pilotée par la CMDB renversent ce réglage par défaut sans ralentir personne.
Les leçons qui voyagent sont les plus ingrates. Délimitez vos attributions au namespace. Validez vos contrats sur le fil à travers chaque couche. Vérifiez la propriété à l’admission, pas par inférence. Et gelez les champs d’identité dont dépend le reste de la plateforme. Aucune ne se montre dans une démo. Pourtant, toutes décident si votre système JIT est réellement sûr sur un cluster partagé par des centaines d’équipes. Visez donc le JIT partout où cela compte, et réservez le zéro privilège permanent aux segments où le rayon d’impact court le plus haut.
Réponses directes
Questions fréquentes
Qu'est-ce que l'accès à privilèges just-in-time dans Kubernetes ?
L'accès à privilèges just-in-time accorde des droits Kubernetes élevés uniquement pour une fenêtre bornée, puis les révoque automatiquement. Au lieu de `RoleBinding` d'administration permanents, un intervenant demande une élévation limitée dans le temps qui nomme un utilisateur, un rôle et un namespace. Cela cible directement le sur-privilège, qui affecte 99 % des identités cloud selon Unit 42 (Palo Alto Networks Unit 42, 2025).
Une attribution JIT doit-elle utiliser un RoleBinding ou un ClusterRoleBinding ?
Sur un cluster partagé, utilisez un `RoleBinding` à portée namespace. Un `ClusterRoleBinding` accorde le rôle sur tous les namespaces, ce qui devient une escalade transversale aux tenants sur des clusters mutualisés. Les deux formes se ressemblent presque à l'identique en YAML, à un champ près, alors traitez toute attribution JIT qui produit un `ClusterRoleBinding` comme un bug jusqu'à preuve du contraire.
Combien de temps un accès just-in-time doit-il durer ?
Assez longtemps pour la tâche, assez court pour limiter l'exposition, avec un plafond strict fixé par la politique. Ce projet plafonne chaque requête à 48 heures et retombe sur des durées en minutes pour le travail courant. L'expiration automatique compte, car les équipes révoquent rarement les attributions à la main : GitGuardian constate que 64 % des secrets de 2022 étaient toujours actifs quatre ans plus tard (GitGuardian, 2026).
L'accès just-in-time remplace-t-il le RBAC ?
Non. Il complète le RBAC statique plutôt que de le remplacer. Le moindre privilège de régime permanent gouverne toujours le travail quotidien à travers des `RoleBinding` normaux, synchronisés depuis votre source de vérité. Le JIT gère les exceptions : réponse à incident et tâches élevées rares. Garder les deux séparés isole la piste d'audit et la logique d'expiration de vos garde-fous de namespace du quotidien.