Palvelimella oleva hallintapaneeli törmää lähes aina samaan seinään: verkkoprosessin pitää tehdä jotain, mihin sillä ei ole oikeuksia. Estää osoite, avata portti, lukea järjestelmäloki. Lähimpänä oleva vastaus on myöntää yksi kapea rivi sudoers-tiedostossa, eikä mitään muuta:

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

Rivi lukeutuu muodossa ”vain estäminen”. Todellisuudessa se tarkoittaa ”mitä tahansa, rootina”. Seuraavassa: miksi niin käy, miten sen tarkistaa omalla palvelimellaan viidessä minuutissa ja millä tällaiset säännöt korvataan.

Mitä * oikeasti tekee

Ratkaiseva yksityiskohta: sudo ei vertaa argumentteja yksitellen vaan komentoriviä kokonaisuutena, ja * kuviossa ylittää vaivatta välilyönnit — eli argumenttien rajat. Säännön keskellä oleva kirjaimellinen banip ei siksi rajoita mitään: riittää, että sana banip esiintyy jossain komennossa, ja sen edelle kuin jälkeenkin voi välittää mitä tahansa.

Seuraavaksi etsitään ohjelma, jolle voi antaa suoritettavan komennon. Meidän tapauksessa se oli itse säännössä. Fail2banissa estotoimenpide määritellään tekstinä, ja tämä teksti voidaan määritellä uudelleen ajon aikana:

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

Ensimmäinen kutsu korvaa todellisen toimenpiteen tekstin, toinen laukaisee jailin — ja fail2ban, joka ajetaan rootina, suorittaa sijoitetun. Teimme tämän 4. heinäkuuta omalla palvelimellamme: tiedosto hakemistoon /root syntyi rootin oikeuksilla, eli verkkoprosessi sai koneeseen täydet oikeudet säännöstä, joka näytti kapealta.

Matkalla tuli esiin kolme yksityiskohtaa, jotka muiden selostuksista yleensä puuttuvat:

  • valitsimen -c ennen sanaa set sudo leikkaa pois: kuviossa set on kiinni ohjelman nimessä, eikä sen eteen voi sijoittaa mitään;
  • addaction ja action ilman sanaa banip eivät myöskään läpäise — mutta kaikki tarvittava mahtuu yhteen komentoon set … banip …, ja näin kuvio täyttyy;
  • toimenpiteen nimen on oltava todellinen, muuten korvattavaa ei ole: sen näyttää fail2ban-client get <jail> actions.

Kuka muu on samalla listalla

Fail2ban ei ole tässä syyllinen eikä poikkeus. Vaarallista on yhdistelmä NOPASSWD ja tähti. Mitä löysimme sen vierestä omilta palvelimiltamme:

  • journalctl * — loki avautuu sivuttajan kautta, ja sivuttajasta käynnistetään komentotulkki. Sääntö näyttää koskevan lokien lukemista; käytännössä se on root-tulkki. Hoito ei ole kapeampi kuvio vaan ryhmä systemd-journal: silloin journalctl toimii kokonaan ilman sudoa;
  • grep * /var/log/fail2ban.log — grep-komennon ensimmäinen argumentti on kuvio, mutta tähti antaa syöttää myös toisen polun, ja grep ajetaan rootina. /etc/shadow-tiedoston lukeminen tällä säännöllä on yksi komento. Korvaus on sama: ryhmä adm lokien lukemiseen;
  • ufw --force * ja paljaat ufw allow/deny/delete * — verkkoprosessi voi sammuttaa palomuurin kokonaan. Erityisen kiusallista: molemmissa tapauksissa paneeli ei käyttänyt näitä sääntöjä lainkaan, vaan kyse oli kuolleista oikeuksista asennusohjelman varhaisista versioista;
  • lynis-scan.sh * — kääre, joka välittää "$@" root-prosessille, vastaa sääntöä ilman mitään rajoituksia.

Näin näet, mitä sinulla on

Katsottava kohde ei ole tiedosto vaan yhden tietyn käyttäjän todelliset oikeudet — sen, jona PHP-FPM-varanto oikeasti ajetaan (ei aina www-data; yhdellä koneellamme paneeli toimi käyttäjänä admin):

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

Sitten itse tiedostolista, ja tässä on kaksi ansaa, jotka ovat meiltä viemässä aikaa:

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

