Ce qu'un auditeur NIS2 demande vraiment : douze pièces de preuve et le script qui les extrait de vos serveurs
Pas de politique, pas d'intentions : un auditeur veut des artefacts datés. Voici les douze qui reviennent dans pratiquement chaque audit CyFun ou NIS2, avec pour chacune la commande qui la produit, et un evidence-pack.sh qui met tout dans une archive signée encore vérifiable six mois plus tard.
Sommaire
Un audit n'est pas un examen sur ce que vous savez, c'est une vérification de ce que vous pouvez montrer. L'auditeur a une liste de contrôles, et pour chaque contrôle il pose la même question : « qu'est-ce qui le prouve ? » La réponse « on le fait » vaut zéro. La réponse « voici la sortie du 14 août, voici celle du 14 septembre, et voici l'écart que nous avons corrigé à ce moment-là » vaut le maximum. Cet article est la liste des douze artefacts que vous voulez avoir prêts, plus le script qui les collecte.
Les douze pièces de preuve
Pour chacune : quel contrôle elle couvre (NIS2 art. 21 §2 et l'équivalent ISO 27001:2022), ce que l'auditeur veut voir, et la commande.
1. Inventaire des actifs daté
NIS2 (a), (i) · ISO A.5.9 Quoi : par hôte, l'OS, le noyau, les ports en écoute, les services actifs. Pas « une CMDB » mais une liste reproductible.
hostname -f; date -Is; . /etc/os-release; echo "$PRETTY_NAME"; uname -r
ss -tulnH | awk '{print $1, $5}' | sort -u
systemctl list-units --type=service --state=running --no-legend | awk '{print $1}'
2. État des correctifs et délai
NIS2 (e) · ISO A.8.8 Quoi : quelles mises à jour sont en attente, quand la dernière a été installée, et combien de jours entre publication et installation.
apt list --upgradable 2>/dev/null | grep -v '^Listing'
grep -h 'status installed' /var/log/dpkg.log* | awk '{print $1, $5}' | sort | tail -50
[ -f /var/run/reboot-required ] && { echo REBOOT-REQUIRED; cat /var/run/reboot-required.pkgs; }
systemctl is-enabled unattended-upgrades apt-daily-upgrade.timer 2>/dev/null
3. Vulnérabilités ouvertes, priorisées
NIS2 (d), (e) · ISO A.8.8 Quoi : la liste des CVE de cet hôte, avec EPSS/KEV, et la décision par élément (corriger, atténuer, accepter). Voir le guide OSV ; le script qui s'y trouve produit le tsv. La preuve est le tsv plus une ligne par risque accepté :
CVE-2026-1234 libxml2 accepted-risk 2026-09-01 "non atteignable : lib utilisée uniquement par un batch hors ligne" approuvé : J. Peeters
4. Accès : comptes, sudo, clés SSH
NIS2 (i) · ISO A.5.15, A.5.18, A.8.2 Quoi : qui peut entrer, avec quels droits, et quand cela a été revu pour la dernière fois.
awk -F: '$3>=1000 && $7!~/nologin|false/{print $1}' /etc/passwd
getent group sudo admin wheel 2>/dev/null
grep -rhE '^[^#].*ALL' /etc/sudoers /etc/sudoers.d/ 2>/dev/null
for d in /root /home/*; do [ -f "$d/.ssh/authorized_keys" ] && { echo "== $d"; awk '{print $1, $NF}' "$d/.ssh/authorized_keys"; }; done
journalctl -u ssh --since "365 days ago" --no-pager -o short-iso | grep -E 'Accepted (publickey|password)' | awk '{print $7, $1}' | sort -k1,1 -k2,2r | awk '!seen[$1]++' # last SSH login per user
La revue trimestrielle est une ligne à part dans votre journal : 2026-09-15 access-review web-01: 4 comptes, 1 supprimé (ex-employé), 0 clé inconnue — J. Peeters.
5. MFA et politique d'authentification
NIS2 (j) · ISO A.5.17, A.8.5 Quoi : que la connexion par mot de passe est désactivée et que l'accès d'administration exige un second facteur.
sshd -T | grep -iE '^(passwordauthentication|permitrootlogin|authenticationmethods|kbdinteractiveauthentication) '
grep -l pam_google_authenticator /etc/pam.d/* 2>/dev/null
Pour un IdP (Entra, Keycloak, Authentik) : l'export de la politique MFA, daté.
6. Journalisation et rétention
NIS2 (b) · ISO A.8.15 Quoi : que les logs existent, combien de temps ils sont conservés, et que l'horloge est juste.
journalctl --disk-usage
grep -hE '^(Storage|SystemMaxUse|MaxRetentionSec)' /etc/systemd/journald.conf /etc/systemd/journald.conf.d/*.conf 2>/dev/null
journalctl --list-boots | head -3 # boot le plus ancien = jusqu'où vous pouvez remonter
timedatectl show -p NTPSynchronized -p Timezone
7. La détection est active, et testée
NIS2 (b) · ISO A.8.16 Quoi : quelles détections tournent (brute force, honeypots, intégrité) et quand elles ont été testées pour la dernière fois.
systemctl is-active fail2ban auditd 2>/dev/null
sudo fail2ban-client status sshd 2>/dev/null | grep -E 'Total (failed|banned)'
sudo auditctl -l 2>/dev/null | grep -c canary
Plus votre journal de test : 2026-09-01 canary-test web-01: alerte reçue après 3 min — OK. Une détection sans journal de test est, pour un auditeur, une détection qui fonctionne peut-être.
8. Sauvegarde : récente, vérifiable, restaurée
NIS2 (c) · ISO A.8.13 Quoi : dernière sauvegarde réussie, dernière vérification d'intégrité, dernier test de restauration.
restic -r "$REPO" snapshots --latest 1 --json 2>/dev/null | jq -r '.[0] | "\(.time) \(.hostname) \(.paths|join(","))"'
tail -5 /srv/inventory/restore-tests.log 2>/dev/null
Voir vérifier les sauvegardes pour le script de test de restauration.
9. Cryptographie : certificats et protocoles
NIS2 (h) · ISO A.8.24 Quoi : quels certificats tournent, quand ils expirent, et que les anciens protocoles sont désactivés.
for port in $(ss -tlnH | awk '{sub(/.*:/,"",$4); print $4}' | sort -un); do
cert=$(echo | timeout 3 openssl s_client -connect "localhost:$port" 2>/dev/null \
| openssl x509 -noout -subject -enddate 2>/dev/null | paste -sd' ')
[ -n "$cert" ] && echo "port $port: $cert"
done
echo | openssl s_client -connect localhost:443 -tls1_1 2>&1 | grep -q 'Cipher is (NONE)' && echo "TLS1.1: désactivé" || echo "TLS1.1: ACTIF"
10. Pare-feu et segmentation réseau
NIS2 (g) · ISO A.8.20, A.8.22 Quoi : refus par défaut en entrée, et la liste des ports ouverts correspondant à l'inventaire.
sudo ufw status verbose 2>/dev/null || sudo nft list ruleset 2>/dev/null | head -60
11. Intégrité de la configuration
NIS2 (e) · ISO A.8.9 Quoi : que la configuration critique ne change pas sans qu'on le remarque. La forme la plus simple : une liste de hachages que vous comparez périodiquement.
sudo find /etc/ssh /etc/sudoers /etc/sudoers.d /etc/pam.d /etc/ufw /etc/cron.d -type f -exec sha256sum {} + | sort -k2 > /srv/inventory/$(hostname -s)-config.sha256
# La prochaine fois : qu'est-ce qui a changé ?
sha256sum -c --quiet /srv/inventory/$(hostname -s)-config.sha256 2>&1 | grep -v ': OK$'
12. Registre des incidents et procédure de notification
NIS2 (b), art. 23 · ISO A.5.24–A.5.28 Quoi : la liste des incidents (y compris les petits, y compris les « fausses alertes »), avec heure de détection, évaluation, action et — s'il était significatif — l'heure de notification au CCB. Ce n'est pas une commande ; c'est un tableau que vous tenez à jour. Vide, c'est suspect ; un auditeur ne croit pas qu'il ne s'est rien passé en un an.
Le script : evidence-pack.sh
Tout ce qui précède dans une archive par hôte, avec un manifeste de hachages et une signature qui démontre que l'archive n'a pas été modifiée depuis. Nous signons avec ssh-keygen -Y (OpenSSH ≥ 8.0, donc présent partout) et une clé Ed25519 distincte réservée aux preuves.
# Une seule fois : une clé de preuve, pas votre clé SSH personnelle
sudo install -d -m 0700 /etc/evidence
sudo ssh-keygen -q -t ed25519 -N '' -C 'evidence@'"$(hostname -s)" -f /etc/evidence/key
# La clé publique, vous la donnez à l'auditeur (et la mettez dans git) :
cat /etc/evidence/key.pub
sudo tee /usr/local/sbin/evidence-pack.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
HOST=$(hostname -s); TS=$(date -u +%Y%m%dT%H%M%SZ)
OUT=/srv/evidence/$HOST-$TS; mkdir -p "$OUT"
run() { local name=$1; shift; { echo "# $name — $(date -Is) — $HOST"; "$@" 2>&1 || true; } > "$OUT/$name.txt"; }
run 01-assets bash -c 'hostname -f; . /etc/os-release; echo "$PRETTY_NAME"; uname -r; ss -tulnH | awk "{print \$1, \$5}" | sort -u; systemctl list-units --type=service --state=running --no-legend | awk "{print \$1}"'
run 02-patching bash -c 'apt list --upgradable 2>/dev/null | grep -v "^Listing"; grep -h "status installed" /var/log/dpkg.log* | awk "{print \$1, \$5}" | sort | tail -50; cat /var/run/reboot-required.pkgs 2>/dev/null; systemctl is-enabled unattended-upgrades apt-daily-upgrade.timer'
run 03-vulns bash -c 'ls -t /var/lib/cve-scan/*.tsv 2>/dev/null | head -1 | xargs -r cat'
run 04-access bash -c 'awk -F: "\$3>=1000 && \$7!~/nologin|false/{print \$1}" /etc/passwd; getent group sudo admin wheel; grep -rhE "^[^#].*ALL" /etc/sudoers /etc/sudoers.d/; for d in /root /home/*; do [ -f "$d/.ssh/authorized_keys" ] && { echo "== $d"; awk "{print \$1, \$NF}" "$d/.ssh/authorized_keys"; }; done; journalctl -u ssh --since "365 days ago" --no-pager -o short-iso | grep -E "Accepted (publickey|password)" | awk "{print \$7, \$1}" | sort -k1,1 -k2,2r | awk "!seen[\$1]++"'
run 05-auth bash -c 'sshd -T | grep -iE "^(passwordauthentication|permitrootlogin|authenticationmethods|kbdinteractiveauthentication) "; grep -l pam_google_authenticator /etc/pam.d/* 2>/dev/null'
run 06-logging bash -c 'journalctl --disk-usage; grep -hE "^(Storage|SystemMaxUse|MaxRetentionSec)" /etc/systemd/journald.conf /etc/systemd/journald.conf.d/*.conf 2>/dev/null; journalctl --list-boots | head -3; timedatectl show -p NTPSynchronized -p Timezone'
run 07-detection bash -c 'systemctl is-active fail2ban auditd; fail2ban-client status sshd 2>/dev/null | grep -E "Total (failed|banned)"; auditctl -l 2>/dev/null | grep -c canary; tail -20 /srv/inventory/detection-tests.log 2>/dev/null'
run 08-backup bash -c 'restic -r "${RESTIC_REPOSITORY:-/srv/backup}" snapshots --latest 1 --json 2>/dev/null | jq -r ".[0] | \"\(.time) \(.hostname) \(.paths|join(\",\"))\""; tail -5 /srv/inventory/restore-tests.log 2>/dev/null'
run 09-crypto bash -c 'for port in $(ss -tlnH | awk "{sub(/.*:/,\"\",\$4); print \$4}" | sort -un); do cert=$(echo | timeout 3 openssl s_client -connect "localhost:$port" 2>/dev/null | openssl x509 -noout -subject -enddate 2>/dev/null | paste -sd" "); [ -n "$cert" ] && echo "port $port: $cert"; done; true'
run 10-firewall bash -c 'ufw status verbose 2>/dev/null || nft list ruleset 2>/dev/null | head -60'
run 11-config-hash bash -c 'find /etc/ssh /etc/sudoers /etc/sudoers.d /etc/pam.d /etc/ufw /etc/cron.d -type f -exec sha256sum {} + 2>/dev/null | sort -k2'
run 12-incidents bash -c 'cat /srv/inventory/incident-register.md 2>/dev/null || echo "(aucun registre trouvé — à créer)"'
# Manifeste + signature
( cd "$OUT" && sha256sum *.txt > MANIFEST.sha256 )
ssh-keygen -Y sign -f /etc/evidence/key -n evidence "$OUT/MANIFEST.sha256"
tar -C /srv/evidence -czf "$OUT.tar.gz" "$(basename "$OUT")" && rm -rf "$OUT"
echo "$OUT.tar.gz"
EOF
sudo chmod 0755 /usr/local/sbin/evidence-pack.sh
sudo install -d /srv/evidence
echo '0 5 1 * * root /usr/local/sbin/evidence-pack.sh >> /var/log/evidence-pack.log 2>&1' | sudo tee /etc/cron.d/evidence-pack
Une archive le premier de chaque mois. N'importe qui peut vérifier avec la clé publique, sans accès au serveur :
tar -xzf web-01-20260901T050000Z.tar.gz && cd web-01-20260901T050000Z
echo "evidence@web-01 $(cat key.pub)" > allowed_signers # key.pub reçu de l'administrateur
ssh-keygen -Y verify -f allowed_signers -I evidence@web-01 -n evidence -s MANIFEST.sha256.sig < MANIFEST.sha256
sha256sum -c MANIFEST.sha256
Deux OK : le manifeste est signé par la clé de cet hôte, et aucun fichier n'a changé depuis. C'est la différence entre « un répertoire de fichiers texte » et une preuve.
Pièges
- La preuve sur le serveur que vous prouvez. Si le serveur est compromis, l'archive qui s'y trouve ne vaut rien. Copiez chaque archive immédiatement ailleurs (rsync vers un serveur de preuves, ou un object store avec object lock).
- La clé privée sur le même hôte. Qui est root peut signer de nouvelles archives. C'est acceptable pour « non modifié après coup », pas pour « non falsifié par root ». Si vous voulez ce dernier point, signez sur une machine distincte qui récupère les archives.
- Des horodatages sans source de temps. Un auditeur peut demander comment vous savez que
2026-09-01T05:00:00Zest correct. La synchronisation NTP (pièce 6) est la réponse ; pour une preuve vraiment dure, il existe l'horodatage RFC 3161, mais c'est excessif pour la plupart des PME. - Tout collecter, ne rien lire. Une archive que personne n'ouvre n'attrape pas l'écart. Fixez un rendez-vous trimestriel : deux archives côte à côte,
diff, expliquer les écarts. - Laisser le registre des incidents vide. « Aucun incident » est un signal d'alarme pour un auditeur. Les bans fail2ban, une fausse alerte honeypot, un certificat expiré vu juste à temps : autant de lignes dans le registre.
Ce qu'il vous manque encore
- Le résumé de flotte. Trente archives par mois ne répondent pas à « quel pourcentage de mes hôtes avait la MFA imposée le 1er septembre ? ». C'est un script sur les archives — puis un tableur.
- La continuité entre les instantanés. L'archive du 1er septembre et celle du 1er octobre ne disent rien du 15 septembre. Un contrôle tombé en panne dix jours entre-temps, vous ne le voyez pas.
- Une chaîne vérifiable. Chaque archive est signée isolément. Qu'une archive manque (ou soit remplacée par une plus ancienne) ne prouve rien — il faut pour cela une chaîne de hachages qui relie chaque archive à la précédente.
Comment monsys fait
monsys exécute les requêtes de preuve en continu sur tous les hôtes et conserve le résultat dans un transparency log : chaque entrée contient le hachage de la précédente, et chaque entrée est signée Ed25519 avec une clé par tenant. L'audit pack mensuel est un JSONL.gz stable à l'octet plus un PDF avec un verify.py hors ligne — l'auditeur n'a pas besoin de compte. Par contrôle NIS2, ISO 27001 et CRA, vous voyez un pourcentage de couverture, et un contrôle qui perd sa preuve déclenche une alerte avant le prochain audit. L'Auditor Workbench crée en un clic un ZIP avec uniquement les artefacts du périmètre demandé.
FAQ
Un auditeur accepte-t-il des scripts maison comme preuve ?
Oui, à condition que la sortie soit datée, reproductible et impossible à modifier après coup. D'où le manifeste de hachages et la signature. Ce qu'un auditeur n'accepte pas : une capture d'écran sans date, ou un document Word décrivant ce que vous « faites ».
Combien de temps dois-je conserver les preuves ?
NIS2 ne fixe pas de durée pour les preuves techniques ; le CCB peut remonter jusqu'à cinq ans lors d'une enquête. ISO 27001 exige que vous définissiez une durée de conservation. En pratique : trois ans pour les archives mensuelles, et le registre des incidents pour toujours.
Par hôte ou par organisation ?
Les deux. Les artefacts sont par hôte (c'est là que se trouve la configuration), mais l'auditeur veut une réponse à l'échelle de l'organisation (« tous les serveurs ont la MFA »). Collectez par hôte, résumez par organisation, et conservez les deux.
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.