Egy kiszolgálón futó adminisztrációs felület szinte mindig ugyanabba a falba fut: a webes folyamatnak olyat kell tennie, amihez nincs joga. Címet tiltani, portot nyitni, naplót olvasni. A kézenfekvő válasz egyetlen szűk sort megadni a sudoers fájlban, és semmi többet:

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

A sor úgy olvasható, hogy „csak tiltás”. Valójában azt jelenti: „bármi, rootként”. Az alábbiakban arról lesz szó, miért alakul ez így, hogyan ellenőrizheti a saját kiszolgálóján öt perc alatt, és mivel váltják ki az ilyen szabályokat.

Mit tesz valójában a *

A döntő részlet: a sudo nem argumentumonként hasonlít össze, hanem a teljes parancssort, és a mintában szereplő * gond nélkül átlép a szóközökön — vagyis az argumentumhatárokon. A szabály közepén álló szó szerinti banip ezért nem korlátoz semmit: elég, ha a banip szó a parancsban valahol előfordul, előtte és utána pedig bármi átadható.

A következő lépés olyan programot találni, amelynek végrehajtandó parancs adható át. A mi esetünkben ez ott állt magában a szabályban. A fail2banban a tiltási művelet szövegként van megadva, és ez a szöveg menet közben újradefiniálható:

fail2ban-client get portscan actions
sudo fail2ban-client set portscan action nftables-multiport actionban "<parancs>" banip 192.0.2.10
sudo fail2ban-client set portscan banip 192.0.2.11

Az első hívás egy valódi művelet szövegét cseréli le, a második pedig elsüti a jailt — és a fail2ban, amely rootként fut, végrehajtja a becsempészettet. Ezt július 4-én tettük meg a saját kiszolgálónkon: a /root könyvtárban rootként jött létre egy fájl, vagyis a webes folyamat teljes jogot kapott a gépen egy szabályból, amely szűknek látszott.

Közben három olyan részlet derült ki, amely mások leírásaiból általában kimarad:

  • a -c kapcsolót a set előtt a sudo levágja: a mintában a set közvetlenül a programnévhez van tapasztva, és elé semmi nem szúrható be;
  • az addaction és az action a banip szó nélkül szintén nem megy át — de minden szükséges belefér egyetlen set … banip … parancsba, és ezzel a minta teljesül;
  • a művelet nevének valódinak kell lennie, különben nincs mit lecserélni: a fail2ban-client get <jail> actions mutatja meg.

Ki áll még ugyanezen a listán

A fail2ban itt nem hibás, és nem is egyedi eset. A veszélyes maga a NOPASSWD és a csillag párosítása. Amit mellette találtunk a saját kiszolgálóinkon:

  • journalctl * — a napló egy oldalazón keresztül nyílik meg, az oldalazóból pedig parancsértelmező indítható. A szabály látszatra a naplók olvasásáról szól; a gyakorlatban root-parancsértelmező. A megoldás nem szűkebb minta, hanem a systemd-journal csoport: akkor a journalctl egyáltalán sudo nélkül működik;
  • grep * /var/log/fail2ban.log — a grep első argumentuma a minta, de a csillag megengedi egy második útvonal megadását is, és a grep rootként fut. Az /etc/shadow elolvasása ezzel a szabállyal egyetlen parancs. A csere ugyanaz: az adm csoport a naplók olvasásához;
  • ufw --force * és a csupasz ufw allow/deny/delete * — a webes folyamat teljesen kikapcsolhatja a tűzfalat. Külön kellemetlen, hogy a felület egyik esetben sem használta ezeket a szabályokat: halott engedélyek maradtak a telepítő korai változataiból;
  • lynis-scan.sh * — egy burkoló, amely a "$@" tartalmát továbbadja egy root folyamatnak, felér egy mindenféle korlátozás nélküli szabállyal.

Hogyan nézhető meg, mi van önnél

Nem a fájlt kell megnézni, hanem egy konkrét felhasználó tényleges jogait — azét, amelyikkel a PHP-FPM készlet valóban fut (nem mindig www-data; az egyik gépünkön a felület admin néven futott):

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

Utána magát a fájllistát, és itt két csapda van, amely időnkbe került:

sudo grep -rn 'NOPASSWD' /etc/sudoers /etc/sudoers.d/
ls -la /etc/sudoers.d/