Ensimmäinen: säännöt elävät kahdessa paikassa yhtä aikaa. Meillä vaarallinen rivi oli sekä tiedostossa /etc/sudoers.d/monitor että pääasiallisessa /etc/sudoers-tiedostossa — jälkimmäisessä noin kahdeksana kopiona, joita asennusohjelman eri versiot olivat kerryttäneet. Yhden paikan siivoaminen ja rauhoittuminen on tavallinen tapa jättää reikä auki.

Toinen: sudo ohittaa tiedostot, joiden nimessä on piste. Tiedosto www-data.bak3 hakemistossa /etc/sudoers.d/ näyttää voimassa olevalta säännöltä ja lukeutuu voimassa olevana sääntönä, mutta ei vaikuta mihinkään. Tämä toimii kahteen suuntaan: ”sääntö on, oikeuksia ei”, ja väärä rauha siitä, että asetustiedoston kopio ”on tuossa vieressä”.

Millä korvataan

Kuvion kapeuttaminen ei auta — tähti missä tahansa rivillä palauttaa tehtävän alkuun. Vain yksi tapa kestää: root-kääre, joka ottaa kiinteät paikkakohtaiset argumentit, tarkistaa ne itse ja kutsuu ohjelmaa ilman pienintäkään mahdollisuutta lisätä jotain.

#!/bin/sh
# /usr/local/sbin/arciveo-f2b — ban|unban <jail> <ip>, eikä mitään muuta
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

Kääre kuuluu rootille, oikeudet 755, ja — tämä ei ole valinnaista — se ei saa olla kirjoitettavissa verkosta, muuten koko rakennelma on turha. Sudoers-tiedostoon jää vain se:

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

Argumentteja ei luetella säännössä: ne tarkistaa itse skripti, ja sen sisällä paikat ovat kiinteät, joten toimenpiteen tekstiä ei ole mihin livauttaa. Pelkästään lukevat säännöt — tilatiedot, ss, ipset list — menevät omille riveilleen, myös ilman tähteä silloin kun se on mahdollista.

Erikseen sudon korvaamisesta ryhmillä. Lähestymistapa on oikea: systemd-journal järjestelmälokia varten ja adm lokitiedostoja varten tulevat halvemmiksi ja turvallisemmiksi kuin mikään sudo-sääntö. Mutta verkkokäyttäjää ei voi vetää ryhmistä ulos sokkona. Poistimme www-data-käyttäjän yhdestä ryhmästä paneelipalvelimella ja saimme kerralla 403-virheen kaikilla sivustoilla: Apache ajetaan siellä käyttäjänä www-data, sivustojen tiedostot kuuluvat toiselle käyttäjälle, ja lukuoikeuden antoi nimenomaan tuo ryhmäjäsenyys. Palautus vaati Apachen ja PHP-FPM:n täydellisen uudelleenkäynnistyksen — ei uudelleenlatausta — koska vanhat työprosessit säilyttävät aiemman ryhmäjoukkonsa ja tuottavat ilmiön ”toimii, sitten 403”.

Tarkistus muutoksen jälkeen

Ennen sudoers-tiedoston vaihtamista sen syntaksi on tarkistettava — muuten voi jäädä kokonaan ilman sudoa:

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

Sen jälkeen on varmistettava, että vanha reitti on kuollut, ja nimenomaan tuon käyttäjän nimissä:

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

Vastauksen pitää olla a password is required, kun taas käären kutsuminen roskalla osoitteen sijasta antaa invalid ip. Tässä vaanii oma ansansa: sudo -v tallentaa vahvistuksen välimuistiin viideksitoista minuutiksi, ja sen jälkeen mikä tahansa tarkistus komennolla sudo -n menee läpi ”onnistuneesti”. Juuri näin vahvistimme kerran säännön, jota palvelimella ei ollut lainkaan. Siksi sudo -k ennen tarkistusta on pakollinen.

Mihin seurantaa tarvitaan

Sudo-säännöt muuttuvat harvoin, mutta ne muuttuvat äänettömästi: asennusohjelma kirjoittaa niihin lisää, hosting-paneeli täydentää omillaan, poistettu paketti jättää rivinsä jälkeensä. Kertaluonteinen katselmus sulkee sen, mikä on tänään, eikä kerro mitään siitä, mitä seuraava päivitys tuo. Käytännön johtopäätös on yksinkertainen: luettelo siitä, kuka voi tulla rootiksi, kuuluu näkyville muun palvelimen tilan viereen eikä muistiin tapahtuman jälkeen. Miltä tämä näyttää koottuna, esittävät alla olevat demosivut.