NIS2, ISO 27001 & preuvesintermédiaire6 min de lecture

Une revue d'accès trimestrielle en une heure : comptes, sudo, clés SSH et SSO face à la liste du personnel — avec script

ISO 27001 A.5.18 et NIS2 art. 21 §2 (i) exigent de vérifier périodiquement qui a accès et si c'est toujours justifié. En pratique, cela devient un après-midi de tableurs, ou cela n'arrive pas. Ce script fait de la revue une heure : il pose l'inventaire technique (serveurs, sudo, clés, comptes IdP) à côté de la liste RH, marque chaque écart, et produit le rapport signé qu'un auditeur accepte.

Sommaire
  1. Ce qu'une revue d'accès doit produire
  2. Étape 1 : rassembler les sources
  3. Étape 2 : une identité par personne
  4. Étape 3 : le script — comparer et marquer les écarts
  5. Étape 4 : le jugement (l'heure)
  6. Étape 5 : exécuter, prouver, signer
  7. Pièges
  8. Ce qu'il vous manque encore
  9. Comment monsys fait
  10. FAQ

La revue d'accès est la pièce de preuve qui manque dans presque tous les audits, et pas parce que personne ne la juge importante. Elle manque parce que la question « qui a accès à quoi, et est-ce justifié ? » touche cinq sources — les serveurs, l'IdP, le VPN, la console cloud, les RH — et que personne ne les met côte à côte en une journée. Cet article automatise la collecte et la comparaison, pour que l'humain n'ait plus qu'à rendre le jugement. Ce jugement est la seule chose qui ne se scripte pas ; c'est aussi la seule chose que l'auditeur veut vraiment voir.

Ce qu'une revue d'accès doit produire

Pour un auditeur (ISO 27001 A.5.18, NIS2 art. 21 §2 (i), CyFun Important ID.AM/PR.AC), c'est le résultat qui compte, pas la méthode. Le résultat est un document avec :

  1. Date et périmètre — quels systèmes, quelle période.
  2. La liste — par personne : quels comptes, quels droits, dernier usage.
  3. Les écarts — comptes sans personne, personnes sans les comptes qu'elles devraient avoir, droits qui ne correspondent pas à la fonction, clés sans propriétaire, comptes inutilisés > 90 jours.
  4. La décision par écart — conserver (avec raison), supprimer (avec date), ou investiguer (avec responsable).
  5. Signature du réviseur, et preuve que les suppressions ont été exécutées.

Tout sauf le point 4 se scripte.

Étape 1 : rassembler les sources

Serveurs. Utilisez l'inventaire de sudo et authorized_keys sur toute la flotte ; il produit par hôte des lignes user, sudo, key et lastlogin. Pour cette revue, une seule exécution suffit :

R=/srv/access-review/$(date +%Y-Q$(( ($(date +%-m)-1)/3+1 ))); mkdir -p "$R"
while read -r h; do ssh -o BatchMode=yes -o ConnectTimeout=5 "$h" sudo /usr/local/sbin/access-inventory.sh; done < /srv/inventory/hosts.txt > "$R/servers.tsv"

IdP (SSO). Chaque IdP a un export. Trois exemples ; le résultat est toujours email,nom,rôle,mfa,dernière_connexion :

# Keycloak (admin-cli)
kcadm.sh config credentials --server https://sso.example.be --realm master --user admin
kcadm.sh get users -r example --fields username,email,enabled,attributes -q max=1000 | jq -r '.[] | [.email, .username, (.enabled|tostring)] | @csv' > "$R/idp.csv"
# Authentik
curl -s -H "Authorization: Bearer $AK_TOKEN" 'https://sso.example.be/api/v3/core/users/?page_size=1000' \
  | jq -r '.results[] | [.email, .name, (.is_active|tostring), (.last_login // "never")] | @csv' > "$R/idp.csv"
# Microsoft Entra ID (Graph, avec une inscription d'application ayant User.Read.All)
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" 'https://graph.microsoft.com/v1.0/users?$select=mail,displayName,accountEnabled,signInActivity&$top=999' \
  | jq -r '.value[] | [.mail, .displayName, (.accountEnabled|tostring), (.signInActivity.lastSignInDateTime // "never")] | @csv' > "$R/idp.csv"

Cloud et le reste. aws iam list-users, az ad user list, le serveur VPN (wg show peers, ou l'index CA d'OpenVPN), la base de données (\du dans PostgreSQL). Chacun en csv dans $R/. Pour la première revue, prenez les trois plus importants ; le reste suivra.

RH. Un csv avec email,nom,fonction,en_poste_depuis,sortie. C'est le seul fichier que vous ne pouvez pas générer ; demandez-le aux RH avec une échéance trimestrielle fixe. Sans liste RH, une revue d'accès est un inventaire, pas une revue.

Étape 2 : une identité par personne

Les comptes serveur s'appellent jpeeters, l'IdP dit jan.peeters@example.be, les RH disent « Peeters, Jan ». Un fichier de correspondance est inévitable — créez-le une fois, tenez-le à jour :

# /srv/access-review/identities.csv — email,server_user,idp_user,cloud_user
jan.peeters@example.be,jpeeters,jan.peeters,jan.peeters
deploy@example.be,deploy,,                     # compte de service : le propriétaire est dans owners.csv
ci@example.be,ci-runner,,ci-runner

Les comptes de service reçoivent un propriétaire dans owners.csv (account,owner_email,purpose,expiry). Un compte de service sans propriétaire est par définition un écart.

Étape 3 : le script — comparer et marquer les écarts

sudo tee /usr/local/sbin/access-review.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
# access-review.sh <répertoire de revue>  — attend servers.tsv, idp.csv, hr.csv, identities.csv, owners.csv
set -euo pipefail
R=$1; OUT=$R/findings.tsv; : > "$OUT"
f() { printf '%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" >> "$OUT"; }   # type, sujet, détail, action suggérée

# Tables auxiliaires
awk -F, 'NR>1 {print $2}' "$R/identities.csv" | grep -v '^$' | sort -u > /tmp/known-server-users
awk -F, 'NR>1 && $5=="" {print tolower($1)}' "$R/hr.csv" | sort -u > /tmp/hr-active
awk -F, 'NR>1 && $5!="" {print tolower($1)"\t"$5}' "$R/hr.csv" | sort -u > /tmp/hr-left
awk -F, 'NR>1 {print $1}' "$R/owners.csv" | sort -u > /tmp/owned-svc

# 1. Comptes serveur liés à personne (pas d'identité, pas de propriétaire de service)
awk -F'\t' '$2=="user" {print $3}' "$R/servers.tsv" | sort -u | while read -r u; do
  grep -qx "$u" /tmp/known-server-users || grep -qx "$u" /tmp/owned-svc || f orphan-account "$u" "sur $(awk -F'\t' -v u="$u" '$2=="user" && $3==u {print $1}' "$R/servers.tsv" | sort -u | paste -sd, -)" "lier à une personne/un propriétaire ou supprimer"
done

# 2. Personnes parties avec accès encore actif
while IFS=$'\t' read -r mail left; do
  su=$(awk -F, -v m="$mail" 'tolower($1)==m {print $2}' "$R/identities.csv")
  [ -n "$su" ] && grep -qP "\tuser\t$su\t" "$R/servers.tsv" && f left-but-active "$mail" "compte serveur $su, sortie $left" "SUPPRIMER aujourd'hui"
  grep -qi "^\"\?$mail" "$R/idp.csv" && grep -qi "$mail.*true" "$R/idp.csv" && f left-but-active "$mail" "compte IdP actif, sortie $left" "DÉSACTIVER aujourd'hui"
done < /tmp/hr-left

# 3. Sudo sur des hôtes où la fonction ne l'exige pas
awk -F'\t' '$2=="sudo" {print $3"\t"$1}' "$R/servers.tsv" | sort -u | while IFS=$'\t' read -r u h; do
  mail=$(awk -F, -v u="$u" '$2==u {print $1}' "$R/identities.csv")
  role=$(awk -F, -v m="$mail" 'tolower($1)==m {print $3}' "$R/hr.csv")
  case "$role" in *sysadmin*|*devops*|*ops*|"") ;; *) f sudo-outside-role "$u" "sudo sur $h, fonction : $role" "confirmer ou limiter à des commandes" ;; esac
