Sécurité & détectionintermédiaire5 min de lecture

Inventorier les droits sudo et les authorized_keys sur toute la flotte avec un seul script

Qui peut devenir root sur quel serveur, et avec quelle clé SSH ? Sur un hôte, ce sont cinq commandes ; sur trente hôtes, c'est la question à laquelle plus personne ne répond. Ce script produit par hôte un tsv avec les comptes, les règles sudo et chaque clé publique autorisée (avec empreinte et commentaire), plus le contrôle diff qui signale dès qu'une clé ou une règle sudo est ajoutée.

Sommaire
  1. Étape 1 : ce qu'il y a sur un hôte
  2. Étape 2 : un script, un tsv par hôte
  3. Étape 3 : collecter sur toute la flotte et pivoter par personne
  4. Étape 4 : diff quotidien — signaler ce qui a été ajouté
  5. Étape 5 : nettoyer sans exclure personne
  6. Pièges
  7. Ce qu'il vous manque encore
  8. Comment monsys fait
  9. FAQ

La question arrive toujours au pire moment : un collègue part, un portable est volé, un auditeur demande « qui a root sur le serveur de base de données ? » — et la réponse est une tournée de ssh sur tous les hôtes pendant que tout le monde attend. Le problème n'est pas que l'information manque ; elle est dans /etc/passwd, /etc/sudoers et trente fichiers authorized_keys. Le problème est que personne ne l'a en un seul endroit, datée, sous une forme comparable à celle d'hier.

Étape 1 : ce qu'il y a sur un hôte

# Comptes interactifs (uid ≥ 1000, vrai shell) + root
awk -F: '($3>=1000 || $1=="root") && $7!~/nologin|false/ {print $1, $3, $6, $7}' /etc/passwd

# Qui peut sudo ? Via groupe...
getent group sudo admin wheel 2>/dev/null
# ...et via règles explicites (sans commentaires, Defaults ni directives @include)
sudo grep -rhvE '^\s*(#|@|Defaults|$)' /etc/sudoers /etc/sudoers.d/ 2>/dev/null

