nginx: Kontrola pristupa i zaštita

Ciljevi lekcije

Nakon ove lekcije trebalo bi da:

  • ograničiš pristup po IP adresi i kombinuješ to sa lozinkom
  • postaviš ograničenje brzine zahteva bez blokiranja legitimnih korisnika
  • razumeš burst i nodelay parametre, jer se najčešće pogrešno podese
  • povežeš nginx sa fail2ban-om
  • znaš koje od ovih mera prestaju da rade iza CDN-a i zašto

1. Ograničavanje po IP adresi

location /admin/ {
    allow 192.0.2.0/24;
    allow 10.0.0.0/8;
    allow 127.0.0.1;
    deny all;

    proxy_pass http://backend;
}

Pravila se proveravaju odozgo naniže i prvo poklapanje pobeđuje. Zato deny all uvek ide poslednji — da je na vrhu, blokirao bi sve.

Ista logika radi i u server i u http kontekstu, uz podsetnik na nasleđivanje iz lekcije 3.

Kritično: iza proxy-ja ovo ne radi

Ako je ispred nginx-a CDN, load balancer ili bilo koji drugi posrednik, $remote_addr je adresa tog posrednika, ne korisnika. Tvoje allow pravilo tada ili propušta sve, ili blokira sve — u zavisnosti od toga da li si slučajno naveo opseg posrednika.

Rešenje je realip modul iz lekcije 7:

set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Proveri da radi pre nego što se osloniš na kontrolu pristupa:

curl -H "X-Forwarded-For: 1.2.3.4" http://localhost/
sudo tail -1 /var/log/nginx/access.log

U logu mora pisati adresa koju očekuješ.

Veće liste — geo modul

Za desetine opsega, geo je pregledniji i brži:

geo $dozvoljen {
    default        0;
    192.0.2.0/24   1;
    198.51.100.0/24 1;
    10.0.0.0/8     1;
}

server {
    location /admin/ {
        if ($dozvoljen = 0) {
            return 403;
        }
        proxy_pass http://backend;
    }
}

Opsege možeš držati u zasebnom fajlu i uključiti ga:

geo $dozvoljen {
    default 0;
    include /etc/nginx/dozvoljene-adrese.conf;
}

2. Basic autentifikacija

sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd admin        # -c samo za prvi unos
sudo htpasswd /etc/nginx/.htpasswd drugi-korisnik  # bez -c za dodavanje
sudo chown root:www-data /etc/nginx/.htpasswd
sudo chmod 640 /etc/nginx/.htpasswd

Pazi na -c — ponovna upotreba prepisuje ceo fajl i briše sve postojeće korisnike.

location /admin/ {
    auth_basic "Zatvorena zona";
    auth_basic_user_file /etc/nginx/.htpasswd;

    proxy_pass http://backend;
}

Basic auth koristi isključivo preko HTTPS-a. Kredencijali se šalju kao Base64, što je kodiranje, ne šifrovanje — svako ko presretne saobraćaj čita lozinku u čistom obliku.

Isključivanje za pojedine putanje

location /admin/ {
    auth_basic "Zatvorena zona";
    auth_basic_user_file /etc/nginx/.htpasswd;

    location /admin/javno/ {
        auth_basic off;
    }
}

Kombinovanje IP-a i lozinke

Direktiva satisfy određuje da li su potrebna oba uslova ili je dovoljan jedan.

Iz kancelarije bez lozinke, spolja sa lozinkom:

