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
securepaydans la VMsecurepay-prod, ArgoCD, kube-prometheus-stack avec Loki, environnementssecurepay-stagingetsecurepay-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
jqsur 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é¶
- 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 | ... |
- 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.
- Protégez
mainsursecurepay-platform: merge réservé au rôle Maintainer, que seuls les opérateurs détiennent. Si votre offre GitLab le permet, ajoutez un fichierCODEOWNERSexigeant l'approbation de<groupe>/opspour 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é¶
-
Dans le groupe GitLab (Paramètres → Applications), créez une application « ArgoCD » :
- URL de retour :
http://argocd.securepay.local/api/dex/callback - scopes :
read_useretopenid
- URL de retour :
-
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 labelapp.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.
- Ajoutez la configuration Dex et le RBAC dans
bootstrap/argocd-values.yaml, puis rejouezmake 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
-
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.
-
Une fois au moins un opérateur connecté en SSO, désactivez le compte local :
admin.enabled: "false"dansconfigs.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é¶
-
Créez une application GitLab « Grafana » (URL de retour
http://grafana.securepay.local/login/gitlab, scopesopenid,email,profile), et exposez son secret par External Secrets dans un Secretgrafana-gitlabdu namespacemonitoring. -
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
- 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é¶
-
Créez une application GitLab « OpenBao » avec l'URL de retour
http://localhost:8250/oidc/callback(utilisée parbao login) et le scopeopenid. -
Sur chaque instance, configurez l'authentification OIDC. Le rôle lit le claim
groups_directde 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
- 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"
-
Respectez la matrice : sur
openbao-prod, seul le groupeopsreçoit une politique ; suropenbao-staging,devetops. -
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é¶
- Versionnez dans
security/identity/rbac/unRoleBindingpar profil et par namespace, vers les rôles intégrésview(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éfixeoidc:) :
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
- Passez en revue les ServiceAccounts des workloads et des contrôleurs :
- 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"]
- É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"
- Exécutez-le en CI de
securepay-platform(runner self-hosted) avec un kubeconfig fondé sur le jeton du ServiceAccountrbac-tester.
Deux pièges observés en test
- Sans
--validate=false,kubectl createtente de lister les CRD pour valider le manifeste, droit querbac-testern'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é¶
- 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
- 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
-
Reportez les mêmes arguments dans la création du cluster (
bootstrap/) : une reconstruction doit produire un cluster audité. -
Collectez
audit.logdans Loki avec un collecteur déployé sur le nœud server (montagehostPathde/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]
}
- 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 execapparaît avec le verbeget(WebSocket) : c'est pourquoi la politique cible explicitementpods/execplutôt que le seul verbecreate - 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é¶
-
Sur k3s, le kubeconfig administrateur utilise un certificat de l'utilisateur
system:admin, membre desystem:masters. Faites-en le compte d'urgence : chiffré, stocké hors ligne, accessible à deux personnes, et retiré des postes. -
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
-
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. -
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
groupsetgroups_directdans les jetons GitLab, et quelle conséquence pour OpenBao ? - Pourquoi le rôle intégré
editne convient-il pas à un profil humain en production ? - Pourquoi journaliser les Secrets au niveau
Metadataet nonRequestResponse? - Comment prouver à un auditeur qu'un développeur n'a aucun accès à la production ?