Nginx s rootem
Nginx má po startu ve výchozím stavu v paměti několik procesů. Hlavní proces (master) se spouští s právy uživatele root a spouští si větší množství pracovních procesů (worker) pod zvoleným běžným uživatelem. Ty se starají o samotné zpracování požadavků podle konfigurace a nepotřebují žádná speciální oprávnění.
Hlavní proces ale potřebuje vysoká oprávnění, protože kromě zpracovávání konfigurace a spouštění dalších procesů potřebuje především otevírat TCP porty nižší než 1024. Obvykle to jsou ty webové, tedy 80 a 443. To ovšem v Linuxu může dělat jen root.
Zároveň ale platí, že s vysokými oprávněními by mělo v systému běžet co nejméně procesů. Je to doporučení velmi rozumné, protože v případě úspěšné kompromitace pak nemusí dojít k tak velkým škodám, jako v případě privilegované aplikace.
Nginx bez roota
Linux už ovšem velmi dlouhou dobu nabízí funkci capabilities, což bychom mohli česky přeložit jako schopnosti. Ty umožňují silná oprávnění roota dělit do malých skupin a přidělit tedy procesu třeba jen jedno privilegium. Takový proces pak nemá veškerá božská práva roota, ale jen jednu přesně vybranou schopnost.
Tohle se nám hodí například u našeho web serveru, kterému můžeme přidělit schopnost CAP_NET_BIND_SERVICE
, u které manuálová stránka uvádí: Bind a socket to Internet domain privileged ports (port numbers less than 1024).
Tohle přesně potřebujeme.
Stačí tedy jen správně nakonfigurovat init systém, aby zaváděl webový server pod běžným uživatelem a přidal mu tohle potřebné oprávnění navíc. Podobný postup bude pochopitelně možné využít i u jiných služeb, které se chovají podobně a roota vyžadují jen kvůli otevření portů.
Konfigurujeme službu
Moderní distribuce používají systemd, který je na podobné kousky velmi dobře vybaven. Nejprve si tedy zkopírujeme standardně dodávaný unit soubor na místo určené pro naše lokální úpravy.
# cp /lib/systemd/system/nginx.service /etc/systemd/system/
Poté už jej budeme vždy editovat v novém umístění v /etc/
, kde nehrozí přepsání balíčkovacím systémem při aktualizaci. V sekci [Service]
doplníme informaci o tom, pod jakým uživatelem a skupinou má hlavní proces Nginx běžet:
User=www-data Group=www-data
Tahle jednoduchá změna zajistí běh pod neprivilegovaným uživatelem. Dále mu musíme přidělit zmíněnou schopnost, která dovolí otevření nízkých portů.
AmbientCapabilities=CAP_NET_BIND_SERVICE
Tohle ovšem nestačí, protože Nginx si ještě při startu vytváří soubor /run/nginx.pid
, což by teď přestalo fungovat, protože nadřazený adresář vlastní root. Proto musíme vytvořit nový adresář vlastněný správným uživatelem.
Se systemd je to snadné, stačí opět do konfiguračního souboru služby připsat jeden řádek. Ten způsobí vytvoření správného podadresáře s vhodnými právy:
RuntimeDirectory=nginx
Poté tam ještě musíme nechat umístit onen soubor s PID, takže je potřeba upravit správný řádek, aby obsahoval novou cestu:
PIDFile=/run/nginx/nginx.pid
Konfigurujeme Nginx
Tahle část bude snadná, protože z hlediska běhu web serveru se mění jen jediná věc: umístění zmíněného PID souboru. Proto musíme v hlavním konfiguračním souboru /etc/nginx/nginx.conf
změnit patřičný řádek.
pid /run/nginx/nginx.pid;
To je vlastně vše. Teď stačí už jen službu restartovat a vyzkoušet, že se všechno povedlo.
Restart a ověření
Použijeme dva příkazy, nejprve necháme systémového démona znovu načíst konfiguraci a vytvořit si strom závislostí mezi službami. Poté otočíme samotný web server.
# systemctl daemon-reload # systemctl restart nginx
Protože se vše povedlo, nedostali jsme žádnou chybu. Pokud přeci jen ano, budeme muset zkoumat žurnál pomocí příkazu journalctl -xe
. Nezbývá, než ověřit, že vše funguje, jak jsme si představovali.
Nejprve si vypíšeme seznam procesů v systému a jejich vlastníků:
# ps axu | grep nginx www-data 5119 0.0 0.1 69640 1696 ? Ss 20:39 0:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on; www-data 5120 0.0 0.2 69956 2576 ? S 20:39 0:00 nginx: worker process
Vidíte, že oba procesy běží pod uživatelem www-data
a ani hlavní proces tedy už neběží pod rootem. Nyní ještě výpis otevřených portů pomocí příkazu ss:
# ss -tlpn State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=5120,fd=6),("nginx",pid=5119,fd=6)) LISTEN 0 128 [::]:80 [::]:* users:(("nginx",pid=5120,fd=7),("nginx",pid=5119,fd=7))
Také tady se vše podařilo a webový server si otevřel výchozí TCP port 80. Vše tedy běží jako obvykle, jen zbytečně nepřidělujeme privilegia, která nejsou potřeba.
Pozor na práva
Změna přináší ještě jedno úskalí, které vyplývá ze změny uživatele hlavního procesu. Musíme zajistit, aby všechny konfigurační soubory a další zdroje byly dostupné novému uživateli.
Já jsem testoval na aktuálním Debianu Buster a na něm nejsou potřeba žádné úpravy práv, protože adresář /etc/nginx/
je čitelný pro všechny. Nemusí to tak ale být na každé distribuci a je potřeba si to pohlídat.
Týká se to také například souborů s certifikáty a privátními klíči, které bývají často uloženy na disku jinde a zatímco dříve k nim měl Nginx automaticky přístup (protože jako root mohl kamkoliv), teď už to nemusí automaticky platit. Naštěstí nejde o nijak komplikované úpravy a běžný linuxový správce je snadno zvládne.
Pozor také na použití stejného uživatele pro běh hlavního procesu a webové aplikace napsané například v PHP. Pokud by vše běželo pod jedním uživatelem, mohla by děravá aplikace získat přístup ke všem tajemstvím na serveru, včetně privátních klíčů. Je tedy rozumné použít jiného uživatele pro Nginx a jiného pro provoz PHP-FPM. Tím se jednoduše citlivá data oddělí.
Zdroje
- Článek Sandboxing nginx with systemd (Nick ODell)
- Dokumentace Systemd
- Dokumentace Nginx
- Manuálová stránka capabilities