Webový server v Proxmox LXC: Apache, PHP-FPM, Drupal 10 a zabezpečení krok za krokem

Nový domov pro pět webů na Drupalu jsem postavil jako kontejner LXC na Proxmoxu. Popisuji, proč jsem zvolil kontejner místo virtuálního stroje, jak je server nastavený, co všechno jsem při převzetí webů opravil a jak jsem server zabezpečil.

Proč kontejner LXC

Kontejner LXC sdílí jádro s hostitelem, takže je lehčí než plný virtuální stroj. Startuje za pár sekund a zabere minimum paměti. Pro webový server s PHP a databází nic víc nepotřebuji. Kontejner je neprivilegovaný: root uvnitř kontejneru nemá práva roota na hostiteli.

Největší přínos jsou snapshoty. Před každou riskantní změnou udělám snapshot a při problému se během několika sekund vrátím zpět.

ParametrHodnota
VirtualizaceProxmox VE 9, neprivilegovaný kontejner LXC
SystémDebian 13
Prostředky2 jádra CPU, 4 GB RAM, 40 GB disk na NVMe
Webový serverApache 2.4 (mpm_event) + PHP-FPM 8.4
DatabázeMariaDB 11.8
CacheAPCu
CertifikátyLet's Encrypt (certbot, automatická obnova)
Weby5× Drupal 10, spravované Composerem

Apache s PHP-FPM místo mod_php

Původní server používal klasický modul mod_php. Ten vyžaduje starší režim Apache (prefork), ve kterém každý požadavek drží celý proces s PHP. Nový server používá mpm_event a PHP běží odděleně v PHP-FPM. Statické soubory (obrázky, CSS) obslouží Apache bez PHP a server zvládne víc současných návštěv s méně pamětí.

Zajímavost: IPv6 server, který potřebuje IPv4

Server má veřejnou jen IPv6 adresu. Ukázalo se ale, že Composer na některé služby přes IPv6 nedosáhne. Například GitHub dodnes IPv6 nepodporuje. Kontejner proto dostal ještě privátní IPv4 adresu, která slouží jen pro odchozí spojení: stahování aktualizací a překladů.

Co jsem našel při převzetí webů

  • Konfigurace webů ležely ve špatné složce Apache a jeden soubor měl syntaktickou chybu.
  • Moduly SSL, rewrite a headers nebyly zapnuté.
  • Složky webů měly práva 777, tedy zápis pro kohokoli.
  • Jádro Drupalu a modul Webform obsahovaly kritické bezpečnostní chyby.
  • Některé moduly byly nahrané ručně mimo Composer a nikdo je neaktualizoval.
  • Skript na kontrolu aktualizací hlásil „vše aktuální“, i když nebylo.
  • Správce databáze phpMyAdmin a správce pošty PostfixAdmin byly veřejně dostupné na všech doménách.

Aktualizace Drupalu

Všechny weby jsou teď spravované Composerem, včetně modulů, které byly původně nahrané ručně. Jádro jsem aktualizoval z verze 10.6.15 až na 10.6.20, včetně opravy kritické chyby umožňující vzdálené spuštění kódu. Téma vzhledu Bootstrap5 jsem převedl z nepodporované řady 3.x na 4.0.8, bez změny vzhledu webů.

Aktualizace všech pěti webů dělám jedním příkazem: snapshot, Composer, aktualizace databáze a vyčištění cache. Zajímavý detail: modul Asset Injector při čištění cache maže svou složku a vytvoří ji až při návštěvě webu. Kontrola před aktualizací databáze pak hlásí falešnou chybu zápisu. Řešením je složku před aktualizací vždy vytvořit:

install -d -o www-data -g www-data -m 775 sites/default/files/asset_injector

Zabezpečení serveru

  • SSH jen klíčem: přihlášení heslem je vypnuté, omezený počet pokusů, podrobné logování.
  • fail2ban blokuje adresy, které opakovaně zkoušejí hesla.
  • Automatické bezpečnostní aktualizace systému (unattended-upgrades).
  • phpMyAdmin jen z domácí sítě, PostfixAdmin a nepotřebný poštovní server vypnuté.
  • Bezpečnostní hlavičky ve všech webech, přesměrování na HTTPS a HSTS.
  • Audit nástroji Lynis a rkhunter. Hlášení, která jsou v kontejneru normální, jsem zdokumentoval jako výjimky.
  • Přístup z internetu jen přes Cloudflare: router pouští na porty 80 a 443 jen adresy Cloudflare.

Údržba

Cron spouští úlohy Drupalu každou hodinu, jednou týdně stahuje české překlady a každou noc zálohuje databáze. Certifikáty Let's Encrypt se obnovují samy. Jde to i za proxy Cloudflare, protože ověření přes HTTP projde.

Zpět na přehled celého projektu: Migrace pěti webů z VPS na vlastní server.