Een beheerpaneel op een server loopt bijna altijd tegen dezelfde muur aan: het webproces moet iets doen waarvoor het geen rechten heeft. Een adres blokkeren, een poort openen, het journaal lezen. Het antwoord dat zich aandient is één smalle regel in sudoers toekennen, en niets daarbuiten:
www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client set * banip *
De regel leest als «alleen blokkeren». In werkelijkheid betekent hij «wat dan ook, als root». Hieronder: waarom dat zo uitpakt, hoe u het op uw eigen server in vijf minuten nagaat en waarmee zulke regels worden vervangen.
Wat * werkelijk doet
Het beslissende punt: sudo vergelijkt niet argument voor argument maar de commandoregel als geheel, en * in het patroon loopt vlot over spaties heen — dus over argumentgrenzen. Het letterlijke banip midden in de regel beperkt daarom niets: het is genoeg dat het woord banip ergens in het commando voorkomt, en ervoor zowel als erna kan alles worden meegegeven.
Vervolgens zoekt men een programma waaraan een uit te voeren commando kan worden doorgegeven. In ons geval stond het in de regel zelf. Bij fail2ban wordt de blokkeeractie als tekst vastgelegd, en die tekst laat zich tijdens het draaien herdefiniëren:
fail2ban-client get portscan actions
sudo fail2ban-client set portscan action nftables-multiport actionban "<commando>" banip 192.0.2.10
sudo fail2ban-client set portscan banip 192.0.2.11
De eerste aanroep vervangt de actietekst van een echte actie, de tweede laat de jail afgaan — en fail2ban, dat als root draait, voert uit wat erin is gezet. Wij hebben dit op 4 juli op onze eigen server gedaan: een bestand in /root werd als root aangemaakt, dat wil zeggen dat het webproces volledige rechten op de machine kreeg uit een regel die smal leek.
Onderweg kwamen drie bijzonderheden boven die in andermans beschrijvingen doorgaans ontbreken:
- de optie
-cvóórsetknipt sudo eraf: in het patroon zitsetvast aan de naam van het programma en ervoor valt niets in te voegen; addactionenactionzonder het woordbanipkomen er ook niet door — maar al het nodige past in één commandoset … banip …, en daarmee is aan het patroon voldaan;- de naam van de actie moet een echte zijn, anders is er niets te vervangen:
fail2ban-client get <jail> actionslaat hem zien.
Wie er nog op dezelfde lijst staan
fail2ban is hier niet de schuldige en ook geen uitzondering. Gevaarlijk is de combinatie van NOPASSWD met een sterretje. Wat wij er op onze eigen servers naast vonden:
journalctl *— het journaal gaat open via een pager, en vanuit een pager start men een shell. De regel lijkt over het lezen van logs te gaan; in de praktijk is het een rootshell. De remedie is geen nauwer patroon maar de groepsystemd-journal: dan werktjournalctlhelemaal zonder sudo;grep * /var/log/fail2ban.log— het eerste argument vangrepis het patroon, maar het sterretje laat toe ook een tweede pad mee te geven, en grep draait als root./etc/shadowuitlezen met die regel is één commando. De vervanging is dezelfde: de groepadmvoor het lezen van logs;ufw --force *en de naakteufw allow/deny/delete *— het webproces kan de firewall in zijn geheel uitzetten. Extra onaangenaam: in beide gevallen gebruikte het paneel die regels helemaal niet — dode machtigingen uit vroege versies van het installatieprogramma;lynis-scan.sh *— een omhulsel dat"$@"doorgeeft aan een rootproces staat gelijk aan een regel zonder enige beperking.
Hoe u ziet wat er bij u staat
Waar het om gaat is niet het bestand maar de feitelijke rechten van één bepaalde gebruiker — degene waaronder de PHP-FPM-pool werkelijk draait (niet altijd www-data; op een van onze machines draaide het paneel als admin):
ps -o user= -C php-fpm | sort -u
sudo -l -U www-data
sudo -l -U admin
Daarna de bestandslijst zelf, en hier zitten twee valkuilen die ons tijd hebben gekost:
sudo grep -rn 'NOPASSWD' /etc/sudoers /etc/sudoers.d/
ls -la /etc/sudoers.d/
De eerste: regels leven op twee plaatsen tegelijk. Bij ons stond de gevaarlijke regel zowel in /etc/sudoers.d/monitor als in de hoofd-/etc/sudoers — in die laatste in ongeveer acht exemplaren, opgebouwd door verschillende versies van het installatieprogramma. Eén plek opruimen en rustig achteroverleunen is de gebruikelijke manier om het gat open te laten.
De tweede: sudo negeert bestanden met een punt in de naam. Een bestand www-data.bak3 in /etc/sudoers.d/ ziet uit als een geldende regel en leest als een geldende regel, maar heeft geen werking. Dat werkt naar twee kanten: «de regel is er, de rechten niet», en de valse rust van een configuratiekopie die «er vlak naast ligt».
Waarmee vervangen
Het patroon nauwer maken helpt niet: een sterretje waar dan ook in de regel zet de opgave terug op het begin. Eén aanpak houdt stand: een rootomhulsel dat vaste positionele argumenten aanneemt, ze zelf controleert en het programma aanroept zonder enige mogelijkheid iets toe te voegen.
#!/bin/sh
# /usr/local/sbin/arciveo-f2b — ban|unban <jail> <ip>, en niets meer
set -eu
action="$1"; jail="$2"; ip="$3"
case "$action" in ban|unban) ;; *) echo "invalid action"; exit 1 ;; esac
case "$jail" in *[!a-zA-Z0-9_-]*|'') echo "invalid jail"; exit 1 ;; esac
printf '%s' "$ip" | grep -Eq '^[0-9a-fA-F:.]+$' || { echo "invalid ip"; exit 1; }
case "$action" in
ban) exec /usr/bin/fail2ban-client set "$jail" banip "$ip" ;;
unban) exec /usr/bin/fail2ban-client set "$jail" unbanip "$ip" ;;
esac
Het omhulsel is eigendom van root, rechten 755, en — dit is niet optioneel — het mag niet schrijfbaar zijn vanaf het web, anders is de hele constructie zinloos. In sudoers blijft alleen dit staan:
www-data ALL=(root) NOPASSWD: /usr/local/sbin/arciveo-f2b
Argumenten worden in de regel niet opgesomd: het script controleert ze zelf, en de posities daarin staan vast, dus er is geen plek om een actietekst tussen te schuiven. Regels die alleen lezen — statussen, ss, ipset list — komen op aparte regels, ook zonder sterretje waar dat kan.
Apart iets over het vervangen van sudo door groepen. De aanpak is juist: systemd-journal voor het journaal en adm voor logs zijn goedkoper en veiliger dan welke sudo-regel ook. Maar de webgebruiker blindelings uit groepen halen kan niet. Wij haalden www-data op een paneelhost uit één groep en kregen in één keer 403 op alle sites: Apache draait daar als www-data, de bestanden van de sites horen bij een andere gebruiker, en het leesrecht kwam juist van dat groepslidmaatschap. Het herstel vergde een volledige herstart — geen reload — van Apache en PHP-FPM, omdat oude workers hun eerdere groepenset behouden en «het werkt, en dan 403» opleveren.
Controle na de wijziging
Voordat u een sudoers-bestand vervangt, moet de syntaxis ervan worden nagekeken — anders kunt u zonder sudo komen te zitten:
sudo visudo -cf /etc/sudoers.d/monitor
Daarna nagaan dat de oude route dood is, en wel op naam van diezelfde gebruiker:
sudo -k
sudo -u www-data sudo -n fail2ban-client set portscan banip 192.0.2.10
Het antwoord moet a password is required zijn, terwijl het omhulsel aanroepen met rommel in plaats van een adres invalid ip moet geven. Hier loert een eigen valkuil: sudo -v bewaart de bevestiging vijftien minuten, en daarna slaagt elke controle met sudo -n «met succes». Precies zo hebben wij een keer een regel bevestigd die op de server helemaal niet bestond. Daarom is sudo -k vóór de controle verplicht.
Waartoe dat toezicht dient
Sudo-regels wijzigen zelden, maar ze wijzigen onopgemerkt: het installatieprogramma schrijft erbij, het paneel van de hoster vult het eigen aan, een verwijderd pakket laat zijn regels achter. Een eenmalige doorlichting sluit wat er vandaag is en zegt niets over wat de volgende update meebrengt. De praktische gevolgtrekking is simpel: de lijst van wie root kan worden hoort in het zicht, naast de rest van de toestand van de server, en niet in de herinnering ná een incident. Hoe dat er samengesteld uitziet, tonen de demopagina's hieronder.