NIS2, ISO 27001 & bewijsgevorderd6 min lezen

Wat een NIS2-auditor écht vraagt: twaalf bewijsstukken en het script dat ze uit je servers haalt

Geen beleid, geen intenties: een auditor wil artefacten met een datum. Dit zijn de twaalf die in vrijwel elke CyFun- of NIS2-audit terugkomen, met per stuk het commando dat het oplevert, en een evidence-pack.sh dat alles in één ondertekend archief zet dat over zes maanden nog controleerbaar is.

Inhoud
  1. De twaalf bewijsstukken
  2. Het script: evidence-pack.sh
  3. Valkuilen
  4. Wat je hiermee nog niet hebt
  5. Zo doet monsys het
  6. FAQ

Een audit is geen examen over wat je weet, het is een controle van wat je kunt tonen. De auditor heeft een lijst met controls, en bij elke control stelt hij dezelfde vraag: "waaruit blijkt dat?" Het antwoord "dat doen we" scoort nul. Het antwoord "hier is de output van 14 augustus, hier die van 14 september, en dit is de afwijking die we toen hebben opgelost" scoort vol. Dit artikel is de lijst van twaalf artefacten die je klaar wilt hebben, plus het script dat ze verzamelt.

De twaalf bewijsstukken

Per stuk: welke control het dekt (NIS2 art. 21 §2 en de ISO 27001:2022-tegenhanger), wat de auditor wil zien, en het commando.

1. Asset-inventaris met datum

NIS2 (a), (i) · ISO A.5.9 Wat: per host het OS, de kernel, de luisterende poorten, de draaiende services. Niet "een CMDB" maar een reproduceerbare lijst.

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. Patchstatus en doorlooptijd

NIS2 (e) · ISO A.8.8 Wat: welke updates staan open, wanneer werd de laatste geïnstalleerd, en hoeveel dagen zat er tussen publicatie en installatie.

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. Open kwetsbaarheden, met prioriteit

NIS2 (d), (e) · ISO A.8.8 Wat: de CVE-lijst van deze host, met EPSS/KEV, en de beslissing per item (patchen, mitigeren, accepteren). Zie de OSV-how-to; het script daar produceert de tsv. Het bewijs is de tsv plus een regel per geaccepteerd risico:

CVE-2026-1234  libxml2  accepted-risk  2026-09-01  "niet bereikbaar: lib alleen gebruikt door offline batch"  goedgekeurd: J. Peeters

4. Toegang: accounts, sudo, SSH-keys

NIS2 (i) · ISO A.5.15, A.5.18, A.8.2 Wat: wie kan er binnen, met welke rechten, en wanneer is dat voor het laatst nagekeken.

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

De kwartaal-review is een aparte regel in je log: 2026-09-15 access-review web-01: 4 accounts, 1 verwijderd (ex-medewerker), 0 onbekende keys — J. Peeters.

5. MFA en authenticatiebeleid

NIS2 (j) · ISO A.5.17, A.8.5 Wat: dat wachtwoord-login uit staat en dat beheer-toegang een tweede factor vraagt.

