Et administrationspanel på en server rammer næsten altid samme mur: webprocessen skal gøre noget, den ikke har rettigheder til. Blokere en adresse, åbne en port, læse journalen. Svaret, der ligger lige for, er at give en enkelt smal linje i sudoers og intet andet:
www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client set * banip *
Linjen læses som «kun blokere». I virkeligheden betyder den «hvad som helst, som root». Herunder: hvorfor det ender sådan, hvordan du kontrollerer det på din egen server på fem minutter, og hvad sådanne regler erstattes med.
Hvad * i virkeligheden gør
Den afgørende detalje: sudo sammenligner ikke argument for argument, men kommandolinjen som helhed, og * i mønsteret går uhindret hen over mellemrum — altså hen over argumentgrænser. Literalen banip, der står midt i reglen, begrænser derfor ingenting: det er nok, at ordet banip optræder et sted i kommandoen, og både før og efter kan hvad som helst sendes med.
Derefter leder man efter et program, der kan få en kommando at udføre. I vores tilfælde stod det i selve reglen. I fail2ban angives blokeringshandlingen som tekst, og denne tekst kan omdefineres under drift:
fail2ban-client get portscan actions
sudo fail2ban-client set portscan action nftables-multiport actionban "<kommando>" banip 192.0.2.10
sudo fail2ban-client set portscan banip 192.0.2.11
Det første kald udskifter handlingsteksten i en virkelig handling, det andet får jailet til at udløse — og fail2ban, der kører som root, udfører det indsatte. Vi gjorde dette den 4. juli på vores egen server: en fil i /root blev oprettet som root, altså fik webprocessen fulde rettigheder på maskinen ud af en regel, der så smal ud.
Undervejs kom tre detaljer frem, som normalt mangler i andres gennemgange:
- flaget
-cforansetskæres væk af sudo: i mønsteret siddersetfast på programfilens navn, og der kan intet indsættes foran; addactionogactionuden ordetbanipslipper heller ikke igennem — men alt det nødvendige er der plads til i én kommandoset … banip …, og dermed er mønsteret opfyldt;- handlingens navn skal være et virkeligt, ellers er der ikke noget at udskifte:
fail2ban-client get <jail> actionsviser det.
Hvem der ellers står på samme liste
fail2ban har ingen skyld her og er ikke noget særtilfælde. Det farlige er kombinationen NOPASSWD plus stjerne. Hvad vi fandt ved siden af på vores egne servere:
journalctl *— journalen åbnes via en pager, og fra en pager starter man en skal. Reglen ser ud til at handle om at læse logfiler; i praksis er den en rodskal. Kuren er ikke et smallere mønster, men gruppensystemd-journal: så virkerjournalctlhelt uden sudo;grep * /var/log/fail2ban.log— første argument tilgreper mønsteret, men stjernen tillader også at angive en anden sti, og grep kører som root. At læse/etc/shadowmed den regel er én kommando. Erstatningen er den samme: gruppenadmtil at læse logfiler;ufw --force *og nøgneufw allow/deny/delete *— webprocessen kan slå firewallen helt fra. Særligt ubehageligt: i begge tilfælde brugte panelet slet ikke de regler, døde rettigheder efterladt af tidlige versioner af installationsprogrammet;lynis-scan.sh *— et omslag, der sender"$@"videre til en rodproces, svarer til en regel uden nogen begrænsninger.
Sådan ser du, hvad du har
Det, der skal ses på, er ikke filen, men de faktiske rettigheder for én bestemt bruger — den, PHP-FPM-puljen faktisk kører som (ikke altid www-data; på en af vores maskiner kørte panelet som admin):
ps -o user= -C php-fpm | sort -u
sudo -l -U www-data
sudo -l -U admin
Dernæst selve fillisten, og her er der to fælder, der har kostet os tid:
sudo grep -rn 'NOPASSWD' /etc/sudoers /etc/sudoers.d/
ls -la /etc/sudoers.d/
Den første: regler lever på to steder samtidig. Hos os stod den farlige linje både i /etc/sudoers.d/monitor og i hovedfilen /etc/sudoers — i den sidste i omkring otte kopier, samlet op af forskellige versioner af installationsprogrammet. At rydde op på ét sted og slå sig til ro er den sædvanlige måde at lade hullet stå åbent.
Den anden: sudo ignorerer filer, hvis navn indeholder et punktum. En fil www-data.bak3 i /etc/sudoers.d/ ser ud som en gældende regel og læses som en gældende regel, men har ingen virkning. Det slår begge veje: «reglen er der, rettighederne ikke», og den falske tryghed ved en konfigurationskopi, der «ligger lige ved siden af».
Hvad man erstatter med
At gøre mønsteret smallere hjælper ikke — en stjerne hvor som helst på linjen sætter opgaven tilbage til start. Kun én tilgang holder: et rodomslag, der tager faste positionsargumenter, validerer dem selv og kalder programmet uden den mindste mulighed for at tilføje noget.
#!/bin/sh
# /usr/local/sbin/arciveo-f2b — ban|unban <jail> <ip>, og intet andet
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
Omslaget ejes af root, rettigheder 755, og — dette er ikke valgfrit — det må ikke være skrivbart fra weben, ellers er hele konstruktionen meningsløs. I sudoers står kun det tilbage:
www-data ALL=(root) NOPASSWD: /usr/local/sbin/arciveo-f2b
Argumenter opregnes ikke i reglen: skriptet kontrollerer dem selv, og positionerne i det er faste, så der er ingen steder at snige en handlingstekst ind. Regler, der kun læser — statusser, ss, ipset list — kommer på egne linjer, også uden stjerne hvor det er muligt.
Særskilt om at erstatte sudo med grupper. Tilgangen er rigtig: systemd-journal til journalen og adm til logfilerne bliver billigere og sikrere end nogen sudo-regel. Men at trække webbrugeren ud af grupper i blinde går ikke. Vi fjernede www-data fra én gruppe på en panelvært og fik 403 på alle websteder på én gang: Apache kører der som www-data, webstedernes filer tilhører en anden bruger, og læseadgangen kom netop af det gruppemedlemskab. Genoprettelsen krævede en fuldstændig genstart — ikke en reload — af Apache og PHP-FPM, fordi gamle arbejdsprocesser beholder deres tidligere sæt grupper og giver «det virker, og så 403».
Kontrol efter ændringen
Før en sudoers-fil udskiftes, skal dens syntaks kontrolleres — ellers kan man stå helt uden sudo:
sudo visudo -cf /etc/sudoers.d/monitor
Derefter skal det bekræftes, at den gamle vej er død, og det i navnet på netop den bruger:
sudo -k
sudo -u www-data sudo -n fail2ban-client set portscan banip 192.0.2.10
Svaret skal være a password is required, mens et kald til omslaget med skrald i stedet for en adresse skal give invalid ip. Her lurer en fælde for sig: sudo -v gemmer bekræftelsen i femten minutter, og derefter går enhver kontrol med sudo -n igennem «med held». Præcis sådan bekræftede vi engang en regel, der slet ikke fandtes på serveren. Derfor er sudo -k før kontrollen påkrævet.
Hvad observationen er til for
Sudo-regler ændres sjældent, men de ændres lydløst: installationsprogrammet skriver til, hostingpanelet føjer sit eget til, en fjernet pakke lader sine linjer stå. En gennemgang én gang lukker det, der findes i dag, og siger intet om, hvad næste opdatering bringer. Den praktiske slutning er enkel: listen over, hvem der kan blive root, hører til i synsfeltet, ved siden af resten af serverens tilstand, og ikke i erindringen efter en hændelse. Hvordan det ser ud samlet, viser demosiderne nedenfor.