Když od někoho přebíráme server do správy, neptáme se nejdřív „co je potřeba udělat", ale „v jakém je to vlastně stavu". Zákazník obvykle řekne jednu jedinou větu: „běží to". A to je přesně ta věta, po které vždycky uděláme audit.

„Běží to" znamená, že to fungovalo až do téhle chvíle — ne že to přežije první nápor, první reboot nebo první sken portů zvenčí.

Bezpečnostní a stabilizační audit proto děláme hned na začátku, ještě než cokoli měníme. Chceme vědět, co jsme zdědili, kde jsou tikající bomby a co drží pohromadě jen shodou okolností. A ať přebíráme cokoli, na stejnou hrstku věcí narazíme znovu a znovu. Tady je jich osm — u každé: co byl problém, jak jsme to spravili a proč to tak má být.

1. Chybějící swap a OOM killer, který kosí databázi

Klasika. Server běží roky bez odkládacího prostoru, protože „má přece dost RAM". Jenže paměť není nikdy rovnoměrná — přijde špička, jádru dojde volná paměť a nastoupí OOM killer. Ten nevybírá podle viny, ale podle skóre „badness" (oom_score), které jádro počítá především z paměti procesu vůči té dostupné. Obvykle proto padne největší žrout paměti — a tím na webovém stroji bývá databáze. Skóre jde přitom ladit: kritický proces lze přes oom_score_adj chránit, aby přišel na řadu jako poslední.

Přidáme swap jako nárazník. Sám o sobě OOM nezruší — spíš ho oddálí a dá jádru prostor; jeho úplná absence naopak nutí OOM killer nastoupit dřív.

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

A snížíme ochotu jádra sahat po swapu za běžného provozu:

# /etc/sysctl.d/99-swappiness.conf
vm.swappiness = 10

Aktivace přes sysctl --system. Soubor musí mít příponu .conf, jinak ho sysctl --system tiše ignoruje — což je častá příčina toho, že se nastavení „neaktivuje".

Swap není nouzová náhrada chybějící RAM — je to nástroj, který jádru umožňuje odkládat zřídka používané „studené" anonymní stránky a tlumit krátké špičky. Pokud se do něj server opře trvale, thrashuje a je to signál, že chybí paměť nebo že něco žere víc, než má.

2. Přepálené limity webového serveru

Druhý nejčastější nález: Apache s MPM prefork a mod_php. Balíky na Debianu a Ubuntu sice dodávají MaxRequestWorkers na hodnotě 150 (vestavěný default samotného Apache je 256, distribuce ho přepisují), jenže ani těch 150 nikdo nepočítal z paměti tohohle konkrétního stroje. Každý prefork proces s vestavěným PHP si přitom drží řádově desítky MB — u lehčí aplikace kolem 30–80 MB, u těžkého webu s mnoha rozšířeními klidně 150–200 MB. Vynásobte si to — a je jasné, že při náporu procesy přerostou kapacitu RAM a OOM killer dorazí zbytek. Server nespadne kvůli útoku, spadne kvůli návštěvnosti.

Strop procesů počítáme z reálné paměti, ne z defaultu:

<IfModule mpm_prefork_module>
    StartServers            2
    MinSpareServers         2
    MaxSpareServers         5
    MaxRequestWorkers       20
    MaxConnectionsPerChild  1000
</IfModule>

U prefork MPM, kde jeden proces obsluhuje jeden požadavek, vyjdete zhruba z „(paměť vyhrazená webu − rezerva pro systém, databázi a cache) / paměť jednoho procesu", přičemž počítáte s privátní (nesdílenou) částí paměti, ne s naivním RSS z top. U vláknových MPM (worker/event) je strop ServerLimit × ThreadsPerChild, takže samotné dělení paměti dá jen počet procesů, který je nutné ještě vynásobit počtem vláken.

