Административният панел на сървъра почти винаги опира до едно и също: уеб процесът трябва да направи нещо, за което няма права. Да блокира адрес, да отвори порт, да прочете журнала. Отговорът, който сам се налага, е да се даде един тесен ред в sudoers и нищо повече:
www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client set * banip *
Редът се чете като „само блокиране“. В действителност означава „каквото и да е, като root“. По-долу е защо излиза така, как се проверява това на собствен сървър за пет минути и с какво се заменят такива правила.
Какво прави наистина *
Ключовата подробност: sudo сравнява не аргументите един по един, а целия команден ред, а * в шаблона спокойно преминава през интервалите — тоест през границите на аргументите. Буквалното banip, стоящо в средата на правилото, затова не ограничава нищо: достатъчно е думата banip да се появи някъде в командата, а преди нея и след нея може да се подаде каквото и да било.
По-нататък се търси програма, на която може да се подаде команда за изпълнение. В нашия случай тя беше направо в самото правило. При fail2ban действието за блокиране се задава като текст и този текст може да се препише в движение:
fail2ban-client get portscan actions
sudo fail2ban-client set portscan action nftables-multiport actionban "<команда>" banip 192.0.2.10
sudo fail2ban-client set portscan banip 192.0.2.11
Първото извикване подменя текста на действието при истинско действие, второто принуждава jail-а да се задейства — и fail2ban, който работи като root, изпълнява подхвърленото. Направихме това на 4 юли на собствения си сървър: файл в /root се създаде с права на root, тоест уеб процесът получи пълни права върху машината от правило, което изглеждаше тясно.
По пътя излязоха три подробности, които в чуждите разбори обикновено липсват:
- ключът
-cпредиsetsudo отрязва: в шаблонаsetе прилепен направо към името на изпълнимия файл и пред него нищо не може да се вмъкне; addactionиactionбез думатаbanipсъщо не минават — но всичко необходимо се вмества в една командаset … banip …, и шаблонът е изпълнен;- името на действието трябва да е истинско, иначе няма какво да се подменя: показва го
fail2ban-client get <jail> actions.
Кой още стои в същия списък
fail2ban тук не е виновен и не е изключение. Опасно е самото съчетание NOPASSWD плюс звездичка. Какво намерихме до него на собствените си сървъри:
journalctl *— журналът се отваря през страничник, а от страничника се пуска обвивка. Правилото на вид е за четене на дневници, на практика е root обвивка. Лекува се не със по-тесен шаблон, а с групатаsystemd-journal: тогаваjournalctlработи изобщо без sudo;grep * /var/log/fail2ban.log— първият аргумент наgrepе шаблонът, но звездичката позволява да се подаде и втори път, а grep се изпълнява като root. Да се прочете/etc/shadowс това правило е една команда. Замяната е същата: групатаadmза четене на дневници;ufw --force *и голитеufw allow/deny/delete *— уеб процесът може да изключи защитната стена напълно. Особено неприятно е, че в двата случая панелът изобщо не използваше тези правила: мъртви разрешения, останали от ранни версии на инсталатора;lynis-scan.sh *— обвивка, която подава"$@"на root процес, е равносилна на правило без никакви ограничения.
Как да видите какво има у вас
Гледа се не файлът, а действителните права на конкретен потребител — този, под който наистина работи пулът на PHP-FPM (не винаги www-data; на една от нашите машини панелът работеше като admin):
ps -o user= -C php-fpm | sort -u
sudo -l -U www-data
sudo -l -U admin
По-нататък самият списък с файлове, и тук има два капана, които ни отнеха време:
sudo grep -rn 'NOPASSWD' /etc/sudoers /etc/sudoers.d/
ls -la /etc/sudoers.d/
Първият: правилата живеят на две места едновременно. У нас опасният ред беше и в /etc/sudoers.d/monitor, и в главния /etc/sudoers — в главния в около осем копия, натрупани от различни версии на инсталатора. Да се изчисти едно място и да се успокоиш е обичайният начин дупката да остане отворена.
Вторият: sudo пренебрегва файловете, в чието име има точка. Файл www-data.bak3 в /etc/sudoers.d/ изглежда като действащо правило и се чете като действащо правило, но не действа. Това работи в двете посоки: „правилото го има, правата — не“ и лъжливото спокойствие от това, че копие на конфигурацията „лежи точно до него“.
С какво да се замени
Да се стеснява шаблонът е безполезно — звездичка на което и да е място в реда връща задачата в началото. Издържа един подход: root обвивка, която приема фиксирани позиционни аргументи, сама ги проверява и извиква програмата без никаква възможност да се допише нещо.
#!/bin/sh
# /usr/local/sbin/arciveo-f2b — ban|unban <jail> <ip>, и нищо повече
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
Обвивката е собственост на root, права 755, и — това не е по избор — не трябва да е записваема от уеб, иначе цялата конструкция е безсмислена. В sudoers остава само тя:
www-data ALL=(root) NOPASSWD: /usr/local/sbin/arciveo-f2b
Аргументите не се изброяват в правилото: проверява ги самият скрипт, а позициите в него са фиксирани, така че няма къде да се пъхне текст на действие. Правилата само за четене — състояния, ss, ipset list — отиват на отделни редове, също без звездичка там, където е възможно.
Отделно за замяната на sudo с групи. Подходът е правилен: systemd-journal за журнала и adm за дневниците излизат по-евтино и по-безопасно от всяко правило на sudo. Но да се изважда уеб потребителят от групи наслуки не може. Извадихме www-data от една група на хост с панел и веднага получихме 403 на всички сайтове: Apache работи там като www-data, файловете на сайтовете принадлежат на друг потребител, а достъпът за четене идваше именно от това членство в групата. Възстановяването изиска пълно рестартиране — не презареждане — на Apache и PHP-FPM, защото старите работни процеси запазват предишния си набор от групи и създават състоянието „работи, а после 403“.
Проверка след промяната
Преди подмяната на файл sudoers синтаксисът му трябва да се провери — иначе може да се остане напълно без sudo:
sudo visudo -cf /etc/sudoers.d/monitor
След това да се потвърди, че старият вектор е мъртъв, и то от името точно на този потребител:
sudo -k
sudo -u www-data sudo -n fail2ban-client set portscan banip 192.0.2.10
Отговорът трябва да бъде a password is required, докато извикването на обвивката с боклук вместо адрес трябва да даде invalid ip. Тук дебне собствен капан: sudo -v кешира потвърждението за петнайсет минути, а след него всяка проверка с sudo -n минава „успешно“. Точно така веднъж потвърдихме правило, което на сървъра изобщо го нямаше. Затова sudo -k преди проверката е задължително.
Смисълът на наблюдението
Правилата на sudo се менят рядко, но се менят безшумно: инсталаторът дописва, панелът на хостинга добавя свои, премахнат пакет оставя след себе си редовете си. Еднократната ревизия затваря това, което е днес, и не казва нищо за това, което ще донесе следващото обновяване. Практическият извод е прост: списъкът на тези, които могат да станат root, стои по-добре пред очите, до останалото състояние на сървъра, отколкото в спомена след инцидент. Как изглежда всичко това събрано, показват демо страниците по-долу.