sshd -T | grep -iE '^(passwordauthentication|permitrootlogin|authenticationmethods|kbdinteractiveauthentication) '
grep -l pam_google_authenticator /etc/pam.d/* 2>/dev/null

Voor een IdP (Entra, Keycloak, Authentik): de export van het MFA-beleid, met datum.

6. Logging en retentie

NIS2 (b) · ISO A.8.15 Wat: dat logs bestaan, hoe lang ze bewaard blijven, en dat de klok klopt.

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         # oudste boot = hoe ver je terug kunt
timedatectl show -p NTPSynchronized -p Timezone

7. Detectie is actief, en wordt getest

NIS2 (b) · ISO A.8.16 Wat: welke detecties draaien (brute-force, honeypots, integriteit) en wanneer ze voor het laatst zijn getest.

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 je testlog: 2026-09-01 canary-test web-01: alert ontvangen na 3 min — OK. Een detectie zonder testlog is voor een auditor een detectie die misschien werkt.

8. Back-up: recent, controleerbaar, hersteld

NIS2 (c) · ISO A.8.13 Wat: laatste geslaagde back-up, laatste integriteitscheck, laatste restore-test.

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

Zie back-ups controleren voor het restore-test-script.

9. Cryptografie: certificaten en protocollen

NIS2 (h) · ISO A.8.24 Wat: welke certificaten er draaien, wanneer ze verlopen, en dat oude protocollen uit staan.

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: uit" || echo "TLS1.1: AAN"

10. Firewall en netwerksegmentatie

NIS2 (g) · ISO A.8.20, A.8.22 Wat: default-deny inkomend, en de lijst van open poorten die overeenkomt met de inventaris.

sudo ufw status verbose 2>/dev/null || sudo nft list ruleset 2>/dev/null | head -60

11. Configuratie-integriteit

NIS2 (e) · ISO A.8.9 Wat: dat kritieke configuratie niet ongemerkt verandert. De eenvoudigste vorm: een hash-lijst die je periodiek vergelijkt.

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
# Volgende keer: wat veranderde er?
sha256sum -c --quiet /srv/inventory/$(hostname -s)-config.sha256 2>&1 | grep -v ': OK$'

12. Incidentregister en meldprocedure

NIS2 (b), art. 23 · ISO A.5.24–A.5.28 Wat: de lijst van incidenten (ook de kleine, ook "vals alarm"), met tijdstip van detectie, beoordeling, actie en — als het significant was — het tijdstip van de melding aan het CCB. Dit is geen commando; het is een tabel die je bijhoudt. Leeg is verdacht; een auditor gelooft niet dat er in een jaar niets is gebeurd.

Het script: evidence-pack.sh

Alles hierboven in één archief per host, met een manifest van hashes en een handtekening die aantoont dat het archief sindsdien niet is gewijzigd. Ondertekenen doen we met ssh-keygen -Y (OpenSSH ≥ 8.0, dus overal aanwezig) en een aparte Ed25519-sleutel die alleen voor bewijs dient.

# Eenmalig: een bewijssleutel, niet je persoonlijke SSH-key
sudo install -d -m 0700 /etc/evidence
sudo ssh-keygen -q -t ed25519 -N '' -C 'evidence@'"$(hostname -s)" -f /etc/evidence/key
# De publieke sleutel geef je aan de auditor (en zet je in 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 "(geen register gevonden — aanmaken)"'

# Manifest + handtekening
( 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

Elke eerste van de maand een archief. Verifiëren kan iedereen met de publieke sleutel, zonder toegang tot de server:

tar -xzf web-01-20260901T050000Z.tar.gz && cd web-01-20260901T050000Z
echo "evidence@web-01 $(cat key.pub)" > allowed_signers      # key.pub van de beheerder gekregen
ssh-keygen -Y verify -f allowed_signers -I evidence@web-01 -n evidence -s MANIFEST.sha256.sig < MANIFEST.sha256
sha256sum -c MANIFEST.sha256

Twee OK's: het manifest is ondertekend door de sleutel van die host, en geen enkel bestand is sindsdien veranderd. Dat is het verschil tussen "een map met tekstbestanden" en bewijs.

Valkuilen

  • Bewijs op de server die je bewijst. Als de server gecompromitteerd raakt, is het archief dat erop staat waardeloos. Kopieer elk archief onmiddellijk naar een andere plek (rsync naar een bewijsserver, of een object store met object-lock).
  • De privésleutel op dezelfde host. Wie root is, kan nieuwe archieven ondertekenen. Dat is aanvaardbaar voor "niet achteraf gewijzigd", niet voor "niet vervalst door root". Wil je dat laatste, dan onderteken je op een aparte machine die de archieven ophaalt.
  • Tijdstempels zonder tijdbron. Een auditor kan vragen hoe je weet dat 2026-09-01T05:00:00Z klopt. NTP-sync (bewijsstuk 6) is het antwoord; voor het echt harde bewijs bestaat RFC 3161-timestamping, maar dat is voor de meeste KMO's overkill.
  • Alles verzamelen, niets lezen. Een archief dat niemand opent, vangt de afwijking niet. Zet een kwartaal-afspraak: twee archieven naast elkaar, diff, afwijkingen verklaren.
  • Het incidentregister leeg laten. "Geen incidenten" is voor een auditor een rode vlag. Fail2ban-bans, een vals honeypot-alarm, een verlopen certificaat dat je net op tijd zag: allemaal regels in het register.

Wat je hiermee nog niet hebt

  • Fleet-samenvatting. Dertig archieven per maand geven geen antwoord op "hoeveel procent van mijn hosts had op 1 september MFA afgedwongen?". Dat is een script over de archieven heen — en dan een spreadsheet.
  • Continuïteit tussen de snapshots. Het archief van 1 september en dat van 1 oktober zeggen niets over 15 september. Een control die tussendoor tien dagen uitviel, zie je niet.
  • Een verifieerbare keten. Elk archief is los ondertekend. Dat een archief ontbreekt (of vervangen is door een ouder), bewijst niets — daar heb je een hash-keten voor nodig die elk archief aan het vorige koppelt.

Zo doet monsys het

monsys draait de evidence-queries continu over alle hosts en bewaart het resultaat in een transparency log: elke entry bevat de hash van de vorige, en elke entry is Ed25519-ondertekend met een per-tenant sleutel. Het maandelijkse audit pack is een byte-stabiel JSONL.gz plus PDF met een offline verify.py — de auditor heeft geen account nodig. Per NIS2-, ISO 27001- en CRA-control zie je een dekkingspercentage, en een control die bewijs verliest geeft een alert vóór de volgende audit. De Auditor Workbench maakt een one-click ZIP met alleen de artefacten voor de gevraagde scope.

FAQ

Accepteert een auditor zelfgemaakte scripts als bewijs?

Ja, mits de output een datum heeft, reproduceerbaar is en niet achteraf gewijzigd kan zijn. Vandaar het manifest met hashes en de handtekening. Wat een auditor niet accepteert: een screenshot zonder datum, of een Word-document dat beschrijft wat je "doet".

Hoe lang moet ik bewijs bewaren?

NIS2 zelf noemt geen termijn voor technisch bewijs; het CCB kan tot vijf jaar terug vragen bij een onderzoek. ISO 27001 vraagt dat je een bewaartermijn definieert. Praktisch: drie jaar voor maandelijkse archieven, en het incidentregister voor altijd.

Moet dit per host of per organisatie?

Beide. De artefacten zijn per host (daar staat de configuratie), maar de auditor wil een organisatiebreed antwoord ("alle servers hebben MFA"). Verzamel per host, vat samen per organisatie, en bewaar allebei.

Geschreven door het monsys-team — sysadmins die dit dagelijks doen.

Zelf gedaan? Laat monsys het bijhouden.

Alles uit deze how-to draait in monsys als doorlopende check, met historie, alerts en audit-bewijs. 5 servers gratis, EU-gehost in België, geïnstalleerd in 60 seconden.