MaxConnectionsPerChild po nastaveném počtu obsloužených spojení ukončí celý dětský proces, který systém nahradí novým — u vláknových MPM se počítá napříč všemi jeho vlákny. Ve výchozím stavu je 0, tedy vypnuto; nenulová hodnota ohraničuje růst paměti z úniků v aplikaci (samotný leak nespraví, jen mu zabrání neomezeně se hromadit).

Lepší architektura: MPM event plus PHP-FPM

Tohle je ale záplata, ne cíl. Architektonicky lepší je oddělit web od PHP: MPM event (na moderním Debianu a Ubuntu ostatně výchozí) plus PHP-FPM. Procesy Apache pak nedrží PHP v sobě a spotřebují jednotky až nižší desítky MB, zatímco počet PHP procesů limitujete zvlášť přes pm.max_children ve FPM poolu. Web a PHP se tím přestanou navzájem tahat ke dnu.

3. Služby, které jen žerou paměť

Skoro na každém přebíraném serveru běží něco, co tam nemá co dělat. Typicky kompletní stack na skenování pošty — antivirový démon a filtry — na stroji, kde se žádná pošta neprovozuje. Takový démon si přitom drží databázi signatur v paměti a ukrojí stovky MB až gigabajty, které by se hodily webu nebo databázi.

Řešení je nudné, ale účinné: co se nepoužívá, vypneme.

systemctl disable --now clamav-daemon amavis

Pravidlo je jednoduché — na produkčním serveru běží jen to, co k jeho roli patří. Každá zbytečná služba je spotřeba paměti navíc a zároveň další plocha, kterou je nutné hlídat a záplatovat.

4. „Nainstaloval jsem = hotovo" je past

Tohle je zrádnější, než se zdá. Najdeme nainstalovaný fail2ban, v přehledu balíčků svítí zeleně, formálně je „vyřešeno". Jenže „nainstalováno" neznamená „chrání".

Někdy se služba kvůli chybě v konfiguraci vůbec nespustí — to systemd zaznamená jako failed a jde to dohledat. Zrádnější je druhá varianta: démon běží (active), ale jail tiše nikdy nikoho nezabanuje. Typicky proto, že čte log, který na daném systému neexistuje — sshd dnes na Debianu i Ubuntu píše do journalu, soubor /var/log/auth.log už nemusí existovat, a souborový jail tak čte prázdno. Žádné bany, žádná chyba. Řešením je nechat jail číst z journalu (backend = systemd), případně srovnat failregex s reálným formátem logu.

systemctl status fail2ban
fail2ban-client status sshd

U jailu nás nezajímá jen to, že „je", ale číslo Currently banned a to, že filtr reálně sedí na log (ověříte přes fail2ban-regex).

Poučení, které si neseme do každé zakázky: nainstalováno neznamená funkční. U bezpečnostní služby platí dvojnásob — nezajímá nás, že je nasazená, zajímá nás, že běží a že reálně dělá svou práci.

5. SSH dokořán

Povolené přihlášení heslem, k tomu přímý login pod rootem. To je kombinace, kterou boti na internetu milují — hádání hesel na rootovský účet je nejlevnější cesta dovnitř. Přecházíme na klíče a heslům zavřeme dveře:

# /etc/ssh/sshd_config.d/10-hardening.conf
PubkeyAuthentication yes
PasswordAuthentication no
PermitRootLogin prohibit-password

Reálně novou změnou je tu PasswordAuthentication no — zbylé dvě direktivy jsou v moderním OpenSSH už výchozí a uvádíme je pro explicitnost. Dvě věci, na kterých to nejčastěji padá:

  • Na pořadí souborů záleží. V sshd_config vyhrává první výskyt direktivy a soubory v sshd_config.d/ se čtou abecedně. Ubuntu tam přitom běžně má 50-cloud-init.conf s PasswordAuthentication yes. Náš soubor proto musí mít jméno, které se řadí před něj (odtud 10-), jinak zůstane přihlašování heslem tiše povolené. A musí končit na .conf, jinak se nenačte vůbec.
  • Změnu je potřeba ověřit a promítnout. sshd -t zkontroluje syntaxi, teprve pak konfiguraci znovunačtete.

