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č -c před set sudo odřízne: ve vzoru je set přilepený přímo k názvu binárky a před něj nelze nic vložit;
  • addaction a action bez slova banip také neprojdou — ale vše potřebné se vejde do jediného příkazu set … 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 skupina systemd-journal: pak journalctl funguje úplně bez sudo;
  • grep * /var/log/fail2ban.log — prvním argumentem grep je vzor, ale hvězdička dovolí podat i druhou cestu a grep se vykonává pod rootem. Přečíst /etc/shadow tímto pravidlem je jeden příkaz. Náhrada je stejná: skupina adm na č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.