Security & detectiegevorderd4 min lezen

Sudo-rechten en authorized_keys fleet-breed inventariseren met één script

Wie kan er op welke server root worden, en met welke SSH-sleutel? Op één host is dat vijf commando's; op dertig hosts is het de vraag die niemand meer beantwoordt. Dit script levert per host één tsv met accounts, sudo-regels en elke geautoriseerde public key (met fingerprint en comment), en de diff-check die meldt zodra er een key of sudo-regel bijkomt.

Inhoud
  1. Stap 1: wat er op één host staat
  2. Stap 2: één script, één tsv per host
  3. Stap 3: fleet-breed verzamelen en per persoon draaien
  4. Stap 4: dagelijkse diff — meld wat erbij kwam
  5. Stap 5: opruimen zonder iemand buiten te sluiten
  6. Valkuilen
  7. Wat je hiermee nog niet hebt
  8. Zo doet monsys het
  9. FAQ

De vraag komt altijd op het slechtste moment: een collega vertrekt, een laptop wordt gestolen, een auditor vraagt "wie heeft root op de databaseserver?" — en het antwoord is een rondje ssh over alle hosts terwijl iedereen wacht. Het probleem is niet dat de informatie ontbreekt; ze staat in /etc/passwd, /etc/sudoers en dertig authorized_keys-bestanden. Het probleem is dat niemand ze op één plek heeft, met een datum, in een vorm die je kunt vergelijken met gisteren.

Stap 1: wat er op één host staat

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

# Wie mag sudo? Via groep...
getent group sudo admin wheel 2>/dev/null
# ...en via expliciete regels (zonder commentaar en Defaults)
sudo grep -rhvE '^\s*(#|@|Defaults|$)' /etc/sudoers /etc/sudoers.d/ 2>/dev/null

# Welke keys zijn geautoriseerd, waar, en van wie?
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, fingerprint, comment, type
done

Twee dingen die ssh-keygen -lf je meteen laat zien: keys van 1024-bit RSA of van het type ssh-dss (allebei sinds jaren verboden in OpenSSH-defaults, maar nog verrassend vaak aanwezig), en keys zonder comment — waarvan niemand weet van wie ze zijn.

Vergeet de plaatsen buiten authorized_keys niet:

# Alternatieve locaties uit sshd_config
sshd -T | grep -iE '^(authorizedkeysfile|authorizedkeyscommand|trustedusercakeys) '
# SSH-certificaten (CA) — dan zit de autorisatie bij de CA, niet in een bestand
# Systeemaccounts met een shell én een key (deploy-users, backup-users)
awk -F: '$3<1000 && $7!~/nologin|false/ {print $1}' /etc/passwd

Stap 2: één script, één tsv per host

Het script levert drie soorten regels — user, sudo, key — in een vorm die je kunt sorteren, diffen en in een spreadsheet plakken.

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

# Accounts met een shell (incl. systeemaccounts met shell — deploy-users zijn óók toegang)
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

# Laatste SSH-login per account, uit de journal (lastlog en wtmp zijn op Ubuntu ≥ 24.04 niet meer betrouwbaar/aanwezig)
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 groepslidmaatschap
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 expliciete regels (zonder commentaar, Defaults en @include-directieven)
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 }'

# Keys: één regel per geautoriseerde key, met fingerprint, type en comment
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 (geen comment)   ← van wie?

Stap 3: fleet-breed verzamelen en per persoon draaien

Vanaf een beheer-host, over een 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"

# Draai de vraag om: per KEY, op welke hosts en onder welk account staat hij?
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 (geen comment)         → web-01:deploy   ← één key, geen eigenaar: opruimen of labelen
# SHA256:aa1… RSA ci-runner              → web-01:deploy, web-02:deploy, db-01:root   ← CI-key op root van de database?

# Per persoon: op welke hosts heeft hij sudo?
awk -F'\t' '$2=="sudo" {print $3, "→", $1, "("$4")"}' "$D/$(date +%F).tsv" | sort

Die omgekeerde lijst — per key, waar staat hij — is waar de verrassingen zitten: de CI-key op de root-account van de database, de key van een ex-collega onder een gedeeld deploy-account, en de key zonder comment die niemand durft te verwijderen.

Stap 4: dagelijkse diff — meld wat erbij kwam

Een nieuwe key of sudo-regel is altijd óf een bewuste wijziging (dan staat er een ticket tegenover) óf een incident. In beide gevallen wil je hem dezelfde dag zien.

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
# Alleen keys en sudo-regels vergelijken (lastlogin verandert elke dag)
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

