Jak mé VPS pro někoho těžilo 135 dnů krypto

Dostupné v jazycích:

Crypto miner being sneaky

Před dvěma lety jsem napsal článek o tom, jak mi GitLab rozbil VPS. Dva roky? Datum jsem si musel zkontrolovat dvakrát, protože jsem myslel, že to bylo někdy loni na jaře. Evidentně ne - blog tu celou dobu tiše seděl a chytal prach, zatímco jsem se nedíval, což se, jak uvidíte, ukázalo být tak trochu hlavním tématem celého příběhu.

Každopádně. Ten článek jsem zakončil tím, že bych možná měl přejít na Giteu, Forgejo nebo něco jiného, ale že jsem dost líný a už jsem se upsal GitLabu, takže zatím může zůstat.

Ještě další dva roky jsem zůstal líný. Pak se mi jedno úterní ráno všechno zřítilo na hlavu najednou a do večera jsem migroval svůj Git server, vyhodil kryptoměnový miner, který od února bydlel na mém stroji, zjistil, že můj server za celý svůj život neměl firewall, a sám se z něj omylem zamkl. Ne nutně v tomto pořadí, co se týče míry trapnosti.

Začalo to, jako takové věci obvykle začínají, něčím naprosto banálním. Mám malý hobby projekt - PWA hru - a chtěl jsem pro ni .gitignore. To bylo všechno. To byl celý plán na ráno.

Projekt jsem pushnul do svého self-hostovaného GitLabu a ten mi oznámil "Deploy in progress. Please try again in a few minutes."

Dobře. Pár minut uběhlo. Pořád se nasazovalo. Pak se celé GUI GitLabu změnilo na "Deploy in progress" - každá stránka, každá URL. Nemohl jsem nic zrušit, protože ke zrušení čehokoli potřebujete GUI a právě to GUI bylo pryč.

Moje první teorie byla Auto DevOps. Jenže jakmile GitLab naběhl, Auto DevOps jsem vypnul, uložil změny a znovu dostal "Deploy in progress", aniž bych vůbec něco pushnul.

Tak jsem vytáhl logy a ty vyprávěly úplně jiný příběh. Puma - Rails server, který je webové rozhraní GitLabu - se restartovala každé tři až čtyři minuty. A každý start trval 116 sekund. Cyklus tedy vypadal takto: dvě minuty startování, jednu minutu běžet, nechat se zabít, opakovat. Stránka "Deploy in progress" nikdy nebyla žádný deploy. Byl to fallback Apache ErrorDocument 503, který mi poslušně zobrazoval údržbovou stránku pokaždé, když backend umřel. Což bylo v podstatě pořád.

Proč umíral? df a free na to odpověděly asi za sekundu. Disk na 96 procentech, z 30 GB volných 1,2 GB. RAM kompletně vyčerpaná: celkem 3,8 GB, volných 105 MB a swap na 100 procentech. Prostě už nic nezbylo. GitLab nebyl rozbitý, dusil se.

A tady jsem konečně přiznal věc, kterou jsem napsal už před dvěma lety a pak ji ignoroval: tohle VPS nesplňuje minimální požadavky GitLabu a žádné ladění počtu workerů Pumy to nezmění. Takže jsem udělal to, k čemu jsem byl před dvěma lety příliš líný. Gitea. Docker kontejner, SQLite, zhruba 150 MB RAM v klidovém stavu místo několika gigabajtů.

Migrace měla jeden pěkný háček. Abych dostal repozitáře z GitLabu ven, musel jsem nejdřív GitLab dostat do dostatečně zdravého stavu, aby mi je mohl předat, což odmítl udělat - každý clone přes HTTPS skončil chybou "remote: Nil JSON web token" a kódem 403. To není problém s přihlašovacími údaji, ale selhání komunikace mezi GitLab Workhorse a Gitaly, protože instance byla příliš nemocná na to, aby si dokázala podepsat vlastní tokeny. Můj Git server byl tak rozbitý, že mi nedokázal vydat můj vlastní zdrojový kód.

