Każdego dnia moje logi serwera rejestrują tysiące automatycznych skanów podatności. Same requesty wyglądają dość groźnie:
/wp-admin/wp.php/111.php/code.php/.git/config/i.php/admin/api/.env/kal.php
Zaskoczyła mnie nie ich liczba, ale reakcja serwera. Kiedy obserwowałem ten ruch, obciążenie CPU nie zmieniało się w zauważalny sposób.
Ta obserwacja jest prawdziwa, ale moje pierwotne wyjaśnienie było zbyt kategoryczne. Statyczne serwowanie nie jest magiczną tarczą, a odpowiedź 404 też ma koszt. Rzeczywista zaleta mojej architektury jest bardziej konkretna: wiele oportunistycznych probe’ów jest tanich, bo w publicznym request path prawie nie ma logiki aplikacyjnej po stronie serwera, do której mogłyby dotrzeć.
To są probe’y, a nie dowód kompromitacji
Widząc /wp-admin/wp.php albo /111.php, łatwo nazwać każdy request „exploitem”. Technicznie nie jest to precyzyjne. Najczęściej widzę automatyczne probing: boty próbują popularnych nazw plików i endpointów i sprawdzają, co odpowie.
Część ścieżek wyraźnie celuje w WordPressa lub PHP. Inne, jak /.git/config czy /api/.env, szukają przypadkowo ujawnionej konfiguracji albo sekretów. /admin jest jeszcze bardziej ogólne i może być legalną trasą. Podejrzany request mówi mi, czego szuka scanner, a nie że podatność faktycznie istnieje.
W moim przypadku kluczowa jest architektura: publiczny serwis to prerenderowany statyczny HTML generowany przez Next.js i serwowany bezpośrednio przez Nginx.
Dlaczego ten ruch jest tani na moim stacku
Dla stron, o których piszę, za każdym requestem nie stoi runtime PHP ani baza danych. Nginx serwuje istniejące pliki. Jeżeli scanner prosi o /111.php, a pliku nie ma, nie uruchamia się aplikacja PHP, nie wykonuje się kod WordPressa i nie powstaje query do bazy. Request po prostu kończy się miss i 404.
To zmienia model kosztu. W aplikacji dynamicznej request może przejść przez routing, framework, autoryzację, rendering, bazę danych albo inne usługi. Mój statyczny path ma dużo mniej ruchomych części. Nginx nadal musi przyjąć połączenie, przetworzyć HTTP, znaleźć zasób, zapisać log i odesłać odpowiedź, ale pracy aplikacyjnej jest mało.
Korzystam też z agresywnego cachowania, co pomaga przy normalnym serwowaniu statycznych treści. Nie przypisywałbym jednak cache’owi każdego losowego 404. Nowy bezsensowny URI może nadal przejść przez zwykłe przetwarzanie Nginx i filesystem lookup, zanim serwer zwróci 404.
Ważna korekta: statyczne nie znaczy „bezpieczne z definicji”
Ten fragment dziś sformułowałbym znacznie ostrożniej. Statyczna architektura usuwa wiele komponentów server-side z publicznego request path, ale nie usuwa potrzeby dbania o bezpieczeństwo.
- Ujawniony plik nadal jest ujawniony. Jeśli
.git,.env, backup albo secret trafi do publicznego katalogu, Nginx może go podać równie sprawnie jak HTML, jeśli konfiguracja tego nie blokuje. - Nginx, TLS, system operacyjny i sieć nadal są częścią attack surface. Trzeba je aktualizować i sensownie konfigurować.
- Kod client-side nadal może mieć podatności. Statyczny HTML nie czyni automatycznie bezpiecznym JavaScriptu, logiki auth w przeglądarce ani third-party scripts.
- Resource exhaustion nadal jest możliwy. Połączenia, pasmo, TLS handshakes, workerzy, log I/O i przestrzeń dyskowa są skończone. Wystarczająco duży ruch może zaszkodzić także statycznej stronie.
Poprawny wniosek nie brzmi więc „te requesty nie mają znaczenia”, tylko: w tej architekturze wiele probe’ów specyficznych dla aplikacji nie znajduje niczego, co mogłoby wykonać.
„Tysiące skanów dziennie” może brzmieć groźniej niż realne obciążenie
Logi wzmacniają wrażenie ataku, bo każda nieudana próba jest widoczna jako osobna linia. Sam dzienny licznik nie mówi jednak, czy ruch jest operacyjnie istotny. Kilka tysięcy requestów rozłożonych na 24 godziny to co innego niż ta sama liczba w krótkim burst.
CPU to też tylko jeden sygnał. W moim przypadku pozostawało stabilne, co jest użytecznym dowodem, że w czasie obserwacji ten ruch nie tworzył zauważalnego obciążenia obliczeniowego. Nie dowodzi to jednak zerowego kosztu w innych zasobach. Przy pełnej ocenie sprawdziłbym również request rate, bandwidth, aktywne połączenia, wykorzystanie workerów Nginx, latency, rozmiar logów i użycie dysku.
Czego nauczyło mnie obserwowanie tego szumu
Najważniejsza lekcja jest taka, że sama architektura może uczynić całe kategorie automatycznych ataków mało istotnymi bez pisania osobnej reguły dla każdej signature skanera. Bot może cały dzień szukać backdoorów WordPressa i PHP; jeśli publiczny request path nie ma WordPressa ani PHP, takie probe’y mają niewiele do wykorzystania.
Nie utożsamiam już jednak „nieistotne dla mojego stacku” z „nieszkodliwy ruch”. Moja obecna checklista jest prosta:
- Wiedzieć, jakie runtime’y i usługi są naprawdę osiągalne z publicznego request path.
- Trzymać sekrety, repository metadata, backupy i build artifacts poza web rootem albo jawnie je blokować.
- Traktować powtarzalne probe’y kończące się 404 jako normalny background noise Internetu, dopóki nie ma innych oznak targetingu lub wpływu.
- Nie uznawać abusive traffic za darmowy tylko dlatego, że CPU wygląda dobrze.
- Aktualizować Nginx, OS, biblioteki TLS i zależności nawet wtedy, gdy aplikacja jest statyczna.
Czego ta obserwacja nie dowodzi
Nie robiłem kontrolowanego benchmarku i nie symulowałem DDoS. Obserwowałem jedynie production logs i obciążenie serwera, gdy każdego dnia przychodziły tysiące requestów skanerów. To wystarcza do praktycznego wniosku o moim setupie, ale nie do uniwersalnej zasady „statyczne zawsze wygrywa”.
Mogę natomiast potwierdzić coś bardziej precyzyjnego: w moim Next.js static export serwowanym przez Nginx większość obserwowanego scanner traffic kończyła się jako tanie file misses i 404, zamiast docierać do runtime’u PHP lub aplikacji opartej na bazie danych. Podczas obserwacji obciążenie CPU nie zmieniło się.
To właśnie ten rodzaj resilience jest dla mnie ważny. Mniej ruchomych części nie czyni systemu niewrażliwym. Po prostu zostawia przypadkowemu internetowemu szumowi mniej miejsc, w których może zamienić się w realną pracę aplikacji.