(Zet de while read-loop uit stap 3 in /usr/local/sbin/access-collect.sh.) Het resultaat: elke ochtend een bericht als er iets veranderde, en anders stilte. Een ADDED … key … root …-regel zonder ticket is een incident, geen to-do.

Stap 5: opruimen zonder iemand buiten te sluiten

De lijst is één ding; keys weghalen is het spannende deel. Werk in deze volgorde:

  1. Comment toevoegen aan elke key waarvan je de eigenaar kent (ssh-keygen kan dat niet; bewerk het bestand: het derde veld is vrij).
  2. Keys zonder eigenaar eerst 30 dagen loggen: LogLevel VERBOSE in sshd_config logt de fingerprint bij elke login (Accepted publickey for deploy … SHA256:9f3…). Wordt hij niet gebruikt: weg.
  3. Gedeelde accounts (deploy met acht keys) omzetten naar persoonlijke accounts + sudo-regel, of naar SSH-certificaten met korte TTL. Dan is elke login herleidbaar tot een persoon.
  4. 1024-bit RSA en DSA vervangen door ed25519 — die accepteert OpenSSH 9+ standaard niet meer, dus ze zijn toch al kapot op de nieuwste hosts.

Valkuilen

  • AuthorizedKeysCommand. Als sshd keys uit een IdP of een centrale bron haalt, is authorized_keys leeg en je inventaris ook. sshd -T vertelt het (stap 1); dan inventariseer je aan de bron.
  • Keys in ~/.ssh/authorized_keys2. Verouderd maar op oude hosts nog gelezen als het in AuthorizedKeysFile staat. Zelfde check.
  • Sudo via %group uit LDAP/SSSD. getent group sudo toont die leden alleen als NSS ze oplost; op hosts met SSSD kan dat traag zijn of leeg bij een storing. Cache-loze run in het onderhoudsvenster.
  • NOPASSWD-regels op commando's met argumenten. NOPASSWD: /usr/bin/systemctl (zonder argumenten) betekent elk systemctl-commando, inclusief systemctl --now disable auditd. Beperk tot het exacte commando met argumenten.
  • sudo -l per gebruiker vergeten. De sudoers-regels zeggen wat mag; sudo -l -U jeroen zegt wat effectief mag na alle groepen en aliassen. Voor de review is dat de betere bron.

Wat je hiermee nog niet hebt

  • De koppeling met personeel. De tsv zegt "jeroen heeft sudo op 12 hosts", niet of Jeroen nog in dienst is. Dat is de kwartaal-review, met de HR-lijst ernaast.
  • Historie per key. Wanneer verscheen deze key, wie zette hem erop (welke sudo-sessie), en wanneer werd hij voor het laatst gebruikt? Drie vragen, drie bronnen.
  • Bewijs. Een auditor wil de inventaris ondertekend en onveranderbaar, en het bewijs dat de diff-alert werkt (een testlog).

Zo doet monsys het

De monsys-agent inventariseert per host de accounts, sudo-rechten (effectief, na groepen en aliassen), authorized_keys per gebruiker met fingerprint en comment, en de laatste login — als onderdeel van de gewone inventaris. De hub toont het fleet-breed per persoon en per key, meldt een nieuwe key of sudo-regel als drift-detectie, en genereert elk kwartaal een ondertekend access-review-rapport (ISO 27001 A.5.18) met alle accounts, hun rechten en hun laatste activiteit — inclusief de SSO-configuratie zonder secrets.

FAQ

Hoe zie ik van wie een SSH-key is als er geen comment bij staat?

Zet LogLevel VERBOSE in sshd_config; vanaf dan logt elke login de fingerprint. Na een paar weken weet je welke keys gebruikt worden en vanaf welk IP. Keys die nooit gebruikt worden, kun je verwijderen; keys die wel gebruikt worden, vraag je na bij de gebruiker van dat IP.

Wat is het verschil tussen de sudo-groep en een sudoers-regel?

Lidmaatschap van sudo (Ubuntu) of wheel (RHEL) geeft ALL=(ALL:ALL) ALL — alles. Een expliciete regel in /etc/sudoers.d/ kan beperkt zijn tot één commando. Voor de review tellen beide; sudo -l -U <user> toont het gecombineerde resultaat.

Hoe vaak moet ik dit draaien?

De inventaris dagelijks (automatisch, met de diff-alert), de menselijke review elk kwartaal. NIS2 en ISO 27001 vragen niet om een frequentie in dagen, maar om aantoonbaarheid dat toegang periodiek wordt herzien — en een kwartaal is wat auditors verwachten.

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.