Tak jsem to obešel. GitLab ukládá repozitáře na disk pod adresářem @hashed, pojmenované hashem místo názvu projektu, což je skvělé pro GitLab a naprosto nepoužitelné pro člověka. Každý z nich ale má v konfiguraci gitu uloženou původní cestu, takže můžete projít adresář, každého repozitáře se zeptat, jak se sám jmenuje, a mapování si sestavit sami. Pak už stačí clone a push do Gitey. Všechny čtyři repozitáře se přenesly - zdrojový kód, branche i tagy - bez spolupráce GitLabu.

Přesměroval jsem Apache vhost z portu 8080 na Giteu na portu 3000, nechal stejnou adresu a potom GitLab kompletně odstranil. Vrátilo se pět gigabajtů. Využití disku kleslo z 96 na 80 procent.

Teď to běží jako po másle. Upřímně, měl jsem to udělat už v roce 2024.

Ale s pamětí bylo pořád něco špatně.

To mě štvalo. GitLab byl pryč - největší žrout prostředků na celém stroji, odinstalovaný, odstraněný i se svými datovými adresáři - a přesto jsem měl stále využito 3,5 GB z 3,8 GB a swap byl úplně plný. Tak jsem seřadil procesy podle využití paměti a tam to bylo, úplně nahoře:

   lmstudio 3365393 93.8 59.4 /dev/shm/2775695008

Volně přeloženo: nějaký uživatel spouštěl proces, který žral 94 procent CPU, využíval 59 procent veškeré RAM ve stroji a program, který běžel, byl soubor 2775695008 v /dev/shm.

LM Studio jsem na tomto serveru kdysi opravdu nainstaloval. Byl to experiment, jedno odpoledne jsem si s ním hrál a pak jsem na něj úplně zapomněl. Takže moje upřímná první reakce byla: "Já tu pořád mám spuštěné LM Studio?"

Neměl jsem. LM Studio jen sedělo na disku a nic nedělalo. To, co běželo, byl kryptoměnový miner.

Krypto těžař přistižen při činu

 

Na tom řádku je špatně úplně všechno, a je to špatně velmi specificky. /dev/shm je sdílená paměť - souborový systém, který žije v RAM. Nic legitimního se tam samo neinstaluje. Je zvolen právě proto, že tam není nic na disku, co by se dalo později skenovat nebo najít. Náhodný číselný název souboru je stejný princip. A 94 procent CPU na jinak nečinném serveru, který hostuje několik malých webů, má v zásadě jediné rozumné vysvětlení.

A nebyl tam sám. Byly tam tři části a tohle je ta část, která mi po překonání prvotního naštvání přišla opravdu zajímavá:

  • Samotný miner, ořezaný staticky linkovaný binární soubor v /dev/shm, se připojoval na 170.168.103.27 na portu 29205 a potichu vydělával peníze někomu cizímu.
  • Watchdog ukrytý v /home/lmstudio/.config/systemd/ pod názvem "journalctl" - 15MB falešná kopie skutečného systémového nástroje, jejímž jediným úkolem bylo zjistit, že miner zemřel, a okamžitě spustit nový. Běžel nepřetržitě 135 dní.
  • Záznam @reboot v crontabu uživatele lmstudio, takže kdyby se stroj někdy restartoval, celé by se to samo znovu spustilo.

Ten poslední bod je skoro vtipný. Nikdy se nespustil, protože server nebyl 1155 dní restartován. Tři roky uptime. Dřív jsem na to číslo býval docela hrdý.

Pořadí je při čištění důležité. Když jen zabijete miner, watchdog si toho během sekundy všimne a spustí další a vy pak můžete zabíjet minery donekonečna a divit se, proč prohráváte. Musíte watchdog i miner shodit společně, signálem, který nelze zachytit, potom odstranit payloady a nakonec cron záznam.

Protože celý účet lmstudio byl kompromitovaný a nepoužíval jsem ho, smazal jsem všechno - uživatele, domovský adresář, LM Studio, zkrátka úplně všechno. free -h pak ukázalo 2,4 GB dostupné paměti místo původních 105 MB. Tajemný tlak na paměť, který jsem celé dopoledne přičítal GitLabu, byl ve skutečnosti cizí těžební stroj běžící přibližně čtyři a půl měsíce.

