Et administrasjonspanel på en server treffer nesten alltid samme vegg: webprosessen må gjøre noe den ikke har rettigheter til. Blokkere en adresse, åpne en port, lese journalen. Svaret som ligger nærmest er å gi én smal linje i sudoers, og ingenting mer:

www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client set * banip *

Linjen leses som «bare blokkere». I virkeligheten betyr den «hva som helst, som root». Nedenfor: hvorfor det blir slik, hvordan du kontrollerer det på din egen server på fem minutter, og hva slike regler erstattes med.

Hva * faktisk gjør

Den avgjørende detaljen: sudo sammenligner ikke argument for argument, men kommandolinjen som helhet, og * i mønsteret går uhindret over mellomrom — altså over argumentgrenser. Literalen banip som står midt i regelen, begrenser derfor ingenting: det er nok at ordet banip forekommer et eller annet sted i kommandoen, og både før og etter kan hva som helst sendes med.

Deretter leter man etter et program som kan få en kommando å kjøre. I vårt tilfelle sto det i selve regelen. I fail2ban angis blokkeringshandlingen som tekst, og denne teksten 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 kallet bytter ut handlingsteksten i en virkelig handling, det andre får jailet til å slå til — og fail2ban, som kjører som root, utfører det som er satt inn. Vi gjorde dette 4. juli på vår egen server: en fil i /root ble opprettet som root, altså fikk webprosessen fulle rettigheter på maskinen ut av en regel som så smal ut.

Underveis kom tre detaljer fram som vanligvis mangler i andres gjennomganger:

  • flagget -c foran set klippes bort av sudo: i mønsteret sitter set fast i navnet på programfilen, og ingenting kan settes inn foran;
  • addaction og action uten ordet banip slipper heller ikke gjennom — men alt som trengs, får plass i én kommando set … banip …, og dermed er mønsteret oppfylt;
  • navnet på handlingen må være et virkelig, ellers er det ingenting å bytte ut: fail2ban-client get <jail> actions viser det.

Hvem andre står på samme liste

fail2ban har ingen skyld her og er ikke noe enestående tilfelle. Det farlige er kombinasjonen NOPASSWD pluss stjerne. Hva vi fant ved siden av på våre egne servere:

  • journalctl * — journalen åpnes via en pager, og fra en pager starter man et skall. Regelen ser ut til å handle om å lese logger; i praksis er den et rootskall. Botemiddelet er ikke et smalere mønster, men gruppen systemd-journal: da virker journalctl helt uten sudo;
  • grep * /var/log/fail2ban.log — første argument til grep er mønsteret, men stjernen gjør det mulig å oppgi også en annen filbane, og grep kjører som root. Å lese /etc/shadow med den regelen er én kommando. Erstatningen er den samme: gruppen adm for å lese logger;
  • ufw --force * og nakne ufw allow/deny/delete * — webprosessen kan slå av brannmuren i sin helhet. Særlig ubehagelig: i begge tilfeller brukte panelet ikke disse reglene i det hele tatt, døde rettigheter etter tidlige versjoner av installasjonsprogrammet;
  • lynis-scan.sh * — et omslag som sender "$@" videre til en rootprosess, er likeverdig med en regel uten noen begrensninger.

Slik ser du hva du har

Det som skal ses på, er ikke filen, men de faktiske rettighetene til én bestemt bruker — den som PHP-FPM-poolen virkelig kjører som (ikke alltid www-data; på en av maskinene våre kjørte panelet som admin):

ps -o user= -C php-fpm | sort -u
sudo -l -U www-data
sudo -l -U admin

Deretter selve fillisten, og her er det to feller som har kostet oss 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 oss lå den farlige linjen både i /etc/sudoers.d/monitor og i hovedfilen /etc/sudoers — i den siste i omtrent åtte kopier, samlet opp av forskjellige versjoner av installasjonsprogrammet. Å rydde ett sted og slå seg til ro er den vanlige måten å la hullet stå åpent.

Den andre: sudo overser filer som har et punktum i navnet. En fil www-data.bak3 i /etc/sudoers.d/ ser ut som en gjeldende regel og leses som en gjeldende regel, men har ingen virkning. Dette slår i begge retninger: «regelen finnes, rettighetene ikke», og den falske tryggheten i en konfigurasjonskopi som «ligger rett ved siden av».

Hva man erstatter med

Å gjøre mønsteret smalere nytter ikke — en stjerne hvor som helst på linjen setter oppgaven tilbake til start. Bare én framgangsmåte holder: et rootomslag som tar faste posisjonsargumenter, validerer dem selv og kaller programmet uten den minste mulighet til å legge noe til.

#!/bin/sh
# /usr/local/sbin/arciveo-f2b — ban|unban <jail> <ip>, og ingenting mer
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 eies av root, rettigheter 755, og — dette er ikke valgfritt — det må ikke være skrivbart fra weben, ellers er hele konstruksjonen meningsløs. I sudoers står bare det igjen:

www-data ALL=(root) NOPASSWD: /usr/local/sbin/arciveo-f2b

Argumenter listes ikke opp i regelen: skriptet kontrollerer dem selv, og posisjonene i det er faste, så det finnes ikke noe sted å snike inn en handlingstekst. Regler som bare leser — statuser, ss, ipset list — går på egne linjer, også uten stjerne der det er mulig.

Særskilt om å erstatte sudo med grupper. Framgangsmåten er riktig: systemd-journal for journalen og adm for loggene blir billigere og sikrere enn noen sudo-regel. Men å trekke webbrukeren ut av grupper i blinde går ikke. Vi fjernet www-data fra én gruppe på en panelvert og fikk 403 på alle nettstedene på én gang: Apache kjører der som www-data, filene til nettstedene tilhører en annen bruker, og lesetilgangen kom nettopp av det gruppemedlemskapet. Gjenopprettingen krevde en fullstendig omstart — ikke en reload — av Apache og PHP-FPM, fordi gamle arbeidsprosesser beholder sitt tidligere sett av grupper og gir «det virker, så kommer 403».

Kontroll etter endringen

Før en sudoers-fil byttes ut, må syntaksen kontrolleres — ellers kan man stå helt uten sudo:

sudo visudo -cf /etc/sudoers.d/monitor

Etterpå skal det bekreftes at den gamle veien er død, og det i navnet til nettopp den brukeren:

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 kall til omslaget med søppel i stedet for en adresse skal gi invalid ip. Her lurer en egen felle: sudo -v mellomlagrer bekreftelsen i femten minutter, og etter det går enhver kontroll med sudo -n gjennom «med hell». Nøyaktig slik bekreftet vi en gang en regel som ikke fantes på serveren i det hele tatt. Derfor er sudo -k før kontrollen påkrevd.

Hva observasjonen er til for

Sudo-regler endres sjelden, men de endres lydløst: installasjonsprogrammet skriver til, hostingpanelet føyer til sitt, en fjernet pakke lar linjene sine ligge igjen. En gjennomgang én gang lukker det som finnes i dag og sier ingenting om hva neste oppdatering bringer. Den praktiske slutningen er enkel: listen over hvem som kan bli root, hører til i synsfeltet, ved siden av resten av serverens tilstand, og ikke i hukommelsen etter en hendelse. Hvordan det ser ut satt sammen, viser demosidene nedenfor.