Administrační panel na serveru naráží téměř vždy na stejnou zeď: webový proces potřebuje udělat něco, na co nemá práva. Zablokovat adresu, otevřít port, přečíst žurnál. Odpověď, která se nabízí, je udělit jeden úzký řádek v sudoers a nic víc:
www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client set * banip *
Řádek se čte jako „jen blokovat“. Ve skutečnosti znamená „cokoli, jako root“. Níže je, proč to tak vychází, jak si to na vlastním serveru zkontrolovat za pět minut a čím se taková pravidla nahrazují.
Co * skutečně dělá
Klíčový detail: sudo neporovnává jednotlivé argumenty, ale celý příkazový řádek, a * ve vzoru bez problémů přechází přes mezery — tedy přes hranice argumentů. Literál banip stojící uprostřed pravidla proto nic neomezuje: stačí, aby se slovo banip v příkazu někde objevilo, a před ním i za ním lze předat cokoli.
Dál se hledá program, kterému lze předat příkaz k vykonání. V našem případě stál přímo v pravidle. Ve fail2ban je blokační akce zapsána jako text a tento text lze předefinovat za běhu:
fail2ban-client get portscan actions
sudo fail2ban-client set portscan action nftables-multiport actionban "<příkaz>" banip 192.0.2.10
sudo fail2ban-client set portscan banip 192.0.2.11
První volání podmění text akce u skutečné akce, druhé přinutí jail zasáhnout — a fail2ban, běžící pod rootem, vykoná podstrčené. Udělali jsme to 4. července na vlastním serveru: soubor v /root vznikl pod rootem, tedy webový proces získal na stroji plná práva z pravidla, které vypadalo úzce.
Po cestě vyšly na povrch tři podrobnosti, které v cizích rozborech obvykle chybí:
- přepínač
-cpředsetsudo odřízne: ve vzoru jesetpřilepený přímo k názvu binárky a před něj nelze nic vložit; addactionaactionbez slovabaniptaké neprojdou — ale vše potřebné se vejde do jediného příkazuset … banip …, a vzor je tím splněn;- název akce musí být skutečný, jinak není co podměňovat: zobrazí ho
fail2ban-client get <jail> actions.
Kdo další stojí na témže seznamu
fail2ban tu není vinen ani výjimečný. Nebezpečná je sama kombinace NOPASSWD a hvězdičky. Co jsme našli hned vedle na vlastních serverech:
journalctl *— žurnál se otevírá přes pager a z pageru se spouští shell. Pravidlo na pohled vypadá na čtení logů, v praxi je to rootovský shell. Lékem není užší vzor, ale skupinasystemd-journal: pakjournalctlfunguje úplně bez sudo;grep * /var/log/fail2ban.log— prvním argumentemgrepje vzor, ale hvězdička dovolí podat i druhou cestu a grep se vykonává pod rootem. Přečíst/etc/shadowtímto pravidlem je jeden příkaz. Náhrada je stejná: skupinaadmna čtení logů;ufw --force *a holéufw allow/deny/delete *— webový proces může vypnout firewall celý. Zvlášť nepříjemné: v obou případech panel tato pravidla vůbec nepoužíval, byla to mrtvá oprávnění po raných verzích instalátoru;lynis-scan.sh *— obálka, která předává"$@"do rootovského procesu, se vyrovná pravidlu bez jakýchkoli omezení.
Jak se podívat, co máte
Dívat se nemá na soubor, ale na skutečná práva konkrétního uživatele — toho, pod kterým opravdu běží pool PHP-FPM (ne vždy www-data; na jednom našem stroji panel běžel jako admin):
ps -o user= -C php-fpm | sort -u
sudo -l -U www-data
sudo -l -U admin
Dál samotný seznam souborů, a tady jsou dvě pasti, na kterých jsme tratili čas:
sudo grep -rn 'NOPASSWD' /etc/sudoers /etc/sudoers.d/
ls -la /etc/sudoers.d/
První: pravidla žijí na dvou místech zároveň. U nás byl nebezpečný řádek jak v /etc/sudoers.d/monitor, tak v hlavním /etc/sudoers — v hlavním v asi osmi kopiích, nasbíraných různými verzemi instalátoru. Uklidit jedno místo a uklidnit se je běžný způsob, jak nechat díru otevřenou.
Druhá: sudo ignoruje soubory, které mají v názvu tečku. Soubor www-data.bak3 v /etc/sudoers.d/ vypadá jako platné pravidlo a čte se jako platné pravidlo, ale nemá žádný účinek. Funguje to na obě strany: „pravidlo je, práva nejsou“ a falešný klid z toho, že kopie konfigurace „leží hned vedle“.
Čím nahradit
Zužovat vzor je bez užitku — hvězdička kdekoli na řádku vrací úlohu na začátek. Obstojí jediný postup: rootovská obálka, která přijímá pevné poziční argumenty, sama je ověří a zavolá program bez nejmenší možnosti něco dopsat.
#!/bin/sh
# /usr/local/sbin/arciveo-f2b — ban|unban <jail> <ip>, a nic víc
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
Obálka patří rootovi, práva 755, a — to není volitelné — nesmí být zapisovatelná z webu, jinak je celá konstrukce bezpředmětná. V sudoers zůstane jen ona:
www-data ALL=(root) NOPASSWD: /usr/local/sbin/arciveo-f2b
Argumenty se v pravidle nevypisují: ověřuje je sám skript a pozice v něm jsou pevné, takže není kam vsunout text akce. Pravidla jen pro čtení — stavy, ss, ipset list — jdou na samostatné řádky, také bez hvězdičky, kde to jde.
Zvlášť k nahrazení sudo skupinami. Postup je správný: systemd-journal na žurnál a adm na logy vyjdou levněji a bezpečněji než jakékoli pravidlo sudo. Ale vytahovat webového uživatele ze skupin naslepo nelze. Odebrali jsme www-data z jedné skupiny na panelovém stroji a rázem dostali 403 na všech webech: Apache tam běží jako www-data, soubory webů patří jinému uživateli a přístup ke čtení dávalo právě to členství ve skupině. Obnova vyžadovala úplný restart — ne reload — Apache a PHP-FPM, protože staří workeři si drží dřívější sadu skupin a vytvářejí stav „funguje to, a pak 403“.
Kontrola po úpravě
Před výměnou souboru sudoers je třeba zkontrolovat jeho syntaxi — jinak lze zůstat úplně bez sudo:
sudo visudo -cf /etc/sudoers.d/monitor
Potom potvrdit, že starý vektor je mrtvý, a to jménem právě toho uživatele:
sudo -k
sudo -u www-data sudo -n fail2ban-client set portscan banip 192.0.2.10
Odpověď má být a password is required, zatímco zavolání obálky s nesmyslem místo adresy má dát invalid ip. Číhá tu vlastní past: sudo -v uloží potvrzení do mezipaměti na patnáct minut a po něm každá kontrola pomocí sudo -n projde „úspěšně“. Právě tak jsme jednou potvrdili pravidlo, které na serveru vůbec nebylo. Proto je sudo -k před kontrolou povinné.
K čemu je sledování
Pravidla sudo se mění zřídka, ale mění se neslyšně: instalátor k nim dopisuje, panel hostingu přidává své, odebraný balík po sobě nechává vlastní řádky. Jednorázová revize zavře to, co je dnes, a neříká nic o tom, co přinese příští aktualizace. Praktický závěr je jednoduchý: seznam těch, kdo se mohou stát rootem, patří na dohled vedle ostatního stavu serveru, a ne do vzpomínky po incidentu. Jak to vypadá poskládané dohromady, ukazují ukázkové stránky níže.