RE:NODE

Jak bezpečně zablokovat HTTP/1.0 v nginx a Apache

Text: Tým RE:NODE
16 min čtení


Abys v nginx zablokoval HTTP/1.0, zkontroluj $server_protocol a hned vrať odpověď: if ($server_protocol = HTTP/1.0) { return 444; } uvnitř bloku server. V Apache porovnej %{SERVER_PROTOCOL} pomocí mod_rewrite a odpověz 403, nebo použij blok <If> s Require all denied. Obojí napíšeš za minutu. Déle trvá rozhodnout, jestli to vůbec máš dělat, protože provoz, na který míříš (staří boti, hromadné skenery, požadavky bez hlavičky Host), je promíchaný s hrstkou legitimních klientů, kteří pořád mluví HTTP/1.0 - a to velmi často včetně tvého vlastního reverzního proxy. Nejdřív loguj, potom blokuj, a ve většině případů odvede hodně užitečné práce záchytný výchozí server, který se HTTP/1.0 vůbec netýká.

Proč stojí za to sledovat provoz HTTP/1.0#

HTTP/1.0 nahradilo HTTP/1.1 v roce 1997. Každý prohlížeč vydaný v tomto století umí minimálně 1.1 a přes HTTPS vyjednává HTTP/2 nebo HTTP/3. Když se tedy access log plní požadavky HTTP/1.0, nepocházejí od lidí, kteří čtou tvůj web. Posílají je tři druhy klientů:

  • Hromadné skenery a sběrače bannerů. Nástroj, který projíždí celý prostor IPv4, chce nejkratší možný požadavek, na který dostane odpověď. GET / HTTP/1.0 následované prázdným řádkem je přesně to: žádná povinná hlavička Host, žádný keep-alive k obsluze, server po odpovědi spojení zavře.
  • Staří nebo líní boti. Scrapery postavené na prastarých HTTP knihovnách, sondy zranitelností opsané z desítky let starých exploitů a domácí crawlery, které mají řádek požadavku napsaný natvrdo.
  • Stroje, které mluví se stroji. Health checky load balanceru, benchmarkovací nástroje, monitorovací skripty a - to, co lidi nejčastěji překvapí - reverzní proxy, které komunikují se serverem za sebou.

Zajímavá vlastnost HTTP/1.0 je, že hlavička Host je volitelná. HTTP/1.1 ji vyžaduje a nginx i Apache odmítnou požadavek HTTP/1.1 bez ní chybou 400 Bad Request. Požadavek HTTP/1.0 bez Host je platný, takže server musí odhadnout, který web obsloužit, a vybere výchozí pro danou adresu a port. Tak skenery zjišťují, co na IP adrese běží, aniž by znaly jediné doménové jméno: zeptají se bez jména a přečtou si, co výchozí web vrátí - přihlašovací stránku CMS, chybovou stránku frameworku, verzi stacku v hlavičce.

I tak to stojí za to. Tichší log se snáz hlídá alerty a aplikace, která nikdy nevidí nesmyslné požadavky, na ně neutrácí databázové dotazy. Jen to nezařazuj pod „zabezpečení“.

Nejdřív loguj, potom blokuj#

Než změníš jakékoli chování, zjisti, kdo ti HTTP/1.0 doopravdy posílá. Výchozí formát combined v obou serverech to už zaznamenává, protože řádek požadavku - metoda, cesta a protokol - je jedno pole.

bash
# How many HTTP/1.0 requests, out of how many in total$ grep -c 'HTTP/1.0"' /var/log/nginx/access.log$ wc -l < /var/log/nginx/access.log# Which user agents send them, most frequent first$ grep 'HTTP/1.0"' /var/log/nginx/access.log \    | awk -F'"' '{print $6}' | sort | uniq -c | sort -rn | head -20# Which addresses send them$ grep 'HTTP/1.0"' /var/log/nginx/access.log \    | awk '{print $1}' | sort | uniq -c | sort -rn | head -20

Pro Apache platí tytéž příkazy nad /var/log/apache2/access.log na Debianu a Ubuntu, nebo /var/log/httpd/access_log na systémech rodiny RHEL. Trik s awk -F'"' funguje, protože v kombinovaném formátu je user agent třetí pole v uvozovkách.

