Gouvernance des identités : SSO OIDC, RBAC par profil & audit
Objectifs pédagogiques¶
- Définir des profils d'accès et appliquer la séparation des tâches sur toute la chaîne de livraison
- Brancher ArgoCD, Grafana et OpenBao sur un fournisseur d'identité unique (SSO OIDC)
- Traduire les profils en RBAC ArgoCD, Kubernetes et OpenBao, au moindre privilège
- Encadrer un compte d'urgence (break-glass) sans en faire une porte dérobée
- Activer, centraliser et exploiter les journaux d'audit, puis tester les droits automatiquement
Pourquoi gouverner les identités¶
Ce qui tourne mal sans gouvernance¶
- Un compte de développeur hameçonné donne accès à la production
- Des comptes partagés rendent toute enquête impossible : « admin » n'est personne
- Des droits accordés pour une urgence ne sont jamais retirés
- Un départ n'est pas répercuté dans tous les outils
- Personne ne sait répondre à l'auditeur : qui a accès à quoi, et qui a fait quoi
Les principes¶
- Identité nominative : chaque action est rattachée à une personne ou à un workload précis
- Source unique : les identités humaines vivent chez un seul fournisseur, les outils s'y branchent
- Moindre privilège : des droits par profil et par environnement, jamais « admin par défaut »
- Séparation des tâches : celui qui écrit un changement n'est pas celui qui l'approuve
- Cycle de vie : une arrivée ou un départ se gère à un seul endroit
- Traçabilité : chaque accès sensible laisse une trace exploitable
Profils & séparation des tâches¶
Des profils, pas des personnes¶
| Profil | Staging | Production | Outils |
|---|---|---|---|
| Développeur | lecture, synchronisation | aucun accès | Git, ArgoCD, Grafana |
| Opérateur | lecture, synchronisation | lecture, synchronisation | ArgoCD, Grafana, OpenBao prod |
| Sécurité | lecture | lecture hors secrets | Grafana, DefectDojo, audit |
| Auditeur | lecture | lecture des journaux | Grafana (lecture) |
| Break-glass | administrateur | hors ligne, alerté |
- Chaque profil correspond à un groupe chez le fournisseur d'identité
- Les droits s'attribuent au groupe, jamais à l'individu
Séparation des tâches¶
Écrire, approuver et déployer en production sont trois droits distincts
- Git : branche
mainprotégée, merge réservé aux opérateurs, approbation obligatoire par une autre personne que l'auteur - CODEOWNERS : les fichiers de production exigent l'approbation du profil opérateur (offres Premium et Ultimate)
- ArgoCD : la synchronisation de l'AppProject de production est réservée au profil opérateur
- OpenBao : seuls les opérateurs accèdent à l'instance de production
- Kubernetes : aucun profil humain n'écrit directement dans le cluster, tout passe par Git
Le compte d'urgence (break-glass)¶
Le SSO, ArgoCD ou le fournisseur d'identité peuvent eux-mêmes tomber : il faut une issue de secours
- Conservation : hors ligne, chiffrée, accès à deux personnes
- Détection : toute utilisation déclenche une alerte immédiate
- Justification : chaque usage donne lieu à un compte rendu
- Rotation : identifiants renouvelés après chaque utilisation
- Sur k3s, c'est le kubeconfig administrateur, dont le certificat appartient au groupe
system:masters - Un compte d'urgence utilisé au quotidien n'est plus un compte d'urgence
SSO OIDC¶
OIDC en bref¶
- Fournisseur d'identité (IdP) : authentifie l'utilisateur et émet un ID token signé
- Client : l'application (ArgoCD, Grafana, OpenBao) enregistrée chez l'IdP avec une URL de retour
- Claims : les informations portées par le jeton (
sub,email, groupes) - Flux authorization code : l'utilisateur s'authentifie chez l'IdP, l'application échange un code contre les jetons
- L'application n'a jamais le mot de passe de l'utilisateur
- Les groupes du jeton deviennent la clé de l'autorisation dans chaque outil
GitLab comme fournisseur d'identité¶
- Une application de groupe par outil (Groupe → Paramètres → Applications), avec son URL de retour
- Les profils sont des sous-groupes :
<groupe>/dev,<groupe>/ops,<groupe>/sec,<groupe>/auditeurs - Le claim
groups_direct(groupes dont l'utilisateur est membre direct) est inclus dans l'ID token - Le claim
groups(avec héritage) n'est disponible que dansuserinfo - Conséquence : un outil qui ne lit que l'ID token ne voit que les appartenances directes
- Un départ de l'entreprise ou d'un sous-groupe se répercute à la session suivante
Dex, le courtier d'identité¶
- Dex se place entre les applications et un ou plusieurs fournisseurs (GitLab, LDAP, autre IdP)
- Il normalise les identités et expose les groupes sous un claim
groupshomogène - Le connecteur GitLab demande les scopes
read_useretopenid - Son option
groupsfiltre les groupes transmis et refuse les utilisateurs qui n'en font partie d'aucun - ArgoCD embarque Dex : son URL de retour est
<url argocd>/api/dex/callback - Un Dex autonome peut servir tous les outils, y compris
kubectl(en bonus)
ArgoCD : SSO GitLab par Dex¶
# values du chart argo-cd
configs:
cm:
url: http://argocd.securepay.local
admin.enabled: "false" # une fois le SSO validé
dex.config: |
connectors:
- type: gitlab
id: gitlab
name: GitLab
config:
baseURL: https://gitlab.com
clientID: <application id>
clientSecret: $argocd-dex-gitlab:clientSecret
groups:
- <groupe>/dev
- <groupe>/ops
- <groupe>/sec
- <groupe>/auditeurs
$argocd-dex-gitlab:clientSecretlit un Secret portant le labelapp.kubernetes.io/part-of: argocd, alimenté par External Secrets
ArgoCD : RBAC par groupe et par projet¶
configs:
rbac:
policy.default: "" # aucun droit par défaut
scopes: "[groups]"
policy.csv: |
p, role:dev, applications, get, */*, allow
p, role:dev, applications, sync, staging/*, allow
p, role:ops, applications, get, */*, allow
p, role:ops, applications, sync, */*, allow
p, role:sec, applications, get, */*, allow
p, role:auditeur, applications, get, */*, allow
g, <groupe>/dev, role:dev
g, <groupe>/ops, role:ops
g, <groupe>/sec, role:sec
g, <groupe>/auditeurs, role:auditeur
- Format :
p, sujet, ressource, action, projet/objet, effetpuisg, groupe, rôle exec(terminal web) reste désactivé : personne n'ouvre de shell en production depuis ArgoCD
Grafana : connexion GitLab¶
# values kube-prometheus-stack
grafana:
grafana.ini:
server:
root_url: http://grafana.securepay.local
auth.gitlab:
enabled: true
client_id: <application id>
client_secret: $__file{/etc/secrets/grafana-gitlab/client_secret}
scopes: openid email profile
allowed_groups: <groupe>/dev <groupe>/ops <groupe>/sec <groupe>/auditeurs
role_attribute_path: contains(groups[*], '<groupe>/ops') && 'Editor' || 'Viewer'
auth:
disable_login_form: true
- L'administrateur local de Grafana devient un compte d'urgence, son mot de passe rejoint OpenBao
OpenBao : groupes externes¶
bao auth enable oidc
bao write auth/oidc/config oidc_discovery_url="https://gitlab.com" \
oidc_client_id="<application id>" oidc_client_secret="<secret>" \
default_role="humains"
bao write auth/oidc/role/humains user_claim="sub" groups_claim="groups_direct" \
allowed_redirect_uris="http://localhost:8250/oidc/callback" token_ttl=1h
# Le groupe GitLab <groupe>/ops reçoit la politique securepay-ops
ACC=$(bao auth list -format=json | jq -r '."oidc/".accessor')
bao write identity/group name="ops" type="external" policies="securepay-ops"
GID=$(bao read -field=id identity/group/name/ops)
bao write identity/group-alias name="<groupe>/ops" mount_accessor="$ACC" canonical_id="$GID"
- Connexion :
bao login -method=oidc role=humains - Sur l'instance de production, seul le groupe
opsreçoit une politique
RBAC Kubernetes¶
Rappels¶
- Role et ClusterRole : un ensemble de droits (verbes sur des ressources)
- RoleBinding : accorde un rôle dans un namespace ; ClusterRoleBinding : dans tout le cluster
- Sujets :
User,GroupouServiceAccount - Rôles intégrés :
view(lecture sans les Secrets),edit(écriture, Secrets inclus),admin - Un RoleBinding peut référencer une ClusterRole : on réutilise
viewnamespace par namespace - Les droits sont additifs : il n'existe pas de règle « deny »
Lier les profils aux namespaces¶
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-view
namespace: securepay-staging
subjects:
- kind: Group
name: oidc:<groupe>/dev
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
- Le préfixe
oidc:est celui que l'API Server ajoutera aux groupes OIDC - Aucun binding pour
devdanssecurepay-prod: l'absence de droit est la règle
Les droits qui permettent d'escalader¶
createsurpods: monter n'importe quel Secret du namespace et utiliser n'importe quel ServiceAccountget/listsursecrets: lire tous les secrets, y compris des jetons de ServiceAccountpods/exec: ouvrir un shell dans un conteneur en cours d'exécution- Verbes
escalate,bindetimpersonate: s'attribuer ou emprunter des droits supplémentaires - Jokers
*sur les verbes ou les ressources - Ces droits sont à réserver aux contrôleurs qui en ont besoin, et à tracer
Les identités des workloads¶
- Un ServiceAccount dédié par workload, jamais le ServiceAccount
default automountServiceAccountToken: falsesauf si le workload appelle l'API Kubernetes- Les contrôleurs (ESO, remédiateur, ArgoCD) n'ont que les verbes et namespaces nécessaires
- Revue régulière :
kubectl auth can-i --list --as=system:serviceaccount:<ns>:<sa> -n <ns> - Des outils comme KubeHound cartographient les chemins d'attaque issus du RBAC
Accès kubectl humain par OIDC¶
- L'API Server valide lui-même les ID tokens : arguments
--oidc-*ou configuration d'authentification structurée (AuthenticationConfiguration) des versions récentes - Il ne lit que l'ID token : les groupes doivent y figurer, d'où l'intérêt de Dex
- Côté poste, le plugin kubelogin ouvre le navigateur et renouvelle les jetons
- Le préfixe des groupes (
oidc:) évite toute collision avec des groupes internes commesystem:masters - Une fois en place, plus aucun kubeconfig administrateur ne circule hors compte d'urgence
Audit & revues d'accès¶
La politique d'audit de l'API Kubernetes¶
| Niveau | Ce qui est journalisé |
|---|---|
None | rien |
Metadata | utilisateur, verbe, ressource, horodatage, réponse |
Request | métadonnées + corps de la requête |
RequestResponse | métadonnées + corps de la requête et de la réponse |
- Les Secrets sont journalisés au niveau
Metadata: on trace l'accès sans copier la valeur - La première règle qui correspond s'applique : l'ordre des règles compte
Exemple de politique¶
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages: ["RequestReceived"]
rules:
- level: None
nonResourceURLs: ["/healthz*", "/readyz*", "/livez*", "/version"]
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps", "serviceaccounts/token",
"pods/exec", "pods/attach", "pods/portforward"]
- level: RequestResponse
verbs: ["create", "update", "patch", "delete"]
resources:
- group: "rbac.authorization.k8s.io"
- level: Metadata
verbs: ["create", "update", "patch", "delete"]
- level: None
- Un
kubectl execpasse par WebSocket et apparaît avec le verbeget: d'où la règle explicite surpods/exec - Les modifications RBAC sont tracées avec leur contenu, les lectures courantes ignorées
Activer l'audit sur k3s¶
k3d cluster create securepay --servers 1 --agents 2 \
--volume "$PWD/audit-policy.yaml:/var/lib/rancher/k3s/server/audit-policy.yaml@server:0" \
--k3s-arg "--kube-apiserver-arg=audit-policy-file=/var/lib/rancher/k3s/server/audit-policy.yaml@server:0" \
--k3s-arg "--kube-apiserver-arg=audit-log-path=/var/lib/rancher/k3s/server/logs/audit.log@server:0" \
--k3s-arg "--kube-apiserver-arg=audit-log-maxage=7@server:0"
- Le journal est écrit sur le nœud server, au format JSON, un événement par ligne
- Un collecteur (Grafana Alloy, Vector) déployé sur ce nœud l'envoie dans Loki
- Les journaux d'audit d'OpenBao et d'ArgoCD rejoignent la même stack
Exploiter les journaux¶
# Shell ouvert dans un pod de production
{job="kube-audit"} | json | objectRef_subresource="exec" | objectRef_namespace="securepay-prod"
# Utilisation du compte d'urgence
{job="kube-audit"} | json | user_username="system:admin"
# Lecture de secrets par un humain
{job="kube-audit"} | json | objectRef_resource="secrets" | user_username!~"system:serviceaccount:.*"
- Chaque requête alimente une règle d'alerte du ruler Loki ou un panneau de tableau de bord
- Les mêmes questions doivent trouver une réponse en moins d'une minute pendant un incident
Tester les droits automatiquement¶
# L'API répond sans emprunter d'identité : SubjectAccessReview
kubectl create --validate=false -o jsonpath='{.status.allowed}' -f - <<EOF
apiVersion: authorization.k8s.io/v1
kind: SubjectAccessReview
spec:
user: alice
groups: ["oidc:<groupe>/dev"]
resourceAttributes:
namespace: securepay-prod
verb: get
resource: pods
EOF
# Attendu : false
- Un script parcourt la matrice des profils et échoue au premier droit inattendu
- Exécuté en CI avec un ServiceAccount qui n'a que le droit de créer des
subjectaccessreviews --validate=falseévite quekubectltente de lister les CRD, droit que ce ServiceAccount n'a pas
Anti-patterns à proscrire¶
- Comptes partagés (
admin,ops) dans ArgoCD, Grafana ou OpenBao - Profils humains avec
editoucluster-admin« pour dépanner » - Droits attribués à des individus plutôt qu'à des groupes
- Compte d'urgence utilisé au quotidien ou jamais renouvelé
- Journaux d'audit au niveau
RequestResponsesur les Secrets - Revue des accès jamais faite, départs non répercutés
À retenir¶
- Les profils sont des groupes du fournisseur d'identité, et chaque outil s'y branche par OIDC
- Écrire, approuver et déployer en production sont trois droits distincts
- RBAC au moindre privilège :
viewpar namespace, aucun droit d'écriture humain en production, attention aux verbes qui permettent d'escalader - Le compte d'urgence existe, mais il est conservé hors ligne, alerté et renouvelé
- L'audit trace l'accès aux secrets sans en copier la valeur, et les droits se testent en CI