location /admin/ {
    satisfy any;

    allow 192.0.2.0/24;
    deny all;

    auth_basic "Zatvorena zona";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

Oba uslova obavezna — i prava mreža i lozinka:

location /admin/ {
    satisfy all;

    allow 192.0.2.0/24;
    deny all;

    auth_basic "Zatvorena zona";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

satisfy all je podrazumevano. Prva varijanta je češće ono što ljudi zapravo žele.

3. Ograničavanje brzine zahteva

Ovo je najkorisniji sloj zaštite koji nginx nudi — brani od preplavljivanja, od pogađanja lozinki i od botova koji struže sadržaj.

Definicija zone

http {
    limit_req_zone $binary_remote_addr zone=opsti:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=prijava:10m rate=5r/m;
    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
}

Zašto $binary_remote_addr, a ne $remote_addr: binarni oblik zauzima 4 bajta za IPv4 umesto do 15 znakova. U zoni od 1 MB staje oko 16.000 adresa, pa je 10 MB dovoljno za 160.000 — više nego dovoljno za većinu sajtova.

Brzina se navodi po sekundi (r/s) ili po minutu (r/m). Interno nginx radi u milisekundama: 10r/s znači jedan zahtev na svakih 100 ms, ne deset odjednom pa pauza.

Primena

server {
    location / {
        limit_req zone=opsti burst=20 nodelay;
        limit_req_status 429;
    }

    location /prijava {
        limit_req zone=prijava burst=3;
        limit_req_status 429;
    }
}

burst i nodelay — deo koji se najčešće pogrešno podesi

Bez burst: svaki zahtev preko dozvoljene brzine se odmah odbija. Ovo je prestrogo — normalno učitavanje stranice šalje desetine zahteva u kratkom naletu i legitimni korisnik odmah dobija grešku.

Sa burst=20: do dvadeset zahteva preko brzine se stavlja u red i obrađuje sporije, tempom zadate brzine. Ništa se ne odbija dok se red ne prepuni, ali korisnik oseća kašnjenje.

Sa burst=20 nodelay: tih dvadeset se obrađuje odmah, bez kašnjenja, a mesta u redu se oslobađaju zadatom brzinom. Ovo je gotovo uvek ono što želiš za običan web saobraćaj — nalet prolazi normalno, a trajno prekoračenje se blokira.

delay=10 burst=20 je sredina: prvih deset odmah, narednih deset uz kašnjenje, preko toga odbijanje.

Praktično pravilo: za obične stranice burst postavi na 15–20 sa nodelay. Za osetljive putanje poput prijave koristi malu brzinu i mali burst bez nodelay, jer tu kašnjenje radi u tvoju korist.

limit_req_status 429 — podrazumevani odgovor je 503, što znači „server nedostupan" i može navesti pretraživače da misle da si u kvaru. Kod 429 („previše zahteva") je tačniji i klijenti ga ispravno tumače.

Testiranje pre puštanja

Prvo samo posmatraj, bez blokiranja:

limit_req zone=opsti burst=20 nodelay;
limit_req_log_level warn;

Pusti nekoliko dana i prebroj koliko je legitimnih korisnika pogođeno:

sudo grep "limiting requests" /var/log/nginx/error.log | wc -l
sudo grep "limiting requests" /var/log/nginx/error.log \
  | grep -oP 'client: \K[0-9.]+' | sort | uniq -c | sort -rn | head

Ako se u listi pojavljuju adrese tvojih korisnika, brzina je prestroga.

Izuzimanje pojedinih adresa

geo $bez_limita {
    default        0;
    192.0.2.0/24   1;      # kancelarija
    10.0.0.0/8     1;      # monitoring
}

map $bez_limita $limit_kljuc {
    0 $binary_remote_addr;
    1 "";                   # prazan ključ = nema ograničenja
}

limit_req_zone $limit_kljuc zone=opsti:10m rate=10r/s;

Prazan ključ isključuje ograničenje za tu adresu.

4. Ograničavanje broja konekcija

http {
    limit_conn_zone $binary_remote_addr zone=po_ip:10m;
    limit_conn_zone $server_name zone=po_sajtu:10m;
}

server {
    limit_conn po_ip 10;
    limit_conn po_sajtu 1000;
}

Ovo ograničava istovremene konekcije, ne brzinu. Brani od klijenata koji otvore stotinu konekcija i drže ih otvorenim.

Budi umeren sa brojem — browser otvara više konekcija za jednu stranicu, a korisnici iza NAT-a dele adresu. Vrednost ispod 10 po IP adresi lako pogađa legitimne korisnike.

5. Ograničavanje brzine prenosa

location /preuzimanja/ {
    limit_rate_after 10m;
    limit_rate 500k;
}

Prvih 10 MB ide punom brzinom, ostatak na 500 KB/s. Time male fajlove i stranice ne usporavaš, a sprečavaš da jedan korisnik zasiti vezu preuzimanjem velike datoteke.

6. Blokiranje po zaglavljima

Korisnički agenti

Umesto if direktive, koristi map — čistije je i ne pati od poznatih problema if-a unutar location bloka:

map $http_user_agent $blokiran_agent {
    default                        0;
    ""                             1;      # prazan User-Agent
    ~*(sqlmap|nikto|nmap|masscan)  1;
    ~*(semrushbot|ahrefsbot|mj12bot) 1;    # agresivni SEO botovi
}

server {
    if ($blokiran_agent) {
        return 403;
    }
}

Ne očekuj previše od ovoga. Ozbiljan napadač postavlja User-Agent na onaj Chrome-ov. Ovo je korisno protiv automatizovanih alata koji se ne trude i protiv botova koji ti troše propusni opseg.

Ograničavanje metoda

location / {
    limit_except GET HEAD POST {
        deny all;
    }
}

Blokira PUT, DELETE, TRACE i ostalo što ti aplikacija verovatno ne koristi.

Blokiranje putanja koje ne postoje na tvom sajtu

location ~* /(wp-admin|wp-login|xmlrpc\.php|\.env|phpmyadmin) {
    deny all;
    access_log off;
    log_not_found off;
}

Ako ne koristiš WordPress, svaki zahtev za /wp-login.php je pokušaj napada. Blokiranje uz isključeno logovanje čisti logove od šuma.

7. fail2ban

nginx blokira pojedinačan zahtev; fail2ban blokira adresu na nivou firewall-a nakon više neuspelih pokušaja.

sudo apt install fail2ban

Napravi /etc/fail2ban/jail.local (ne diraj jail.conf, prepisuje se pri nadogradnji):

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 192.0.2.0/24

[sshd]
enabled = true

[nginx-http-auth]
enabled  = true
port     = http,https
logpath  = /var/log/nginx/*error.log

[nginx-limit-req]
enabled  = true
port     = http,https
logpath  = /var/log/nginx/*error.log
findtime = 60
maxretry = 10
bantime  = 1h

[nginx-botsearch]
enabled  = true
port     = http,https
logpath  = /var/log/nginx/*error.log
maxretry = 3

nginx-limit-req se oslanja na poruke koje generiše limit_req — ta dva mehanizma se lepo dopunjuju. nginx odbija zahteve, fail2ban nakon deset odbijanja izbacuje adresu na sat vremena i time štedi resurse.

Obavezno stavi svoju adresu u ignoreip pre pokretanja. Zaključavanje samog sebe je uobičajena greška.

sudo systemctl enable --now fail2ban
sudo fail2ban-client status
sudo fail2ban-client status nginx-limit-req

Ručno odblokiravanje:

sudo fail2ban-client set nginx-limit-req unbanip 192.0.2.55

Sopstveni filter

Za blokiranje po specifičnom obrascu, napravi /etc/fail2ban/filter.d/nginx-403.conf:

[Definition]
failregex = ^<HOST>.*"(GET|POST|HEAD).*" 403
ignoreregex =

I odgovarajući jail koji čita access log. Testiraj filter pre uključivanja:

sudo fail2ban-regex /var/log/nginx/access.log \
  /etc/fail2ban/filter.d/nginx-403.conf

Komanda ispisuje koliko je redova pogođeno — ako je nula, regularni izraz ne odgovara tvom formatu loga.

Iza CDN-a fail2ban ne radi

Ovo je važno. Ako je ispred tebe Cloudflare ili sličan servis, u logu je adresa CDN-a. fail2ban će blokirati CDN, čime obaraš sajt za sve korisnike.

Dva rešenja: podesi realip (odeljak 1) tako da u logu bude prava adresa, ili koristi mehanizme blokiranja koje nudi sam CDN.

8. Zaštita od sporih napada

Slowloris i srodni napadi drže konekcije otvorenim šaljući podatke vrlo sporo, dok ne potroše sve dostupne konekcije.

http {
    client_header_timeout 10s;
    client_body_timeout   10s;
    send_timeout          10s;
    keepalive_timeout     30s;

    client_max_body_size       10m;
    client_body_buffer_size    16k;
    large_client_header_buffers 4 8k;
}

Uz limit_conn iz odeljka 4, ovo pokriva većinu takvih pokušaja. nginx je zbog svoje arhitekture (lekcija 1) prirodno otporniji na ovu vrstu napada od servera koji troše proces po konekciji, ali tajmauti i dalje vrede.

Budi pažljiv sa client_body_timeout ako imaš korisnike na sporim vezama koji otpremaju velike fajlove — 10 sekundi je pauza između dva primljena komada, ne ukupno vreme, ali vredi proveriti.

9. Šta ovim merama ne postižeš

Iskrena procena, da ne bi gradio lažan osećaj sigurnosti:

Ovo nije zaštita od DDoS-a. Ozbiljan volumetrijski napad zasićuje vezu pre nego što saobraćaj stigne do nginx-a. Za to ti treba usluga na mrežnom nivou (CDN sa zaštitom, provajderov scrubbing).

Ovo nije zamena za bezbedan kod. SQL injekcija, XSS i greške u autorizaciji se rešavaju u aplikaciji. nginx može da oteža, ne da spreči.

Ovo nije WAF. Ako ti treba pregled sadržaja zahteva, postoji ModSecurity sa OWASP CRS pravilima — ali to je zaseban projekat sa sopstvenim troškom održavanja i lažnim pozitivima.

Ono što ovim merama jeste postignuto: podignut prag za automatizovane napade, zaštita od pogađanja lozinki, kontrola nad time ko pristupa osetljivim delovima, i čistiji logovi u kojima se pravi problem lakše primeti.


Praktična vežba

1. Ograniči pristup po IP adresi i proveri obe strane:

location /admin/ {
    allow 127.0.0.1;
    deny all;
    return 200 "admin zona\n";
}
curl -s -H "Host: test.local" http://localhost/admin/
curl -s -H "Host: test.local" -H "X-Forwarded-For: 1.2.3.4" http://localhost/admin/

Drugi zahtev i dalje prolazi — jer realip nije podešen. Dodaj ga i ponovi, pa objasni sebi razliku.

2. Postavi basic auth i testiraj:

sudo htpasswd -bc /etc/nginx/.htpasswd test lozinka123
curl -s -H "Host: test.local" http://localhost/admin/            # 401
curl -su test:lozinka123 -H "Host: test.local" http://localhost/admin/

3. Isprobaj satisfy any — ukloni allow 127.0.0.1 i uveri se da lozinka i dalje otvara pristup, pa vrati allow i uveri se da lozinka više nije potrebna.

4. Postavi rate limiting i vidi razliku između podešavanja. Prvo bez burst:

limit_req_zone $binary_remote_addr zone=test:10m rate=2r/s;

location /limit/ {
    limit_req zone=test;
    limit_req_status 429;
    return 200 "prosao\n";
}
for i in $(seq 1 10); do
  curl -s -o /dev/null -w "%{http_code} " -H "Host: test.local" http://localhost/limit/
done; echo

Videćeš uglavnom 429. Sada dodaj burst=5 i ponovi — zahtevi prolaze, ali sporo. Zatim dodaj nodelay i ponovi — prolaze odmah. Izmeri i ukupno vreme sa time.

5. Pogledaj šta se upisalo u error log:

sudo grep "limiting requests" /var/log/nginx/error.log | tail -5

To je tačno onaj zapis koji fail2ban traži.

6. Postavi limit_conn i testiraj sa paralelnim zahtevima:

for i in $(seq 1 20); do
  curl -s -o /dev/null -w "%{http_code} " -H "Host: test.local" \
    http://localhost/spori & 
done; wait; echo

7. Podesi fail2ban sa nginx-limit-req jail-om. Ne zaboravi ignoreip. Zatim izazovi blokiranje sa druge mašine ili privremeno ukloni svoju adresu iz ignoreip:

sudo fail2ban-client status nginx-limit-req
sudo iptables -L -n | grep f2b
sudo fail2ban-client set nginx-limit-req unbanip TVOJA_ADRESA

8. Testiraj sopstveni filter sa fail2ban-regex i proveri da hvata redove koje očekuješ.

9. Blokiraj WordPress putanje i proveri da ne završavaju u logu:

curl -s -o /dev/null -w "%{http_code}\n" -H "Host: test.local" \
  http://localhost/wp-login.php
sudo tail -3 /var/log/nginx/access.log

Rezime

  • allow/deny se proveravaju odozgo naniže, prvo poklapanje pobeđuje — deny all uvek poslednji.
  • Iza CDN-a ili load balancera kontrola pristupa po IP-u ne radi bez realip modula. Isto važi i za fail2ban.
  • Basic auth šalje lozinku Base64 kodiranu — koristi ga isključivo preko HTTPS-a.
  • satisfy any znači „IP ili lozinka", satisfy all znači „oba". Podrazumevano je all.
  • $binary_remote_addr u zoni umesto $remote_addr — troši četvrtinu memorije.
  • burst=20 nodelay je pravi izbor za obične stranice; za prijavu koristi malu brzinu bez nodelay.
  • limit_req_status 429 umesto podrazumevanog 503.
  • Pusti rate limiting prvo u posmatranje i prebroj koliko legitimnih korisnika pogađa.
  • fail2ban nginx-limit-req jail se oslanja na poruke koje generiše limit_req — dva sloja iste odbrane.
  • Uvek dodaj svoju adresu u ignoreip pre pokretanja fail2ban-a.
  • Ništa od ovoga nije zaštita od volumetrijskog DDoS-a niti zamena za bezbedan aplikacioni kod.

Pitanja za proveru

  1. Zašto deny all mora biti poslednje pravilo?
  2. Sajt je iza Cloudflare-a i allow 192.0.2.0/24 propušta sve. Zašto i kako to rešavaš?
  3. U čemu je razlika između burst=20 i burst=20 nodelay?
  4. Zašto je za putanju za prijavu bolje ne koristiti nodelay?
  5. Šta znači satisfy any uz allow i auth_basic u istom bloku?
  6. Zašto se u limit_req_zone koristi $binary_remote_addr?
  7. Šta se dogodi ako pokreneš fail2ban iza CDN-a bez podešenog realip?
  8. Koje vrste napada ove mere ne rešavaju?

Sledeća lekcija: hardening — skrivanje verzije, sigurnosna zaglavlja, AppArmor, izolacija procesa i lista za proveru zatečenog servera.

Comments

Popular posts from this blog

Početak u Linuxu — šta je i zašto se uči

Konverzija tipova podataka u Pythonu

groupadd