Co ve výstupu hledat:

  • Vlastní adresy. 127.0.0.1, privátní rozsah jako 10.0.0.0/8 nebo adresa load balanceru. Jsou to stroje, které provozuješ, a jejich zablokování něco rozbije.
  • Rozpoznatelné nástroje. ApacheBench, název monitorovacího produktu, health checky HAProxy, uptime služba. Každý z nich je něčí práce.
  • Prázdné nebo nic neříkající user agenty z dlouhého chvostu adres, které chodí na /, /.env, /wp-login.php, /phpmyadmin/ a podobně. To je provoz, který chceš pryč.

Nech to běžet týden, ne odpoledne. Týdenní cron úlohy, měsíční reporty a platební poskytovatel, který volá zpět dvakrát ročně, se nakonec ukážou a týden je nejkratší okno, které jich zachytí většinu. Článek Logy, které stojí za to uchovat popisuje, jak dlouho držet access logy, aby se takováto kontrola dala udělat i zpětně.

V nginx můžeš požadavky HTTP/1.0 zapisovat i do samostatného souboru, díky čemuž je týden sledování bezbolestný. Parametr if= u access_log zapíše požadavek jen tehdy, když je proměnná neprázdná a není 0:

/etc/nginx/conf.d/http10-log.conf
map $server_protocol $is_http10 {    default     0;    "HTTP/1.0"  1;}
nginx
server {    # ... your existing configuration ...    access_log /var/log/nginx/access.log combined;    access_log /var/log/nginx/http10.log combined if=$is_http10;}

Apache to dělá pomocí SetEnvIf, který umí přímo porovnávat Request_Protocol, a CustomLog, který se spustí jen tehdy, když je proměnná nastavená:

/etc/apache2/conf-available/http10-log.conf
SetEnvIf Request_Protocol "^HTTP/1\.0$" old_httpCustomLog ${APACHE_LOG_DIR}/http10.log combined env=old_http

Zapni to příkazem a2enconf http10-log a načti konfiguraci znovu. Na systémech rodiny RHEL dej soubor do /etc/httpd/conf.d/ a ${APACHE_LOG_DIR} nahraď za logs.

Nejdřív záchytný výchozí server#

Většina toho, co lidé od „blokování HTTP/1.0“ chtějí, je ve skutečnosti „přestat odpovídat na požadavky, které nejmenují žádný z mých webů“. Na to existuje čistší řešení, které funguje pro každou verzi protokolu: výchozí server, který odmítne všechno, takže ke skutečnému webu se dostanou jen požadavky se skutečným hostname.

bez Host nebo neznámé jménoznámý HostKlientlibovolná verze HTTPnginxport 80 / 443Záchytný serverdefault_server, 444Tvůj webshoda server_nameAplikace
Kam požadavek spadne, určuje hlavička Host

V nginx je to blok server označený default_server na každém portu. Požadavky bez hlavičky Host i požadavky s hlavičkou, která neodpovídá žádnému server_name, skončí tady:

/etc/nginx/sites-available/00-catchall.conf
server {    listen 80 default_server;    listen [::]:80 default_server;    server_name _;    return 444;}server {    listen 443 ssl default_server;    listen [::]:443 ssl default_server;    server_name _;    ssl_reject_handshake on;}

444 není skutečný HTTP stavový kód. Říká nginxu, aby zavřel spojení, aniž by cokoli poslal, takže se klient nedozví nic - ani jaký serverový software odpověděl. Na portu 443 proběhne TLS handshake před jakýmkoli HTTP požadavkem, takže samotné return 444 by nejdřív potřebovalo certifikát, který by předložilo; ssl_reject_handshake on (nginx 1.19.4 a novější) odmítne handshake pro jména, která jsi nenastavil, a certifikát nepotřebuje. Na starším nginxu musíš záchytnému serveru dát self-signed certifikát. Průvodce reverzním proxy v nginx pokrývá zbytek rozvržení, včetně toho, proč smí mít default_server jen jeden blok na port.

Apache žádné klíčové slovo default_server nemá. Výchozí je první načtený VirtualHost pro danou adresu a port, proto Debian pojmenovává svůj výchozí web 000-default.conf. Nahraď jeho obsah vhostem, který zakáže všechno:

/etc/apache2/sites-available/000-default.conf
<VirtualHost *:80>    ServerName default.invalid    <Location />        Require all denied    </Location>    ErrorLog ${APACHE_LOG_DIR}/default-error.log    CustomLog ${APACHE_LOG_DIR}/default-access.log combined</VirtualHost>

Apache nedokáže tiše zahodit spojení jako nginx, takže vrací 403 Forbidden. Doména nejvyšší úrovně .invalid je rezervovaná a nikdy se nepřeloží, takže je zaručeno, že jí žádný skutečný požadavek podle jména neodpovídá.

S nasazeným záchytným serverem už požadavek HTTP/1.0 bez Host nedostane nic užitečného. Pro mnoho webů to stačí a zbývající provoz HTTP/1.0 - požadavky, které tvoji doménu jmenují - je dost neškodný na to, aby se nechal být. Pokud ho přesto chceš pryč, čti dál.

Blokování HTTP/1.0 v nginx#

$server_protocol obsahuje protokol, který klient použil: HTTP/1.0, HTTP/1.1, HTTP/2.0 nebo HTTP/3.0. Nejjednodušší blok ho porovná přímo:

nginx
server {    listen 80;    server_name example.com www.example.com;    if ($server_protocol = HTTP/1.0) {        return 444;    }    # ... the rest of the site ...}

Obvyklé varování, že if v nginx je nebezpečné, platí pro if uvnitř location v kombinaci s dalšími direktivami. if na úrovni server, který obsahuje jen return, patří mezi případy, které dokumentace nginx popisuje jako bezpečné.

Porovnávej HTTP/1.0 kladně. Pravidlo zapsané jako if ($server_protocol != HTTP/1.1) vypadá rovnocenně, ale není: zablokuje i každého návštěvníka přes HTTP/2 a HTTP/3, což jsou na HTTPS webu skoro všichni.

Přístup přes map, s výjimkami

Holé if zablokuje úplně všechny. Ve chvíli, kdy potřebuješ výjimku - monitorovací server, load balancer, 127.0.0.1 - se lépe čte map a žije na jednom místě pro všechny weby. geo označí důvěryhodné adresy a map spojí obě proměnné do jednoho rozhodnutí:

/etc/nginx/conf.d/http10-block.conf
geo $http10_trusted {    default        0;    127.0.0.1      1;    10.0.0.0/8     1;    203.0.113.25   1;   # monitoring host}map "$server_protocol:$http10_trusted" $http10_block {    default          0;    "HTTP/1.0:0"     1;}

Potom v každém bloku server, který to má vynucovat:

nginx
if ($http10_block) {    return 444;}

Soubory v conf.d se vkládají do kontextu http, kde map a geo musí být. Obojí se vyhodnocuje líně, jen když se proměnná použije, takže jejich definice nic nestojí na webech, které se na ně neodkazují. Chceš-li z pravidla vyjmout user agenta místo adresy, přidej do zdroje mapy $http_user_agent a použij regulární klíče, ale pamatuj, že user agent je to, co o sobě klient řekne.

444, 426 nebo 403

Jakou odpověď poslat, záleží na tom, kdo ji bude číst.

OdpověďCo klient vidíKdy ji použít
444Spojení zavřené, žádné bajtyProvoz je nevyžádaný a nic za ním nestojí
426Upgrade Required, s důvodemSkutečný klient může potřebovat vysvětlení
403ForbiddenChceš běžné, dobře srozumitelné odmítnutí

426 Upgrade Required je zdvořilá odpověď ve stylu standardu: říká, že požadavek byl pochopen a že je potřeba novější protokol. Specifikace HTTP očekává, že hlavička Upgrade ten protokol pojmenuje, a v nginx je add_header uvnitř if povolený jen na úrovni location, takže úplná verze vypadá takto:

nginx
location / {    if ($http10_block) {        add_header Upgrade "HTTP/1.1" always;        add_header Connection "Upgrade" always;        return 426 "HTTP/1.1 or newer is required.\n";    }    # ... proxy_pass, try_files, etc ...}

always je důležité: bez něj se add_header uplatní jen u krátkého seznamu kódů úspěchu a přesměrování a hlavičky by z odpovědi 426 tiše zmizely. V praxi je 444 správná volba pro většinu webů. Skener stavové řádky nečte a legitimní klient, který dostane prázdnou odpověď, vyrobí jasnou chybu (curl: (52) Empty reply from server), která ti přesně řekne, kde hledat.

Blokování HTTP/1.0 v Apache#

Apache nabízí tři způsoby. Vyber si jeden; jejich míchání udělá ladění pro dalšího člověka utrpením.

mod_rewrite nad SERVER_PROTOCOL nebo THE_REQUEST

Tuto verzi ukazuje většina návodů a funguje v hlavní konfiguraci i v .htaccess:

.htaccess or <VirtualHost>
RewriteEngine OnRewriteCond %{SERVER_PROTOCOL} =HTTP/1.0RewriteRule ^ - [F]

%{SERVER_PROTOCOL} obsahuje řetězec protokolu a předpona = z toho dělá přesné porovnání řetězců místo regulárního výrazu. [F] vrátí 403 Forbidden a zastaví zpracování. Chceš-li místo toho poslat 426, použij RewriteRule ^ - [R=426,L] - když je stavový kód předaný do R mimo rozsah 3xx, mod_rewrite zahodí substituci a vrátí jen ten kód.

%{THE_REQUEST} je celý původní řádek požadavku, například GET /index.php HTTP/1.0. Porovnávání nad ním je pro tento účel rovnocenné:

code
RewriteCond %{THE_REQUEST} HTTP/1\.0$RewriteRule ^ - [F]

Rozdíl se projeví jinde: THE_REQUEST nikdy nezmění dřívější přepisy ani interní přesměrování, proto po této proměnné lidé sahají, když blokují podle surového požadavku. Pro kontrolu protokolu se SERVER_PROTOCOL čte přehledněji. Výjimky jsou jedna podmínka navíc: RewriteCond %{REMOTE_ADDR} !^10\. umístěná před pravidlem přeskočí privátní rozsah.

V .htaccess mod_rewrite funguje jen tehdy, když AllowOverride daného adresáře obsahuje FileInfo (nebo All). Pokud pravidla vypadají, že nedělají nic, je to první věc ke kontrole, hned potom ověř, jestli je mod_rewrite vůbec zapnutý (a2enmod rewrite na Debianu).

Require s výrazem, bez mod_rewrite

Výrazový parser Apache 2.4 zná také SERVER_PROTOCOL, takže stejné pravidlo jde napsat pomocí řízení přístupu místo přepisování:

<VirtualHost> or .htaccess
<If "%{SERVER_PROTOCOL} == 'HTTP/1.0' && ! -R '10.0.0.0/8'">    Require all denied</If>

-R testuje, jestli je adresa klienta uvnitř podsítě. Případně znovu použij proměnnou old_http z části o logování a napiš Require not env old_http uvnitř bloku <RequireAll> vedle Require all granted. Obě formy vracejí 403. Tento přístup se čte snáz než přepisovací pravidla a nezasahuje do přepisování, které už dělá tvoje CMS, což je na .htaccess ve WordPressu nebo Laravelu skutečná výhoda.

Co HttpProtocolOptions dělá a co ne

HttpProtocolOptions se objevuje téměř v každé odpovědi na tuto otázku a HTTP/1.0 neblokuje. Přibyl v Apache 2.4.24 a řídí, jak přísně se parsuje řádek požadavku a hlavičky. Jeho výchozí hodnota je Strict LenientMethods Allow0.9 a volba, po které lidé sahají, je:

apache2.conf or <VirtualHost>
HttpProtocolOptions Strict Require1.0

Require1.0 odmítá HTTP/0.9 - podobu před rokem 1996, kdy je požadavek jen GET / bez verze a bez hlaviček. HTTP/1.0 a novější se stále přijímá. Nastavit to stojí za to, protože nic legitimního už desítky let 0.9 nemluví, ale je to jiná oprava jiného (a menšího) problému. Strict je už výchozí a způsobí, že Apache odmítne chybně utvořené řádky požadavků a hlavičky chybou 400; nech to zapnuté, pokud tě starý klient nedonutí použít Unsafe.

Testování pomocí curlu#

curl umí na požádání mluvit HTTP/1.0, takže kontroly jsou rychlé. Spouštěj je ze stroje, který není na seznamu výjimek.

bash
# HTTP/1.0 with a Host header - should be blocked if you block 1.0$ curl --http1.0 -sv -o /dev/null http://example.com/# HTTP/1.0 with no Host header - should hit the catch-all$ curl --http1.0 -sv -o /dev/null -H "Host:" http://203.0.113.10/# HTTP/1.1, the control - should work normally$ curl --http1.1 -sv -o /dev/null http://example.com/# Over HTTPS, where browsers will negotiate HTTP/2$ curl --http1.0 -sv -o /dev/null https://example.com/

-H "Host:" s ničím za dvojtečkou říká curlu, aby odstranil hlavičku, kterou by jinak přidal, a je to jediný způsob, jak ho přimět poslat požadavek bez Host. Co čekat:

  • 444 z nginx se zobrazí jako curl: (52) Empty reply from server.
  • 403 nebo 426 se ve výpisu verbose zobrazí jako < HTTP/1.1 403 Forbidden nebo podobně. Všimni si, že server odpovídá svou nejvyšší verzí ve stavovém řádku i tehdy, když byl požadavek 1.0; to je normální.
  • Požadavek HTTP/1.1 bez hlavičky Host odmítnou oba servery chybou 400 ještě dřív, než se tvoje pravidla vůbec spustí.

Pro nejsyrovější možný test, bez jakékoli logiky klienta, napiš požadavek sám:

bash
$ printf 'GET / HTTP/1.0\r\n\r\n' | nc example.com 80

Po testování ověř, že výjimky pořád fungují, tak, že první příkaz spustíš z důvěryhodné adresy, a potvrď, že logy ukazují zablokované požadavky s kódem, který sis vybral. Před načtením konfigurace vždy spusť nginx -t nebo apachectl configtest - překlep v map shodí každý web na serveru, ne jen ten, který jsi upravoval.

Legitimní klienti, které můžeš rozbít#

Tuhle část si přečti dvakrát. Každý z těchto klientů posílá HTTP/1.0 ve výchozím nastavení nebo v běžných konfiguracích:

  • Samotný nginx jako proxy. proxy_http_version má výchozí hodnotu 1.0. Pokud máš nginx před dalším nginxem nebo Apache, backend vidí každý požadavek jako HTTP/1.0 a blokování protokolu na backendu zablokuje celý tvůj web. Buď nastav proxy_http_version 1.1; na přední proxy (což stejně chceš kvůli keep-alive a websocketům), nebo pravidlo vynucuj jen na vnějším serveru.
  • Health checky load balanceru. option httpchk v HAProxy posílá požadavek HTTP/1.0, pokud mu nedáš verzi. Zablokovaný health check označí backend za nedostupný a load balancer za tebe web vypne.
  • ApacheBench. ab mluví jen HTTP/1.0. Tvůj zátěžový test ohlásí každý požadavek jako neúspěšný a ty strávíš hodinu podezíráním aplikace.
  • Monitoring a uptime kontroly. Některé starší kontroly a pluginy mají pořád výchozí 1.0. Hledej je v týdnu logování, místo abys to zjistil, až přestanou chodit alerty.
  • Velmi staré nástroje příkazové řádky a vestavěné klienty. Staré buildy wget, tiskárny, routery, firmware IoT a skripty psané proti prastarým knihovnám. Pokud jeden z nich posílá data na webhook na tvém webu, přestane to tiše fungovat.
  • Webhooky ze starších systémů. Platební, doručovací a ERP integrace jsou místa, kde žije dlouhověký legacy kód. Jsou vzácné, a právě proto je jedno odpoledne logů přehlédne.

Existuje i záludnější verze prvního problému: proxy, kterou nekontroluješ. Když web sedí za CDN, front-endem poskytovatele hostingu nebo jakoukoli jinou reverzní proxy, verze HTTP, kterou vidíš, je ta, kterou proxy použila, aby se k tobě dostala, ne ta, kterou použil návštěvník. Na tarifech RE:NODE pro aplikace a weby takto funguje doména připojená přes proxy slot tarifu: slot obstará certifikát a skutečnou adresu klienta předá v X-Forwarded-For. Než napíšeš pravidlo, zkontroluj na vlastním serveru $server_protocol (nebo %H ve formátu logu Apache): pokud každý požadavek přichází se stejnou verzí, kontrola protokolu na té vrstvě buď nezablokuje nic, nebo zablokuje všechno. Na dedikovaném serveru, kde je tvůj nginx první věc na internetu, je verze, kterou zalogueš, verzí samotného návštěvníka.

Co to nezastaví#

Říct si jasně, kde je hranice, ušetří zklamání. Klient, který pošle HTTP/1.1 s platnou hlavičkou Host, projde přímo skrz všechna pravidla v tomto článku, a moderní skenovací frameworky, nástroje na credential stuffing i scrapery to dělají všechny. Kontrola protokolu odfiltruje nejlínější vrstvu šumu na pozadí; proti odhodlanému útočníkovi nebo dobře napsanému botovi nedělá nic.

Na provoz, který se přes ni dostane, jsou jiné nástroje. Rate limiting omezí, jak rychle může jeden klient narážet na náročné endpointy jako přihlašovací formuláře. Fail2ban čte logy a po opakovaných špatných požadavcích banuje adresy na firewallu - a http10.log z části o logování je jednoduchý vstup pro jail, pokud chceš natrvalo banovat opakované pachatele. Pravidla firewallu, na kterých záleží se věnují zavírání každého portu, který nemusí být otevřený, což odstraní mnohem víc útočné plochy než cokoli udělaného na vrstvě HTTP. Pro web na WordPressu se cestami, které tyto skenery obvykle hledají, zabývá zabezpečení WordPressu.

Rozumné pořadí práce tedy je: nastav záchytný výchozí server, týden loguj HTTP/1.0, přidej výjimky pro všechno své, co se objeví, blokuj pomocí 444 v nginx nebo 403 v Apache, otestuj curlem a nech běžet samostatný log, abys si všiml dne, kdy začne selhávat něco legitimního.

FAQ#

Ovlivní blokování HTTP/1.0 crawlery vyhledávačů?

Ne. Googlebot, Bingbot a další hlavní crawlery používají HTTP/1.1 nebo HTTP/2. Pokud se v tvém logu HTTP/1.0 během týdne logování objeví nějaký okrajový crawler, na kterém ti záleží, přidej pro něj před blokováním výjimku.

Mám v nginx použít 444, nebo 403?

444 pro provoz, o kterém jsi si jistý, že je nevyžádaný, protože klientovi nedá nic - ani potvrzení, jaký server odpověděl. 403 nebo 426, když může být zasažen skutečný klient a jeho majitel potřebuje čitelnou chybu, aby zjistil, co se změnilo.

Blokuje HttpProtocolOptions Require1.0 v Apache HTTP/1.0?

Ne, navzdory názvu. Vyžaduje verzi alespoň 1.0, což znamená, že odmítá HTTP/0.9. Chceš-li odmítnout samotné HTTP/1.0, použij podmínku mod_rewrite nad %{SERVER_PROTOCOL} nebo blok <If> s Require all denied.

Můžu blokovat požadavky bez hlavičky Host, ale povolit HTTP/1.0 s ní?

Ano, a často je to lepší kompromis. Záchytný default_server v nginx nebo první načtený VirtualHost v Apache, který všechno zakazuje, přijme každý požadavek, který nejmenuje žádný známý web, včetně všech požadavků bez Host. Požadavky, které jmenují tvoji doménu, to neovlivní, ať mají jakoukoli verzi.

Proč mi po zablokování HTTP/1.0 spadl celý web?

Skoro jistě proto, že před serverem, který jsi změnil, sedí další proxy a mluví s ním přes HTTP/1.0 - nginx to dělá ve výchozím nastavení. Odeber pravidlo z backendu, uplatni ho na vnější proxy a nastav tam proxy_http_version 1.1.

Pomůže to proti DDoS útoku?

Ne nijak podstatně. Záplavový provoz HTTP/1.0 nepotřebuje a než nginx spojení zavře, už bylo přijato. Objemové útoky se řeší před webovým serverem, ne v jeho konfiguraci.