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.