Aller au contenu

Gouvernance des identités avec SSO GitLab, RBAC et audit

Objectifs pédagogiques

À la fin de ce TP, vous devez être capables de :

  • formaliser des profils d'accès et les porter par des groupes GitLab
  • brancher ArgoCD (par Dex), Grafana et OpenBao sur GitLab en SSO OIDC
  • traduire chaque profil en droits ArgoCD, OpenBao et Kubernetes, au moindre privilège
  • activer la politique d'audit de l'API Kubernetes sur k3s et exploiter les journaux dans Loki
  • encadrer un compte d'urgence (break-glass) et détecter son utilisation
  • tester automatiquement la matrice des droits en CI

Prérequis

  • La plateforme de lab : cluster k3d securepay dans la VM securepay-prod, ArgoCD, kube-prometheus-stack avec Loki, environnements securepay-staging et securepay-prod
  • Les deux instances OpenBao du TP précédent et External Secrets Operator
  • Un groupe GitLab dont vous êtes propriétaire, et des comptes GitLab de test (vos coéquipiers) pour jouer les profils
  • jq sur le poste

Lab uniquement

Les URL restent en HTTP pour simplifier. En dehors d'un lab, chaque URL de retour OIDC doit être en HTTPS.

Contexte

L'audit PSSI de SecurePay demande de prouver, pour chaque environnement, qui a accès à quoi et qui a fait quoi. Aujourd'hui, ArgoCD et Grafana utilisent un compte admin partagé, le kubeconfig administrateur circule dans l'équipe et aucune action sur le cluster n'est journalisée.

Atelier 1 : Profils et groupes GitLab

Travail demandé

  1. Rédigez la matrice des profils dans docs/identites.md :
Profil Groupe GitLab Staging Production Justification
Développeur <groupe>/dev lecture, synchronisation ArgoCD aucun ...
Opérateur <groupe>/ops lecture, synchronisation lecture, synchronisation, OpenBao prod ...
Sécurité <groupe>/sec lecture lecture hors secrets ...
Auditeur <groupe>/auditeurs lecture lecture des journaux ...
Break-glass aucun administrateur ...
  1. Créez les quatre sous-groupes et ajoutez chaque personne directement dans le sous-groupe de son profil.

Appartenance directe

Dans l'ID token émis par GitLab, le claim groups_direct ne contient que les groupes dont l'utilisateur est membre direct. Le claim groups (avec héritage) n'existe que dans la réponse userinfo. Un outil qui ne lit que l'ID token, comme OpenBao à l'atelier 4, ne verra pas une appartenance héritée.

  1. Protégez main sur securepay-platform : merge réservé au rôle Maintainer, que seuls les opérateurs détiennent. Si votre offre GitLab le permet, ajoutez un fichier CODEOWNERS exigeant l'approbation de <groupe>/ops pour la configuration de production, et interdisez l'approbation par l'auteur de la MR.

Critère de réussite

La matrice est versionnée, chaque profil existe dans GitLab, et un développeur ne peut pas fusionner une MR sur main de securepay-platform.

Atelier 2 : SSO ArgoCD par Dex

Travail demandé

  1. Dans le groupe GitLab (Paramètres → Applications), créez une application « ArgoCD » :

    • URL de retour : http://argocd.securepay.local/api/dex/callback
    • scopes : read_user et openid
  2. Rangez le secret de l'application dans OpenBao, puis exposez-le à ArgoCD par External Secrets. ArgoCD lit les secrets référencés sous la forme $<nom>:<clé> dans tout Secret portant le label app.kubernetes.io/part-of: argocd :

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: argocd-dex-gitlab
  namespace: argocd
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: openbao
    kind: SecretStore
  target:
    name: argocd-dex-gitlab
    template:
      metadata:
        labels:
          app.kubernetes.io/part-of: argocd
  data:
    - secretKey: clientSecret
      remoteRef:
        key: platform/argocd-dex-gitlab
        property: clientSecret

SecretStore de plateforme

Cet ExternalSecret suppose, dans le namespace argocd, un SecretStore vers openbao-prod avec un rôle OpenBao dédié : lié au ServiceAccount utilisé par ce SecretStore dans argocd, et limité par sa politique à secret/data/platform/argocd-*. Faites de même pour monitoring à l'atelier 3.

  1. Ajoutez la configuration Dex et le RBAC dans bootstrap/argocd-values.yaml, puis rejouez make bootstrap :