Což mě přivádí ke vstupním dveřím, a tohle je skutečně trapná část, protože si za ni můžu úplně sám.

Účet lmstudio se nikdy nepřihlásil přes SSH - ani jednou, záznamy o přihlášení byly prázdné. Nikdo tedy neuhádl heslo. Stalo se to tak, že LM Studio 0.3.4 má síťový server, já ho nechal poslouchat na 0.0.0.0 místo na localhost a když mě experiment přestal zajímat, nechal jsem to tak. Starý pod-server, otevřený celému internetu, běžící pod vlastním uživatelem. Někdo ho kolem 21. února našel, získal pod tímto uživatelem možnost spouštět kód, nahrál miner a zabydlel se. Nikdy nezískal root, nikdy nedostal shell, nikdy se nedotkl ničeho dalšího. Jen potichu zabral většinu mé RAM a všechno ostatní nechal být, což je přesně důvod, proč jsem si ho nikdy nevšiml.

Pak jsem při kontrole toho, co dalšího je vystavené, spustil iptables -L a dostal skutečný titulek dne:

Server neměl firewall. Vůbec žádný. Ne špatně nakonfigurovaný, ne příliš benevolentní - INPUT politika byla ACCEPT a pravidel bylo nula. Každý port, který na tom stroji naslouchal na 0.0.0.0, byl přímo dostupný z otevřeného internetu, a to celé roky.

To zahrnovalo MariaDB na portu 3306. Také FTP server na portu 21, v plaintextu, o kterém jsem si dokonce ani nepamatoval, že je nainstalovaný. Dále dva Node vývojové servery, aplikaci v Next.js, správce snippetů a několik dalších věcí, které jsem někdy spustil a pak na ně zapomněl. Celkem čtrnáct otevřených portů, z nichž jsem ve skutečnosti chtěl mít veřejných jen šest.

Skutečná oprava tedy nebyla překonfigurovat čtrnáct aplikací jednu po druhé. Bylo jí nainstalovat firewall s výchozím pravidlem deny a otevřít pouze to, co skutečně poskytuji: SSH, HTTP, HTTPS, Gitin port pro git-over-SSH a dva porty pro backend, který opravdu musí být veřejný, protože jeho vlastní frontendový JavaScript ho volá přímo z prohlížeče. Všechno ostatní z internetu zmizelo. Následně jsem to zvenčí otestoval, abych měl jistotu, a databáze, vývojové servery i FTP skutečně z internetu zmizely.

Jedna věc, kterou stojí za to vědět, pokud něco podobného děláte: ufw nefiltruje porty, které publikuje Docker. Docker si zapisuje vlastní iptables pravidla výše v řetězci a vesele obchází váš firewall. Pokud chcete kontejner odstranit z internetu, musíte opravit jeho bindování portu, ne firewall. Tak jsem zjistil, že můj správce snippetů byl po celou dobu také veřejně dostupný.

Když už jsem tohle vše řešil, podíval jsem se na SSH a našel PermitRootLogin yes, PasswordAuthentication yes a 17 282 neúspěšných pokusů o přihlášení v logu, přičemž právě v tu chvíli na server útočil celý roj botů. Jedinou věcí mezi internetem a root shellem na mém serveru bylo to, jak dobré mám heslo. Přepnul jsem na autentizaci pouze pomocí certifikátu, což jsem měl udělat už před lety.

Malá past pro každého, kdo to bude kopírovat: na tomto stroji /etc/ssh/sshd/_config nemá direktivu Include, takže drop-in soubor, který jsem vytvořil v sshd/_config.d/, nikdo vůbec nenačítal. Konfigurace prošla kontrolou, sshd tvrdil, že je všechno v pořádku, ale efektivní nastavení se vůbec nezměnilo. Vždy si ověřte pomocí sshd -T, že se konfigurace skutečně načítá, místo abyste jen věřili, že soubor, který jste napsali, někdo čte.