Az első: a szabályok egyszerre két helyen élnek. Nálunk a veszélyes sor a /etc/sudoers.d/monitor fájlban és a fő /etc/sudoers fájlban is ott volt — az utóbbiban körülbelül nyolc példányban, amelyeket a telepítő különböző változatai halmoztak fel. Egy helyet kitakarítani és megnyugodni a szokásos módja annak, hogy a rés nyitva maradjon.

A második: a sudo figyelmen kívül hagyja azokat a fájlokat, amelyek nevében pont van. A www-data.bak3 fájl a /etc/sudoers.d/ könyvtárban érvényes szabálynak látszik és érvényes szabályként olvasható, de nincs hatása. Ez két irányban működik: „a szabály megvan, a jog nincs”, illetve az a hamis megnyugvás, hogy a konfiguráció egy példánya „ott van mellette”.

Mivel váltható ki

A minta szűkítése hasztalan — egy csillag a sor bármely pontján visszateszi a feladatot a kezdetre. Egyetlen megközelítés állja meg a helyét: egy root burkoló, amely kötött helyzetű argumentumokat vesz át, maga ellenőrzi őket, és úgy hívja meg a programot, hogy semmit ne lehessen hozzáfűzni.

#!/bin/sh
# /usr/local/sbin/arciveo-f2b — ban|unban <jail> <ip>, és semmi más
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

A burkoló root tulajdonában van, jogai 755, és — ez nem opcionális — a webről nem lehet írható, különben az egész szerkezet értelmét veszti. A sudoers fájlban csak ez marad:

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

Az argumentumokat a szabály nem sorolja fel: azokat maga a szkript ellenőrzi, és benne a helyek kötöttek, így nincs hová becsúsztatni egy műveletszöveget. A csak olvasó szabályok — állapotok, ss, ipset list — külön sorokba kerülnek, szintén csillag nélkül, ahol ez lehetséges.

Külön a sudo csoportokkal való kiváltásáról. A megközelítés helyes: a systemd-journal a naplóhoz és az adm a naplófájlokhoz kevesebbe kerül és biztonságosabb bármely sudo-szabálynál. De a webes felhasználót vaktában kivenni a csoportokból nem lehet. Egy paneles gépen kivettük a www-data felhasználót az egyik csoportból, és egyszerre 403-at kaptunk minden oldalon: az Apache ott www-data néven fut, az oldalak fájljai más felhasználóhoz tartoznak, és az olvasási hozzáférést épp az a csoporttagság adta. A visszaállítás az Apache és a PHP-FPM teljes újraindítását igényelte — nem újratöltést —, mert a régi feldolgozók megtartják a korábbi csoportkészletüket, és azt az állapotot hozzák létre, hogy „működik, aztán 403”.

Ellenőrzés a változtatás után

Egy sudoers fájl kicserélése előtt ellenőrizni kell a szintaxisát — különben sudo nélkül maradhat az ember:

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

Utána meg kell erősíteni, hogy a régi út halott, és éppen annak a felhasználónak a nevében:

sudo -k
sudo -u www-data sudo -n fail2ban-client set portscan banip 192.0.2.10

A válasz a password is required legyen, míg a burkoló meghívása cím helyett szeméttel invalid ip választ adjon. Itt saját csapda leselkedik: a sudo -v tizenöt percre gyorsítótárba teszi a megerősítést, és utána bármely sudo -n ellenőrzés „sikeresen” lefut. Pontosan így erősítettünk meg egyszer egy szabályt, amely a kiszolgálón egyáltalán nem volt ott. Ezért a sudo -k az ellenőrzés előtt kötelező.

Mire jó a megfigyelés

A sudo-szabályok ritkán változnak, de hangtalanul: a telepítő hozzáír, a hoszting felülete a magáét fűzi hozzá, egy eltávolított csomag ott hagyja a sorait. Az egyszeri átvizsgálás bezárja azt, ami ma van, és semmit nem mond arról, mit hoz a következő frissítés. A gyakorlati következtetés egyszerű: annak a listája, ki válhat roottá, a kiszolgáló többi állapota mellett, szem előtt a helye — nem pedig az incidens utáni emlékezetben. Hogy mindez összeállítva hogyan néz ki, azt az alábbi bemutatóoldalak mutatják meg.