PermitRootLogin prohibit-password znamená, že se root nepřihlásí heslem (ani přes keyboard-interactive) — ostatní metody, v praxi veřejný klíč, zůstávají povolené. fail2ban je tady doplněk, ne základ: hlavní obranou je, že u rootovského hesla není co hádat.

Na Debianu 12 a Ubuntu 22.04 změnu promítnete přes systemctl reload ssh. Na Ubuntu 24.04, kde je SSH ve výchozím stavu aktivované přes socket (ssh.socket), může reload skončit naprázdno — tam sáhněte po systemctl restart ssh. Než ale vypnete hesla, ověřte, že vám klíč skutečně funguje (viz metodika na konci).

6. Databáze vystavená do internetu

Pravidelný nález, u kterého tuhne krev: MySQL nebo MariaDB naslouchající na veřejné adrese. Na Debianu a Ubuntu je přitom výchozí vazba na 127.0.0.1, takže veřejné naslouchání zpravidla znamená, že to někdo dřív ručně přepnul (typicky na 0.0.0.0). Databáze nemá jediný důvod být dostupná zvenčí, když aplikace běží na témže stroji. Zavřeme to na dvou úrovních — vazbou na localhost i firewallem:

# MariaDB: /etc/mysql/mariadb.conf.d/50-server.cnf  (sekce [mysqld])
# Oracle MySQL: /etc/mysql/mysql.conf.d/mysqld.cnf
bind-address = 127.0.0.1

Pokud stroj poslouchá i na IPv6, svažte i loopback ::1 (bind-address = 127.0.0.1,::1), nebo TCP vypněte úplně přes skip-networking. Po změně službu restartujte.

ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80,443/tcp
ufw enable

Co skutečně poslouchá — TCP i UDP — ověříte přes sudo ss -tulnp (bez roota se u cizích procesů nevyplní sloupec s PID). Řídíme se principem nejmenší expozice: ven nepatří nic, co tam nemusí být. Každý otevřený port je závazek — musíte ho hlídat, jinak ho hlídá někdo jiný za vás.

7. Zastaralý nebo doběhlý systém bez záplat

Server na verzi, které skončila bezpečnostní podpora, je jen otázka času. Bez záplat se hromadí známé zranitelnosti, na které existují hotové exploity. První krok je vždycky doinstalovat, co chybí, a nastavit pravidelné záplatování.

U doběhlé LTS distribuce ale nemusíte hned řešit velký upgrade. Ubuntu nabízí rozšířenou údržbu přes Ubuntu Pro, který u LTS prodlužuje bezpečnostní podporu z pěti na deset let (a přidává i další věci, například živé záplatování jádra). Pozor ale na licenci: bezplatný tier Ubuntu Pro platí jen pro osobní použití — až na pěti strojích (u členů Ubuntu Community až 50). Server v cizí správě je komerční nasazení, a to vyžaduje placené předplatné; s tím je potřeba u převzatého stroje počítat.

sudo pro attach <token>
sudo pro enable esm-infra   # zpravidla zbytečné, po attach se služby zapnou samy

Po pro attach se výchozí služby (včetně ESM) na LTS serveru aktivují automaticky; samostatný enable potřebujete jen tehdy, když jste stroj připojili s --no-auto-enable nebo službu dřív vypnuli. Tím získáte bezpečnostní záplaty i po konci standardní podpory a upgrade na novější LTS si naplánujete v klidu, ne v panice.

Debian prodlužuje podporu stabilních vydání zhruba na pět let přes projekt Debian LTS — není to samostatná větev, ale fáze podpory, kterou dostane každé stabilní vydání po konci běžného bezpečnostního okna (zajišťuje ji samostatný tým, ne běžný bezpečnostní tým). Tak či tak platí: záplatování je průběžná činnost, ne jednorázový úkon.

8. Dva webové servery na jednom portu

