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
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:
- Comment toevoegen aan elke key waarvan je de eigenaar kent (
ssh-keygenkan dat niet; bewerk het bestand: het derde veld is vrij). - Keys zonder eigenaar eerst 30 dagen loggen:
LogLevel VERBOSEinsshd_configlogt de fingerprint bij elke login (Accepted publickey for deploy … SHA256:9f3…). Wordt hij niet gebruikt: weg. - Gedeelde accounts (
deploymet acht keys) omzetten naar persoonlijke accounts +sudo-regel, of naar SSH-certificaten met korte TTL. Dan is elke login herleidbaar tot een persoon. - 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, isauthorized_keysleeg en je inventaris ook.sshd -Tvertelt het (stap 1); dan inventariseer je aan de bron.- Keys in
~/.ssh/authorized_keys2. Verouderd maar op oude hosts nog gelezen als het inAuthorizedKeysFilestaat. Zelfde check. - Sudo via
%groupuit LDAP/SSSD.getent group sudotoont 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, inclusiefsystemctl --now disable auditd. Beperk tot het exacte commando met argumenten. sudo -lper gebruiker vergeten. De sudoers-regels zeggen wat mag;sudo -l -U jeroenzegt 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.