done

# 4. Clés sans commentaire ou de type interdit
awk -F'\t' '$2=="key" && ($5 ~ /^(RSA 1024|DSA)/ || $5 ~ /no comment|sans commentaire|^[A-Z0-9]+ *$/) {print $3"\t"$1"\t"$5}' "$R/servers.tsv" | sort -u \
  | while IFS=$'\t' read -r u h k; do f weak-or-unowned-key "$u@$h" "$k" "remplacer par ed25519 avec commentaire, ou supprimer"; done

# 5. Inutilisé > 90 jours (serveur : lastlogin du journal ; IdP : dernière connexion)
cut90=$(date -d '-90 days' +%F)
awk -F'\t' '$2=="user" {print $3}' "$R/servers.tsv" | sort -u | while read -r u; do
  last=$(awk -F'\t' -v u="$u" '$2=="lastlogin" && $3==u {print $4}' "$R/servers.tsv" | sort | tail -1)
  [ -z "$last" ] && f unused-90d "$u" "aucune connexion SSH dans l'historique du journal" "supprimer ou documenter pourquoi nécessaire" && continue
  [[ "${last:0:10}" < "$cut90" ]] && f unused-90d "$u" "dernière connexion ${last:0:10}" "supprimer ou documenter"
done

# 6. Comptes IdP sans MFA (colonne selon votre export ; ici : 5e colonne = mfa)
awk -F, 'NR>1 && tolower($5)=="false" {print $1}' "$R/idp.csv" 2>/dev/null | while read -r m; do f no-mfa "$m" "compte IdP sans MFA" "imposer la MFA"; done