# Quelles clés sont autorisées, où, et à qui ?
for d in /root /home/*; do
  f="$d/.ssh/authorized_keys"; [ -f "$f" ] || continue
  echo "== $f"; sudo ssh-keygen -lf "$f" 2>/dev/null       # bits, empreinte, commentaire, type
done

Deux choses que ssh-keygen -lf vous montre immédiatement : des clés RSA 1024 bits ou de type ssh-dss (toutes deux bannies des défauts OpenSSH depuis des années, mais encore étonnamment présentes), et des clés sans commentaire — dont personne ne connaît le propriétaire.

N'oubliez pas les emplacements hors authorized_keys :

# Emplacements alternatifs dans sshd_config
sshd -T | grep -iE '^(authorizedkeysfile|authorizedkeyscommand|trustedusercakeys) '
# Certificats SSH (CA) — l'autorisation est alors à la CA, pas dans un fichier
# Comptes système avec un shell ET une clé (utilisateurs deploy, backup)
awk -F: '$3<1000 && $7!~/nologin|false/ {print $1}' /etc/passwd

Étape 2 : un script, un tsv par hôte

Le script produit trois types de lignes — user, sudo, key — sous une forme que vous pouvez trier, differ et coller dans un tableur.

sudo tee /usr/local/sbin/access-inventory.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
# Sortie : host<TAB>type<TAB>subject<TAB>detail<TAB>extra
H=$(hostname -s)

# Comptes avec un shell (y compris comptes système avec shell — les utilisateurs deploy sont aussi un accès)
awk -F: -v h="$H" '$7!~/nologin|false|sync|halt|shutdown/ {
  printf "%s\tuser\t%s\tuid=%s\tshell=%s\n", h, $1, $3, $7 }' /etc/passwd

# Dernière connexion SSH par compte, depuis le journal (lastlog et wtmp ne sont plus fiables/présents sur Ubuntu ≥ 24.04)
journalctl -u ssh --since "365 days ago" --no-pager -o short-iso 2>/dev/null \
  | grep -E 'Accepted (publickey|password)' | awk '{print $7, $1}' | sort -k1,1 -k2,2r | awk '!seen[$1]++' \
  | awk -v h="$H" '{printf "%s\tlastlogin\t%s\t%s\t\n", h, $1, $2}'

# Sudo via appartenance à un groupe
for g in sudo admin wheel; do
  getent group "$g" 2>/dev/null | awk -F: -v h="$H" -v g="$g" '$4!="" {n=split($4,u,","); for(i=1;i<=n;i++) printf "%s\tsudo\t%s\tgroup=%s\t\n", h, u[i], g}'
done
# Sudo via règles explicites (sans commentaires, Defaults ni directives @include)
grep -rhvE '^\s*(#|@|Defaults|$)' /etc/sudoers /etc/sudoers.d/ 2>/dev/null \
  | awk -v h="$H" '{ subj=$1; $1=""; sub(/^ /,""); printf "%s\tsudo\t%s\trule=%s\t\n", h, subj, $0 }'

# Clés : une ligne par clé autorisée, avec empreinte, type et commentaire
for d in /root /home/*; do
  f="$d/.ssh/authorized_keys"; [ -f "$f" ] || continue
  u=$(basename "$d"); [ "$d" = /root ] && u=root
  ssh-keygen -lf "$f" 2>/dev/null | awk -v h="$H" -v u="$u" '{ fp=$2; type=$NF; gsub(/[()]/,"",type); $1=""; $2=""; $NF=""; sub(/^ +/,""); sub(/ +$/,""); printf "%s\tkey\t%s\t%s\t%s %s\n", h, u, fp, type, $0 }'
done
EOF
sudo chmod 0755 /usr/local/sbin/access-inventory.sh
sudo /usr/local/sbin/access-inventory.sh | column -t -s $'\t' | head -30
# web-01  user       jeroen   uid=1001                         shell=/bin/bash
# web-01  sudo       jeroen   group=sudo
# web-01  sudo       deploy   rule=ALL=(root) NOPASSWD: /usr/bin/systemctl restart webapp
# web-01  key        jeroen   SHA256:7Kq…                      ED25519 jeroen@laptop-2024
# web-01  key        deploy   SHA256:aa1…                      RSA ci-runner
# web-01  key        deploy   SHA256:9f3…                      RSA (sans commentaire)   ← à qui ?

Étape 3 : collecter sur toute la flotte et pivoter par personne

Depuis un hôte d'administration, sur un hosts.txt :

D=/srv/inventory/access; mkdir -p "$D"
while read -r h; do
  ssh -o ConnectTimeout=5 -o BatchMode=yes "$h" sudo /usr/local/sbin/access-inventory.sh 2>/dev/null \
    || echo "$h	error	-	unreachable	" 
done < hosts.txt > "$D/$(date +%F).tsv"

# Retournez la question : par CLÉ, sur quels hôtes et sous quel compte se trouve-t-elle ?
awk -F'\t' '$2=="key" {print $4, $5, "→", $1":"$3}' "$D/$(date +%F).tsv" | sort | uniq
# SHA256:7Kq… ED25519 jeroen@laptop-2024 → db-01:jeroen
# SHA256:7Kq… ED25519 jeroen@laptop-2024 → web-01:jeroen
# SHA256:9f3… RSA (sans commentaire)     → web-01:deploy   ← une clé, pas de propriétaire : nettoyer ou étiqueter
# SHA256:aa1… RSA ci-runner              → web-01:deploy, web-02:deploy, db-01:root   ← clé CI sur le root de la base ?

# Par personne : sur quels hôtes a-t-elle sudo ?
awk -F'\t' '$2=="sudo" {print $3, "→", $1, "("$4")"}' "$D/$(date +%F).tsv" | sort

Cette liste inversée — par clé, où se trouve-t-elle — est là où sont les surprises : la clé CI sur le compte root de la base de données, la clé d'un ex-collègue sous un compte deploy partagé, et la clé sans commentaire que personne n'ose supprimer.

Étape 4 : diff quotidien — signaler ce qui a été ajouté

Une nouvelle clé ou règle sudo est toujours soit un changement délibéré (il y a alors un ticket en face) soit un incident. Dans les deux cas, vous voulez la voir le jour même.

sudo tee /usr/local/sbin/access-diff.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
NTFY="https://ntfy.example.be/security"
D=/srv/inventory/access
today=$D/$(date +%F).tsv
prev=$(ls -1 "$D"/*.tsv 2>/dev/null | grep -v "$today" | tail -1)
[ -f "$prev" ] || exit 0
# Ne comparer que clés, règles sudo et utilisateurs (lastlogin change chaque jour)
added=$(comm -13 <(grep -E $'\t(key|sudo|user)\t' "$prev" | sort) <(grep -E $'\t(key|sudo|user)\t' "$today" | sort))
removed=$(comm -23 <(grep -E $'\t(key|sudo|user)\t' "$prev" | sort) <(grep -E $'\t(key|sudo|user)\t' "$today" | sort))
if [ -n "$added$removed" ]; then
  { [ -n "$added" ] && { echo "ADDED:"; echo "$added"; }; [ -n "$removed" ] && { echo "REMOVED:"; echo "$removed"; }; } \
  | curl -s -H "Title: access change (fleet)" -H "Priority: high" --data-binary @- "$NTFY" >/dev/null
fi
EOF
sudo chmod 0755 /usr/local/sbin/access-diff.sh
echo '15 6 * * * root /usr/local/sbin/access-collect.sh && /usr/local/sbin/access-diff.sh' | sudo tee /etc/cron.d/access-inventory

(Mettez la boucle while read de l'étape 3 dans /usr/local/sbin/access-collect.sh.) Résultat : chaque matin un message si quelque chose a changé, sinon le silence. Une ligne ADDED … key … root … sans ticket est un incident, pas une tâche à faire.

Étape 5 : nettoyer sans exclure personne

La liste est une chose ; retirer des clés est la partie délicate. Procédez dans cet ordre :

  1. Ajouter un commentaire à chaque clé dont vous connaissez le propriétaire (ssh-keygen ne le peut pas ; éditez le fichier : le troisième champ est libre).
  2. Clés sans propriétaire : journaliser 30 jours d'abord : LogLevel VERBOSE dans sshd_config journalise l'empreinte à chaque connexion (Accepted publickey for deploy … SHA256:9f3…). Non utilisée : supprimer.
  3. Comptes partagés (deploy avec huit clés) : convertir en comptes personnels + règle sudo, ou en certificats SSH à courte durée. Chaque connexion est alors traçable à une personne.
  4. RSA 1024 bits et DSA : remplacer par ed25519 — OpenSSH 9+ ne les accepte plus par défaut, donc elles sont déjà cassées sur les hôtes les plus récents.

Pièges

  • AuthorizedKeysCommand. Si sshd récupère les clés depuis un IdP ou une source centrale, authorized_keys est vide et votre inventaire aussi. sshd -T vous le dit (étape 1) ; inventoriez alors à la source.
  • Clés dans ~/.ssh/authorized_keys2. Obsolète mais encore lu sur d'anciens hôtes si c'est dans AuthorizedKeysFile. Même vérification.
  • Sudo via %group depuis LDAP/SSSD. getent group sudo ne montre ces membres que si NSS les résout ; sur les hôtes avec SSSD, cela peut être lent ou vide pendant une panne. Exécution sans cache dans la fenêtre de maintenance.
  • Règles NOPASSWD sur des commandes avec arguments. NOPASSWD: /usr/bin/systemctl (sans arguments) signifie n'importe quelle commande systemctl, y compris systemctl --now disable auditd. Limitez à la commande exacte avec arguments.
  • Oublier sudo -l par utilisateur. Les règles sudoers disent ce qui est permis ; sudo -l -U jeroen dit ce qui est effectivement permis après tous les groupes et alias. Pour la revue, c'est la meilleure source.

Ce qu'il vous manque encore

  • Le lien avec le personnel. Le tsv dit « jeroen a sudo sur 12 hôtes », pas si Jeroen travaille encore ici. C'est la revue trimestrielle, avec la liste RH à côté.
  • L'historique par clé. Quand cette clé est-elle apparue, qui l'a ajoutée (quelle session sudo), et quand a-t-elle été utilisée pour la dernière fois ? Trois questions, trois sources.
  • La preuve. Un auditeur veut l'inventaire signé et immuable, et la preuve que l'alerte diff fonctionne (un journal de test).

Comment monsys fait

L'agent monsys inventorie par hôte les comptes, les droits sudo (effectifs, après groupes et alias), les authorized_keys par utilisateur avec empreinte et commentaire, et la dernière connexion — dans le cadre de l'inventaire habituel. Le hub l'affiche à l'échelle de la flotte par personne et par clé, signale une nouvelle clé ou règle sudo comme détection de dérive, et génère chaque trimestre un rapport de revue d'accès signé (ISO 27001 A.5.18) avec tous les comptes, leurs droits et leur dernière activité — y compris la configuration SSO sans secrets.

FAQ

Comment savoir à qui appartient une clé SSH sans commentaire ?

Mettez LogLevel VERBOSE dans sshd_config ; dès lors chaque connexion journalise l'empreinte. Après quelques semaines, vous savez quelles clés sont utilisées et depuis quelle IP. Les clés jamais utilisées, vous pouvez les supprimer ; les clés utilisées, vous les vérifiez auprès de l'utilisateur de cette IP.

Quelle est la différence entre le groupe sudo et une règle sudoers ?

L'appartenance à sudo (Ubuntu) ou wheel (RHEL) donne ALL=(ALL:ALL) ALL — tout. Une règle explicite dans /etc/sudoers.d/ peut être limitée à une seule commande. Les deux comptent pour la revue ; sudo -l -U <user> montre le résultat combiné.

À quelle fréquence dois-je lancer cela ?

L'inventaire chaque jour (automatiquement, avec l'alerte diff), la revue humaine chaque trimestre. NIS2 et ISO 27001 ne demandent pas une fréquence en jours, mais la démonstration que les accès sont revus périodiquement — et un trimestre est ce que les auditeurs attendent.

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.