Administračný panel na serveri naráža takmer vždy na tú istú stenu: webový proces potrebuje urobiť niečo, na čo nemá práva. Zablokovať adresu, otvoriť port, prečítať žurnál. Odpoveď, ktorá sa núka, je udeliť jeden úzky riadok v sudoers a nič viac:

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

Riadok sa číta ako „iba blokovať“. V skutočnosti znamená „čokoľvek, ako root“. Nižšie je, prečo to tak vychádza, ako si to na vlastnom serveri overiť za päť minút a čím sa také pravidlá nahrádzajú.

Čo * naozaj robí

Kľúčový detail: sudo neporovnáva jednotlivé argumenty, ale celý príkazový riadok, a * vo vzore bez problémov prechádza cez medzery — teda cez hranice argumentov. Literál banip stojaci v strede pravidla preto nič neobmedzuje: postačí, aby sa slovo banip v príkaze niekde vyskytlo, a pred ním aj za ním možno odovzdať čokoľvek.

Ďalej sa hľadá program, ktorému možno odovzdať príkaz na vykonanie. V našom prípade stál priamo v pravidle. Vo fail2ban je blokovacia akcia zapísaná ako text a tento text možno predefinovať počas chodu:

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

Prvé volanie podmení text akcie u skutočnej akcie, druhé prinúti jail zasiahnuť — a fail2ban, ktorý beží pod rootom, vykoná podstrčené. Urobili sme to 4. júla na vlastnom serveri: súbor v /root vznikol pod rootom, teda webový proces získal na stroji plné práva z pravidla, ktoré vyzeralo úzko.

Popri tom vyšli na povrch tri podrobnosti, ktoré v cudzích rozboroch obvykle chýbajú:

  • prepínač -c pred set sudo odstrihne: vo vzore je set prilepený priamo k názvu binárky a pred neho nemožno nič vložiť;
  • addaction a action bez slova banip takisto neprejdú — ale všetko potrebné sa zmestí do jediného príkazu set … banip …, a vzor je tým splnený;
  • názov akcie musí byť skutočný, inak nie je čo podmieňať: zobrazí ho fail2ban-client get <jail> actions.

Kto ďalší stojí na tom istom zozname

fail2ban tu nie je vinný ani výnimočný. Nebezpečná je sama kombinácia NOPASSWD a hviezdičky. Čo sme našli hneď vedľa na vlastných serveroch:

  • journalctl * — žurnál sa otvára cez pager a z pageru sa spúšťa shell. Pravidlo na pohľad vyzerá na čítanie logov, v praxi je to rootovský shell. Liekom nie je užší vzor, ale skupina systemd-journal: potom journalctl funguje úplne bez sudo;
  • grep * /var/log/fail2ban.log — prvým argumentom grep je vzor, ale hviezdička dovolí podať aj druhú cestu a grep sa vykonáva pod rootom. Prečítať /etc/shadow týmto pravidlom je jediný príkaz. Nahradenie je rovnaké: skupina adm na čítanie logov;
  • ufw --force * a holé ufw allow/deny/delete * — webový proces môže vypnúť firewall celý. Zvlášť nepríjemné: v oboch prípadoch panel tieto pravidlá vôbec nepoužíval, boli to mŕtve oprávnenia po ranných verziách inštalátora;
  • lynis-scan.sh * — obal, ktorý posúva "$@" do rootovského procesu, sa vyrovná pravidlu bez akýchkoľvek obmedzení.

Ako sa pozrieť, čo máte

Pozerať sa netreba na súbor, ale na skutočné práva konkrétneho používateľa — toho, pod ktorým naozaj beží pool PHP-FPM (nie vždy www-data; na jednom našom stroji panel bežal ako admin):

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

Ďalej samotný zoznam súborov, a tu sú dve pasti, na ktorých sme stratili čas:

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

Prvá: pravidlá žijú na dvoch miestach naraz. U nás bol nebezpečný riadok aj v /etc/sudoers.d/monitor, aj v hlavnom /etc/sudoers — v hlavnom v asi ôsmich kópiách, nazbieraných rôznymi verziami inštalátora. Vyčistiť jedno miesto a uspokojiť sa je bežný spôsob, ako nechať dieru otvorenú.

Druhá: sudo ignoruje súbory, ktoré majú v názve bodku. Súbor www-data.bak3 v /etc/sudoers.d/ vyzerá ako platné pravidlo a číta sa ako platné pravidlo, ale nemá žiadny účinok. Funguje to na obe strany: „pravidlo je, práva nie“ a falošný pokoj z toho, že kópia konfigurácie „leží hneď vedľa“.

Čím nahradiť

Zužovať vzor je bez úžitku — hviezdička kdekoľvek v riadku vracia úlohu na začiatok. Obstojí jediný postup: rootovský obal, ktorý prijíma pevné pozičné argumenty, sám ich overí a zavolá program bez najmenšej možnosti niečo dopísať.

#!/bin/sh
# /usr/local/sbin/arciveo-f2b — ban|unban <jail> <ip>, a nič viac
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

Obal patrí rootovi, práva 755, a — to nie je voliteľné — nesmie byť zapisovateľný z webu, inak je celá konštrukcia bezpredmetná. V sudoers zostane len on:

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

Argumenty sa v pravidle nevypisujú: overuje ich sám skript a pozície v ňom sú pevné, takže niet kam vsunúť text akcie. Pravidlá len na čítanie — stavy, ss, ipset list — idú na samostatné riadky, takisto bez hviezdičky tam, kde to ide.

Osobitne k nahradeniu sudo skupinami. Postup je správny: systemd-journal na žurnál a adm na logy vyjdú lacnejšie a bezpečnejšie než akékoľvek pravidlo sudo. Ale vytahovať webového používateľa zo skupín naslepo nemožno. Odobrali sme www-data z jednej skupiny na panelovom stroji a hneď sme dostali 403 na všetkých weboch: Apache tam beží ako www-data, súbory webov patria inému používateľovi a prístup na čítanie dávalo práve to členstvo v skupine. Obnova si vyžiadala úplný restart — nie reload — Apache a PHP-FPM, pretože starí workeri si držia predošlú sadu skupín a vytvárajú stav „funguje to, a potom 403“.

Kontrola po úprave

Pred výmenou súboru sudoers treba skontrolovať jeho syntax — inak možno zostať úplne bez sudo:

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

Potom potvrdiť, že starý vektor je mŕtvy, a to menom práve toho používateľa:

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

Odpoveď má byť a password is required, kým zavolanie obalu s nezmyslom namiesto adresy má dať invalid ip. Číha tu vlastná pasť: sudo -v uloží potvrdenie do vyrovnávacej pamäti na pätnásť minút a po ňom každá kontrola pomocou sudo -n prejde „úspešne“. Práve tak sme raz potvrdili pravidlo, ktoré na serveri vôbec nebolo. Preto je sudo -k pred kontrolou povinné.

Na čo je sledovanie

Pravidlá sudo sa menia zriedka, ale menia sa nehlučne: inštalátor k nim dopisuje, panel hostingu pridáva svoje, odobraný balík po sebe necháva vlastné riadky. Jednorazová revízia zavrie to, čo je dnes, a nehovorí nič o tom, čo prinesie nasledujúca aktualizácia. Praktický záver je jednoduchý: zoznam tých, ktorí sa môžu stať rootom, patrí na dohľad vedľa ostatného stavu servera, a nie do spomienky po incidente. Ako to vyzerá poskladané dokopy, ukazujú ukážkové stránky nižšie.