sort -u "$OUT" -o "$OUT"
echo "$(wc -l < "$OUT") findings → $OUT"
column -t -s $'\t' "$OUT" | head -40
EOF
sudo chmod 0755 /usr/local/sbin/access-review.sh
sudo /usr/local/sbin/access-review.sh "$R"

La sortie est un tsv à quatre colonnes : type, sujet, détail, action suggérée. C'est la liste que le réviseur parcourt — et le plus souvent, elle fait entre cinq et trente lignes, pas des centaines.

Étape 4 : le jugement (l'heure)

Ouvrez findings.tsv dans un tableur ou un éditeur et remplissez par ligne une cinquième colonne : keep:<raison>, remove:<date>, ou investigate:<responsable>. Règles :

  • left-but-active n'est jamais keep. Supprimer, aujourd'hui, et journaliser la suppression (étape 5).
  • orphan-account reçoit un propriétaire ou disparaît. « Personne ne sait » vaut remove.
  • sudo-outside-role avec keep exige une raison en une phrase (« rotation d'astreinte T4 »). Il revient le trimestre suivant.
  • unused-90d avec keep exige une date d'utilisation prévue. Sinon remove ; un compte se recrée en dix secondes.
  • no-mfa est remove pour l'accès, pas pour le compte : forcez la MFA à la prochaine connexion.

Enregistrez-le sous findings-reviewed.tsv. Ce fichier est la revue.

Étape 5 : exécuter, prouver, signer

# Exécuter et journaliser les suppressions — par ligne avec remove :
awk -F'\t' '$5 ~ /^remove/' "$R/findings-reviewed.tsv" | while IFS=$'\t' read -r type subj detail action decision; do
  case $type in
    orphan-account|unused-90d|left-but-active)
      u=${subj%%@*}
      for h in $(awk -F'\t' -v u="$u" '$2=="user" && $3==u {print $1}' "$R/servers.tsv" | sort -u); do
        ssh "$h" "sudo userdel -r '$u' 2>&1" && echo "$(date -Is) removed $u from $h ($type)" >> "$R/actions.log"
      done ;;
    weak-or-unowned-key) echo "$(date -Is) TODO manual: remove key $detail for $subj" >> "$R/actions.log" ;;
  esac
done

# Rapport : findings + décisions + actions, hachés et signés (voir le guide des preuves pour la clé)
( cd "$R" && sha256sum servers.tsv idp.csv hr.csv findings.tsv findings-reviewed.tsv actions.log > MANIFEST.sha256 )
ssh-keygen -Y sign -f /etc/evidence/key -n access-review "$R/MANIFEST.sha256"
echo "$(date -Is) access-review $(basename "$R") par $(whoami) : $(wc -l < "$R/findings.tsv") findings, $(grep -c remove "$R/findings-reviewed.tsv") supprimés" | sudo tee -a /srv/inventory/access-reviews.log