configs:
  cm:
    url: http://argocd.securepay.local
    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
  rbac:
    policy.default: ""
    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
  1. Testez chaque profil avec un compte différent : un développeur voit la production mais ne peut pas la synchroniser ; un compte extérieur aux quatre groupes est refusé par Dex.

  2. Une fois au moins un opérateur connecté en SSO, désactivez le compte local : admin.enabled: "false" dans configs.cm.

Critère de réussite

Plus aucune connexion par compte partagé à ArgoCD, et les droits observés correspondent à la matrice.

Atelier 3 : SSO Grafana

Travail demandé

  1. Créez une application GitLab « Grafana » (URL de retour http://grafana.securepay.local/login/gitlab, scopes openid, email, profile), et exposez son secret par External Secrets dans un Secret grafana-gitlab du namespace monitoring.

  2. Complétez les valeurs de kube-prometheus-stack :

grafana:
  extraSecretMounts:
    - name: grafana-gitlab
      secretName: grafana-gitlab
      mountPath: /etc/secrets/grafana-gitlab
      readOnly: true
  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
  1. Rangez le mot de passe de l'administrateur local de Grafana dans OpenBao production : il devient un compte d'urgence.

Critère de réussite

Les opérateurs sont éditeurs, les autres profils lecteurs, et le formulaire de connexion local n'est plus proposé.

Atelier 4 : OpenBao pour les humains

Travail demandé

  1. Créez une application GitLab « OpenBao » avec l'URL de retour http://localhost:8250/oidc/callback (utilisée par bao login) et le scope openid.

  2. Sur chaque instance, configurez l'authentification OIDC. Le rôle lit le claim groups_direct de l'ID token :

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
  1. Associez chaque groupe GitLab autorisé à un groupe externe OpenBao et à une politique :
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"
  1. Respectez la matrice : sur openbao-prod, seul le groupe ops reçoit une politique ; sur openbao-staging, dev et ops.

  2. Connectez-vous avec chaque profil (bao login -method=oidc role=humains) et vérifiez qu'un développeur obtient un jeton sans aucune politique utile sur la production.

Critère de réussite

Chaque lecture humaine d'un secret de production apparaît dans le journal d'audit avec l'identité GitLab de la personne.

Atelier 5 : RBAC Kubernetes et tests automatisés

Travail demandé

  1. Versionnez dans security/identity/rbac/ un RoleBinding par profil et par namespace, vers les rôles intégrés view (lecture sans les Secrets) ou des rôles maison. Les sujets sont les groupes tels que l'API Server les verra avec l'OIDC (préfixe oidc:) :
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
  1. Passez en revue les ServiceAccounts des workloads et des contrôleurs :
kubectl auth can-i --list --as=system:serviceaccount:securepay-prod:securepay-api -n securepay-prod
  1. Créez le ServiceAccount de test, dont le seul droit est de poser des questions à l'autorisateur :
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: rbac-tester
rules:
  - apiGroups: ["authorization.k8s.io"]
    resources: ["subjectaccessreviews"]
    verbs: ["create"]
  1. Écrivez security-tests/rbac-matrix.sh, qui parcourt la matrice et échoue au premier écart :
#!/usr/bin/env bash
set -u
fail=0

# check <groupe> <namespace> <verbe> <ressource> <attendu>
check() {
  local got
  got=$(kubectl create --validate=false -o jsonpath='{.status.allowed}' -f - <<EOF
apiVersion: authorization.k8s.io/v1
kind: SubjectAccessReview
spec:
  user: test-matrice
  groups: ["$1"]
  resourceAttributes: {namespace: "$2", verb: "$3", resource: "$4"}
EOF
)
  if [ "$got" = "$5" ]; then echo "OK    $1 $3 $4 dans $2 -> $got"
  else echo "ÉCART $1 $3 $4 dans $2 -> $got (attendu $5)"; fail=1; fi
}

check "oidc:<groupe>/dev" securepay-staging get pods true
check "oidc:<groupe>/dev" securepay-prod    get pods false
check "oidc:<groupe>/dev" securepay-staging get secrets false
check "oidc:<groupe>/ops" securepay-prod    get pods true
check "oidc:<groupe>/ops" securepay-prod    create pods false

exit "$fail"
  1. Exécutez-le en CI de securepay-platform (runner self-hosted) avec un kubeconfig fondé sur le jeton du ServiceAccount rbac-tester.

Deux pièges observés en test

  • Sans --validate=false, kubectl create tente de lister les CRD pour valider le manifeste, droit que rbac-tester n'a pas : la commande échoue avant même d'interroger l'autorisateur.
  • Juste après une modification RBAC, l'autorisateur peut répondre avec quelques secondes de retard (cache). Attendez avant de rejouer les tests.

Critère de réussite

La matrice des droits est testée à chaque pipeline, avec une identité qui ne peut rien lire ni modifier dans le cluster.

Atelier 6 : Audit de l'API Kubernetes

Travail demandé

  1. Rédigez security/identity/audit-policy.yaml :
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
  1. Activez-la sur le cluster existant, sans le recréer (depuis la VM) : k3s lit les fichiers complémentaires de /etc/rancher/k3s/config.yaml.d/.
docker cp security/identity/audit-policy.yaml \
  k3d-securepay-server-0:/var/lib/rancher/k3s/server/audit-policy.yaml
docker exec -i k3d-securepay-server-0 sh -c \
  'mkdir -p /etc/rancher/k3s/config.yaml.d && cat > /etc/rancher/k3s/config.yaml.d/audit.yaml' <<'EOF'
kube-apiserver-arg:
  - "audit-policy-file=/var/lib/rancher/k3s/server/audit-policy.yaml"
  - "audit-log-path=/var/lib/rancher/k3s/server/logs/audit.log"
  - "audit-log-maxage=7"
EOF
docker restart k3d-securepay-server-0
  1. Reportez les mêmes arguments dans la création du cluster (bootstrap/) : une reconstruction doit produire un cluster audité.

  2. Collectez audit.log dans Loki avec un collecteur déployé sur le nœud server (montage hostPath de /var/lib/rancher/k3s/server/logs). Exemple avec Grafana Alloy :

local.file_match "kube_audit" {
  path_targets = [{"__path__" = "/var/lib/rancher/k3s/server/logs/audit.log", "job" = "kube-audit"}]
}

loki.source.file "kube_audit" {
  targets    = local.file_match.kube_audit.targets
  forward_to = [loki.write.default.receiver]
}
  1. Ouvrez un shell dans un pod de production, lisez un Secret, modifiez un RoleBinding, puis retrouvez chaque action :
{job="kube-audit"} | json | objectRef_subresource="exec" | objectRef_namespace="securepay-prod"
{job="kube-audit"} | json | objectRef_resource="secrets" | user_username!~"system:serviceaccount:.*"
{job="kube-audit"} | json | objectRef_apiGroup="rbac.authorization.k8s.io"

Ce que vous devez observer

  • Un kubectl exec apparaît avec le verbe get (WebSocket) : c'est pourquoi la politique cible explicitement pods/exec plutôt que le seul verbe create
  • La lecture d'un Secret est tracée au niveau Metadata : la valeur n'apparaît jamais dans le journal
  • La modification d'un RoleBinding est journalisée avec son contenu

Critère de réussite

Les trois actions sont retrouvées dans Loki en moins d'une minute, avec l'identité de leur auteur.

Atelier 7 : Le compte d'urgence

Travail demandé

  1. Sur k3s, le kubeconfig administrateur utilise un certificat de l'utilisateur system:admin, membre de system:masters. Faites-en le compte d'urgence : chiffré, stocké hors ligne, accessible à deux personnes, et retiré des postes.

  2. Créez une alerte Loki qui se déclenche à chaque utilisation :

groups:
  - name: break-glass
    rules:
      - alert: BreakGlassUsed
        expr: sum(count_over_time({job="kube-audit"} | json | user_username="system:admin" [5m])) > 0
        labels:
          severity: critical
          component: kube-apiserver
          cause: BreakGlassAccess
        annotations:
          runbook_url: https://gitlab.com/<groupe>/securepay-platform/-/blob/main/docs/runbooks/break-glass.md
  1. Rédigez le runbook break-glass.md : conditions d'utilisation, deux personnes présentes, compte rendu obligatoire, régénération des identifiants après usage.

  2. Simulez une utilisation et vérifiez que l'alerte arrive.

Critère de réussite

Toute utilisation du compte d'urgence est alertée et documentée, et aucun kubeconfig administrateur ne traîne sur un poste.

Bonus : kubectl par OIDC

Déployez un Dex autonome (connecteur GitLab, client kubectl), configurez l'API Server pour valider ses jetons (préfixe de groupes oidc:), puis utilisez le plugin kubelogin. Les RoleBindings de l'atelier 5 s'appliquent alors aux personnes, sans autre modification.

Questions de synthèse

  • Pourquoi attribuer les droits à des groupes plutôt qu'à des personnes ?
  • Quelle différence entre groups et groups_direct dans les jetons GitLab, et quelle conséquence pour OpenBao ?
  • Pourquoi le rôle intégré edit ne convient-il pas à un profil humain en production ?
  • Pourquoi journaliser les Secrets au niveau Metadata et non RequestResponse ?
  • Comment prouver à un auditeur qu'un développeur n'a aucun accès à la production ?