Provozní past, která nespustí bombu hned — počká si na reboot. Na serveru běží nginx i Apache a oba mají obsadit port 80. Dokud se nerestartuje, jede ten, který nastartoval jako první. Po rebootu ale může vyhrát ten druhý, zabere port, správný server na něj nedosáhne a web nenaběhne. Chyba se přitom „stala" před měsíci — jen se projeví až v nejméně vhodnou chvíli.

sudo ss -tlnp 'sport = :80'
systemctl disable --now apache2
systemctl is-active nginx apache2     # co právě běží
systemctl is-enabled nginx apache2    # co nastartuje po bootu

Pravidlo: jeden server na jeden port, čistý stav služeb — co má běžet, je enabled i active, co ne, je disabled. Pozor na rozdíl: is-enabled mluví jen o autostartu při bootu, ne o tom, co právě teď poslouchá — na to je is-active (nebo ss). A hlavně: reboot vyzkoušejte řízeně dřív, než ho budete potřebovat v ostrém provozu.

Shrnutí osmi nálezů

Nález Proč je to problém Náprava
Chybějící swap OOM killer nastupuje dřív, padá největší žrout paměti Swap jako nárazník + vm.swappiness
Přepálené limity webu Procesy přerostou RAM při náporu MaxRequestWorkers z reálné paměti, ideálně event + PHP-FPM
Nepotřebné služby Zbytečná spotřeba paměti a plocha k útoku Vypnout, co k roli stroje nepatří
„Nainstalováno = hotovo" Ochrana formálně je, reálně nechrání Ověřit běh i skutečnou funkci
Otevřené SSH (heslo + root) Nejlevnější cesta dovnitř pro boty Klíče, PasswordAuthentication no
Databáze ven Zbytečně vystavená služba Vazba na localhost + firewall
Systém bez záplat Hromadění známých zranitelností Doinstalovat, nastavit průběžné záplatování
Dva servery na jednom portu Web nenaběhne po rebootu Jeden server na port, čistý stav služeb

Jak vůbec sáhnout na živý produkční server

Všechny tyhle zásahy děláme na stroji, který právě teď někomu běží. Proto máme jednu neměnnou metodiku:

  • Měníme jednu věc a hned ověřujeme. Po zásahu do SSH otevřeme druhé spojení a přesvědčíme se, že se pořád přihlásíme — teprve pak zavřeme to původní. Po zásahu do webu ověříme, že web opravdu odpovídá.
  • U vzdálených změn firewallu a SSH máme pojistku proti zamčení. Naplánovaný automatický revert nebo dočasné pravidlo znamená, že i když se ustřelíme, server se za pár minut vrátí do funkčního stavu a nezůstaneme venku.
  • Reboot bereme jako součást práce, ne jako riziko. Nové jádro se aktivuje až po restartu (živé záplatování typu Livepatch to obchází, ale spoléhat se na ně nedá vždy), takže restartujeme řízeně. Po rebootu počkáme, až boot doběhne — systemctl is-system-running má vrátit running, ne degraded — ověříme prázdné systemctl --failed, zkontrolujeme, že kritické služby jsou enabled i active, a web otestujeme funkčně: ne že „naběhla služba", ale že reálně odpovídá (curl -I na očekávaný stav). Když něco nesedí, příčinu dohledáme v journalctl -b.

Proč přebíraný server nikdy nebereme jako hotový

Ani jeden z těch osmi nálezů nezpůsobí, že „to neběží". Server s neúčinným fail2banem, vystavenou databází a přepálenými limity běží úplně stejně dobře jako zdravý — až do dne, kdy přijde nápor, sken zvenčí nebo obyčejný reboot. Celý smysl auditu při převzetí je najít ten rozdíl dřív než útočník nebo náhoda.

Rozdíl mezi serverem, který zrovna běží, a serverem, který nespadne, je přesně to, co hledáme.

Proto pro nás „běží = je v pořádku" neplatí. Přebíraný server je pro nás v pořádku teprve ve chvíli, kdy víme, že přežije špičku v paměti, ustojí reboot a nemá dokořán nic, co tam nepatří.