Pak přišel restart. 1155 dní uptime znamená, že kernel běžící v paměti není kernel nainstalovaný na disku a řada bezpečnostně opravených binárek stále běžela ve starých verzích. Jenže tři roky starý produkční server nerestartujete jen tak - rizikem není kernel, ale služby, které někdo (já) ručně spustil v roce 2023 a nikdy nenastavil tak, aby se spouštěly automaticky. Takže jsem nejdřív zkontroloval úplně všechno. Ještě že tak: ukázalo se, že můj správce snippetů neměl žádnou restartovací politiku a tiše zemřel několik hodin předtím, když jsem restartoval Docker, a já si toho nevšiml. Po restartu by tedy stejně zůstal mrtvý.

Všechno po restartu naběhlo na novém kernelu. Weby fungovaly, kontejnery běžely, všech šest Node aplikací znovu ožilo.

A pak, protože den ještě nebyl kompletní, jsem přidal fail2ban, aby mi boti nezaplňovali logy. Potom jsem ale spustil automatizovaný skript, který používá SSH, odešel na hodinu pryč a po návratu zjistil, že SSH timeoutuje.

Weby byly všechny v pořádku. Port 443 v pořádku, port 80 v pořádku, Gitea v pořádku. Jen port 22, a jen pro mě. Nastavil jsem totiž fail2ban do agresivního režimu, který počítá každé odpojení před autentizací jako selhání - a hodina krátkých SSH relací z mé vlastní IP adresy vypadá z určitého úhlu přesně jako útok. Fail2ban tedy udělal přesně to, co jsem mu řekl, a zabanoval administrátora.

Tady se naštěstí ukázalo, že VNC konzole v administraci mého poskytovatele má smysl. Schválně jsem ji otestoval dříve téhož dne, ještě před vypnutím přihlašování heslem, právě proto, abych měl cestu zpět, kdybych se někdy sám zamkl venku. Nečekal jsem, že ji budu potřebovat o čtyři hodiny později. Zrušil jsem ban na vlastní IP, přidal ji na whitelist, přepnul zpět do normálního režimu a bylo hotovo. Fail2ban teď banuje asi deset skutečných botů a mě nechává na pokoji.

Úklid po kryptoměnovém mineru

 

Takže: .gitignore.

K čemu se pořád vracím, není miner. Miner byl skoro až zdvořilý - nikdy se nepokusil eskalovat oprávnění, nikdy se nedostal k rootovi, nic nerozbil, a kdyby používal 20 procent mé RAM místo 60, pravděpodobně bych dodnes nevěděl, že tam je. Co mě štve, je způsob, jakým se dovnitř dostal: přes experiment, který jsem jedno odpoledne spustil a pak opustil. Nešlo o zranitelnost něčeho, na čem závisím. Byl to prostě nějaký nástroj, který jsem zapnul, přišel mi mírně zajímavý a zapomněl jsem ho vypnout.

Myslím, že tohle je skutečný způsob, jakým hobbyisté selhávají. Nejde o to, že píšeme nezabezpečený kód. Jde o to, že věci hromadíme. Každý vedlejší projekt, každé "jen si to zkusím", každá služba spuštěná na 0.0.0.0, protože to byl nejrychlejší způsob, jak zjistit, jestli funguje - to všechno zůstává roky běžet na stroji, který nikdo nekontroluje. Tři roky uptime nejsou odznak cti. Jsou to tři roky, kdy se nikdo nedívá.

Stroj je teď v pořádku. Čistý, aktualizovaný, za firewallem, s autentizací pouze pomocí klíčů a s Git serverem, který využívá dvacetinu paměti oproti tomu, který nahradil. Pořád tam mám jednu starou službu, která se snaží spustit World of Warcraft server, jehož binárku jsem někdy smazal, takže selhává a zkouší to pořád dokola, což vám napovídá, že jsem se ještě úplně nepoučil.

Ale alespoň jsem konečně přešel na Giteu. Trvalo to jen dva roky a jeden cizí těžební stroj, než jsem se rozhýbal.

Přidat komentář

Kód jazyka komentáře.

Plain text

  • Nejsou povoleny HTML značky.
  • Řádky a odstavce se zalomí automaticky.
  • Adresy webů a emailů se převedou automaticky na odkazy.
CAPTCHA
Vložte znaky zobrazené na obrázku.
Tato otázka slouží pro ověření, zda jste člověk a pro zabránění automatizovaného spamu.