Le rapport trimestriel se compose maintenant de : l'inventaire (ce qu'il y avait), les findings (ce qui s'écartait), les décisions (ce que vous en avez pensé), les actions (ce que vous avez fait), un manifeste de hachages et une signature. C'est exactement ce qu'ISO 27001 A.5.18 entend par « review at planned intervals » et ce qu'un auditeur veut voir.

Pièges

  • La liste RH n'arrive pas. Sans hr.csv, vous ne pouvez pas calculer les écarts 2 et 3. Fixez l'échéance dans l'agenda des RH, pas celui de l'IT ; et faites échouer le script bruyamment si le fichier a plus de 30 jours.
  • Comptes partagés. deploy avec huit clés est un compte avec huit personnes derrière. Le script voit un propriétaire. Scindez-les (comptes personnels + règle sudo), sinon la revue est incomplète par définition.
  • Revue sans exécution. Une liste de trente remove dont 28 existent encore le trimestre suivant est pire qu'aucune revue : elle prouve que vous saviez. actions.log est là pour ça.
  • Serveurs uniquement. Le rôle de base de données superuser, la politique de bucket S3, le propriétaire de l'organisation GitHub : l'accès est partout. Commencez par trois sources, ajoutez-en une chaque trimestre, et écrivez dans le périmètre ce que vous ne couvrez pas (encore).
  • Vie privée. hr.csv contient des données personnelles ; le répertoire de revue aussi. Accès restreint, durée de conservation convenue (trois ans est courant), et pas de copier-coller dans un chat partagé.

Ce qu'il vous manque encore

  • En continu plutôt que par trimestre. Un compte créé le jour 2 après la revue reste hors de vue pendant 88 jours. Le diff quotidien du guide de flotte l'attrape pour les serveurs ; pour l'IdP, il faut un webhook ou un export quotidien.
  • Une identité pour tous les systèmes. identities.csv est manuel et vieillit. Avec trente employés, ça passe ; avec trois cents, non.
  • La preuve que le réviseur a lu. Une signature sur le manifeste prouve l'intégrité, pas l'attention. Certains auditeurs demandent un paraphe par finding ; une colonne decided_by avec date est la version pragmatique.

Comment monsys fait

monsys collecte le côté serveur en continu (comptes, sudo, clés, dernière connexion par hôte) et relie via l'intégration SSO les comptes IdP aux utilisateurs du tableau de bord. Le rapport de revue d'accès (ISO 27001 A.5.18) se génère automatiquement chaque trimestre : tous les comptes avec rôle, statut MFA et dernière activité, toutes les attributions de rôles par périmètre, la configuration SSO sans secrets, et toutes les mutations d'accès du journal d'audit sur la période — signé Ed25519, stable à l'octet, vérifiable hors ligne. L'évaluation reste humaine ; vous consignez les décisions dans le rapport, et les suppressions passant par monsys (jetons d'agent, utilisateurs du tableau de bord) figurent comme preuve dans le suivant.

FAQ

À quelle fréquence faire une revue d'accès ?

ISO 27001 dit « at planned intervals » ; l'interprétation courante et ce que les auditeurs attendent est chaque trimestre pour les accès administrateur et au moins chaque année pour les utilisateurs ordinaires. Plus un contrôle ciblé sous 24 heures à chaque départ.

Et si les RH ne peuvent pas fournir de liste ?

Vous commencez alors avec ce que vous avez (l'IdP comme meilleure approximation de « qui travaille ici ») et vous écrivez explicitement dans le périmètre que le lien RH manque. Un auditeur apprécie plus un périmètre honnête qu'une revue qui suggère l'exhaustivité.

Chaque finding doit-il avoir une décision ?

Oui. Un finding sans décision est un point ouvert que l'auditeur compte comme « non revu ». investigate:<responsable> est une décision valide, à condition que la revue suivante contienne un résultat.

Rédigé par l'équipe monsys — des sysadmins qui font cela tous les jours.

Fait à la main ? Laissez monsys s'en charger en continu.

Tout ce que contient ce guide tourne dans monsys comme contrôle permanent, avec historique, alertes et preuves d'audit. 5 serveurs gratuits, hébergés en Belgique, installés en 60 secondes.