Zaštita API-ja: ključevi, firewall i ograničavanje pristupa
Otvoren port sa modelom nije isto što i otvoren port sa veb sajtom. Ovde neko ko dođe do adrese ne gleda tvoje stranice — koristi tvoju karticu, tvoju struju i, ako je model povezan sa alatima ili dokumentima, tvoje podatke.
Proxy iz prethodnog teksta je rešio prenos. Ovim tekstom se rešava pitanje ko sme da prođe.
Šta zapravo štitiš
- Karticu. Jedan skript u petlji zauzme resurs koji si kupio.
- Podatke. Sistemski prompt, priloženi dokumenti i istorija razgovora sadrže više nego što misliš.
- Radnje. Ako model ima alate, pristup API-ju je pristup tim alatima.
- Odgovornost. Servis koji generiše šta god ko zatraži, sa tvoje adrese, tvoj je problem.
Ollama nema nikakvu autentifikaciju i neće je dobiti — po zamisli je alat za lokalnu upotrebu. llama.cpp i vLLM imaju jedan zajednički ključ, što je bolje od ničega i lošije od onoga što ti treba.
Zaključak je jednostavan: zaštita se ne dešava u servisu, nego iznad njega.
Slojevi
1. Servis sluša samo na 127.0.0.1 2. Firewall propušta samo 80 i 443 3. Proxy traži ključ ili sertifikat 4. Ograničenje broja zahteva po korisniku 5. Log sa tragom ko je šta tražio
Nijedan sloj sam nije dovoljan, ali prvi je jedini bez koga svi ostali otpadaju. Provera:
$ sudo ss -tulpn | grep -E '11434|8000|3000' tcp LISTEN 0 4096 127.0.0.1:11434 0.0.0.0:*
Ako vidiš 0.0.0.0, stani i to popravi pre svega ostalog.
Ključevi kroz Nginx
Ollama zaglavlje Authorization ignoriše, pa proveru radi proxy. Najjednostavnija varijanta sa jednim ključem:
location /api/ {
if ($http_authorization != "Bearer tajni-kljuc-ovde") {
return 401;
}
proxy_pass http://127.0.0.1:11434/;
proxy_buffering off;
proxy_read_timeout 600s;
}
Radi, ali ima dve mane: ključ stoji u konfiguraciji u čitljivom obliku, i svi korisnici dele isti. Za više korisnika ide mapa:
$ sudo nano /etc/nginx/nginx.conf
map $http_authorization $api_korisnik {
default "";
"Bearer k_alen_a1b2c3d4" "alen";
"Bearer k_marko_e5f6g7h8" "marko";
"Bearer k_skripta_i9j0k1l2" "skripta-backup";
}
location /api/ {
if ($api_korisnik = "") {
return 401 '{"error":"neispravan ključ"}';
}
proxy_pass http://127.0.0.1:11434/;
proxy_set_header X-Korisnik $api_korisnik;
proxy_buffering off;
proxy_read_timeout 600s;
}
Prednost je što sada znaš ko je ko, možeš da oduzmeš pojedinačan ključ brisanjem jednog reda, i imaš ime za log i za ograničenja.
$ sudo nginx -t && sudo systemctl reload nginx
Pravljenje ključeva
$ echo "k_alen_$(openssl rand -hex 16)" k_alen_3f8a2b91c7d45e6f0a1b2c3d4e5f6a7b
Prefiks sa imenom je namerno — kad ključ negde iscuri, odmah znaš čiji je. Fajl sa mapom zaštiti:
$ sudo chmod 640 /etc/nginx/nginx.conf
Ključeve nemoj slati poštom ni ostavljati u istoriji ljuske. Kod korisnika idu u fajl sa ograničenim pravima:
$ echo 'export AI_API_KEY=k_alen_3f8a...' > ~/.ai_env $ chmod 600 ~/.ai_env
$ source ~/.ai_env $ curl https://ai.primer.rs/api/v1/models -H "Authorization: Bearer $AI_API_KEY"
Više o promenljivama okruženja ima u tekstu o .bashrc fajlu.
Ograničenje po korisniku
Sa imenom iz mape, ograničenja se vezuju za korisnika umesto za adresu — što je tačnije, jer više ljudi ume da deli istu izlaznu adresu:
limit_req_zone $api_korisnik zone=po_korisniku:10m rate=30r/m; limit_conn_zone $api_korisnik zone=veze_korisnika:10m;
location /api/ {
limit_req zone=po_korisniku burst=10 nodelay;
limit_conn veze_korisnika 3;
limit_req_status 429;
...
}
Ograničenje istovremenih veza je ovde važnije od broja zahteva. Trideset kratkih upita u minutu je bezazleno; tri duga toka odjednom zauzimaju celu karticu.
Alternativa: bez ključeva uopšte
Ključevi su potrebni kad servis mora da bude na javnoj adresi. Često ne mora.
SSH tunel
$ ssh -L 11434:localhost:11434 korisnik@server
$ curl http://localhost:11434/v1/models
Servis ostaje potpuno zatvoren, a autentifikacija je SSH ključ koji ionako imaš. Za jednog ili dva korisnika ovo je najbolje rešenje — nema šta da iscuri, nema šta da se održava.
Trajno, kroz ~/.ssh/config:
Host ai
HostName server.primer.rs
User alen
LocalForward 11434 localhost:11434
$ ssh -N ai &
VPN
Za tim je WireGuard čistiji od ključeva u proxy-ju. Servis se veže za VPN adresu:
Environment="OLLAMA_HOST=10.8.0.1:11434"
Ko je u VPN-u, ima pristup; ko nije, ne vidi ni port. Nema ključeva koje treba deliti, menjati i oduzimati.
Sertifikat na klijentu
Kad ipak mora javna adresa, mTLS je jači od ključa u zaglavlju:
$ openssl genrsa -out ca.key 4096
$ openssl req -x509 -new -key ca.key -days 1825 -out ca.crt -subj "/CN=AI interni CA"
$ openssl genrsa -out alen.key 2048
$ openssl req -new -key alen.key -out alen.csr -subj "/CN=alen"
$ openssl x509 -req -in alen.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out alen.crt -days 365
server {
listen 443 ssl;
server_name api.primer.rs;
ssl_certificate /etc/letsencrypt/live/api.primer.rs/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.primer.rs/privkey.pem;
ssl_client_certificate /etc/nginx/ca.crt;
ssl_verify_client on;
location / {
proxy_pass http://127.0.0.1:11434;
proxy_set_header X-Korisnik $ssl_client_s_dn_cn;
proxy_buffering off;
proxy_read_timeout 600s;
}
}
$ curl https://api.primer.rs/v1/models --cert alen.crt --key alen.key
Sertifikat se ne može slučajno zalepiti u poruku kao ključ, ima rok trajanja i može se opozvati. Cena je što ga treba postaviti na svaku klijentsku mašinu.
Šta kada biraš
| Situacija | Rešenje |
|---|---|
| Jedan korisnik, ti | SSH tunel |
| Tim u firmi | VPN |
| Skripte na više servera | Ključevi u proxy-ju |
| Javna adresa, osetljivi podaci | mTLS |
| Ljudi kroz pregledač | Open WebUI sa nalozima |
Trag ko je šta tražio
log_format ai_api '$time_iso8601 $api_korisnik $remote_addr '
'$status $request_time $body_bytes_sent "$request"';
access_log /var/log/nginx/ai-api.log ai_api;
2026-08-22T14:23:01+02:00 alen 10.8.0.5 200 12.483 1247 "POST /api/v1/chat/completions" 2026-08-22T14:23:44+02:00 skripta-backup 10.8.0.9 200 3.102 412 "POST /api/v1/chat/completions" 2026-08-22T14:24:12+02:00 - 203.0.113.99 401 0.000 31 "POST /api/v1/chat/completions"
Potrošnja po korisniku za protekli dan:
$ awk '$4 == 200 {broj[$2]++; vreme[$2]+=$5}
END {for (k in broj) printf "%-18s %4d zahteva %8.1f s\n", k, broj[k], vreme[k]}' \
/var/log/nginx/ai-api.log | sort -k2 -rn
alen 134 zahteva 892.3 s skripta-backup 47 zahteva 145.8 s marko 12 zahteva 78.1 s
Neuspeli pokušaji, koji brzo pokažu da li te neko traži:
$ grep ' 401 ' /var/log/nginx/ai-api.log | awk '{print $3}' | sort | uniq -c | sort -rn | head
Ako se ista adresa ponavlja, dodaj je u blokadu:
$ sudo ufw deny from 203.0.113.99
Za automatsko blokiranje ide fail2ban, sa pravilom nad ovim logom.
Šta ne sme napolje
Nekoliko putanja koje otkrivaju više nego što treba:
location /api/v1/models {
return 403;
}
location ~ ^/api/(pull|push|delete|create) {
return 403;
}
Ollamine sopstvene putanje dozvoljavaju preuzimanje i brisanje modela bez ikakve provere. Ako izlažeš Ollamu, propusti isključivo /v1/ putanje, a ostalo zatvori:
location /api/v1/ {
# provera ključa, proxy_pass ...
}
location /api/ {
return 403;
}
Nginx bira najprecizniji location, pa uži blok ima prednost nad širim.
Provera
$ curl -s -o /dev/null -w '%{http_code}\n' https://ai.primer.rs/api/v1/models
401
$ curl -s -o /dev/null -w '%{http_code}\n' https://ai.primer.rs/api/v1/models \
-H "Authorization: Bearer $AI_API_KEY"
200
$ curl -s -o /dev/null -w '%{http_code}\n' https://ai.primer.rs/api/pull \
-H "Authorization: Bearer $AI_API_KEY"
403
Da se portovi ne vide spolja:
$ nmap -p 11434,8000,3000 ai.primer.rs
Da ograničenje radi:
$ for i in $(seq 1 50); do
curl -s -o /dev/null -w '%{http_code} ' https://ai.primer.rs/api/v1/models \
-H "Authorization: Bearer $AI_API_KEY"
done; echo
Posle nekog broja odgovora treba da počnu 429. Ako svih pedeset prođe, ograničenje nije aktivno.
Ova četiri testa pokreni posle svake izmene konfiguracije. Sastavi ih u jednu skriptu, po uzoru na tekst o skriptama za produkciju.
Šta ovo ne rešava
Autentifikacija određuje ko sme da uđe. Ne određuje šta model radi sa onim što dobije.
Korisnik sa ispravnim ključem i dalje može da izvuče sistemski prompt, da modelu podmetne instrukciju kroz dokument koji priloži, ili da preko alata dođe do nečega što nije trebalo. To je zaseban problem i tema teksta o prompt injection napadima.
Praktični scenariji
Scenario 1: ključ radi, ali svi dele isti
Pređi na mapu sa imenima. Bez toga ne znaš ko troši i ne možeš da oduzmeš pristup jednom čoveku.
Scenario 2: ključ je iscureo u repozitorijum
Obriši red iz mape i ponovo učitaj Nginx. Zatim proveri log da vidiš je li korišćen sa nepoznate adrese.
Scenario 3: 401 uprkos ispravnom ključu
Poređenje je doslovno, sa razmakom posle Bearer. Proveri šta stvarno stiže:
$ curl -v ... 2>&1 | grep -i authorization
Scenario 4: neko preuzima modele preko API-ja
Ollamine sopstvene putanje su otvorene. Zatvori sve osim /v1/.
Scenario 5: ograničenje pogađa i legitimne korisnike
Podigni burst, ne rate. Prvi dozvoljava kratke navale, drugi trajno podiže granicu.
Scenario 6: skripta radi lokalno, ne radi kroz proxy
Adresa i ključ se razlikuju od lokalnog poziva. Drži ih u promenljivama okruženja, ne u kodu.
Kratka referenca
ss -tulpn— servis mora slušati na 127.0.0.1map $http_authorization $api_korisnik— ključ po korisnikuopenssl rand -hex 16— pravljenje ključalimit_req_zone $api_korisnik— ograničenje po korisniku, ne po adresilimit_conn— važnije od broja zahtevassh -L 11434:localhost:11434— najbolje rešenje za jednog korisnikassl_verify_client on— mTLSlocation /api/ { return 403; }— zatvaranje svega osim/v1/$api_korisniku logu — trag ko je šta tražiogrep ' 401 '— neuspeli pokušajinmap— provera da portovi nisu vidljivi- Ollama nema autentifikaciju — proxy je jedini sloj
- Ključ nije zaštita od prompt injectiona
Vežba
- Napravi mapu sa dva ključa i potvrdi da svaki daje svoje ime u logu.
- Pošalji zahtev bez ključa i sa pogrešnim ključem, pa uporedi kodove odgovora.
- Zatvori Ollamine sopstvene putanje i potvrdi da
/api/pullvraća 403. - Postavi SSH tunel i pristupi servisu bez ijednog ključa.
- Napiši skriptu koja iz loga računa broj zahteva i ukupno vreme po korisniku.
- Izazovi ograničenje petljom i pronađi posle kog zahteva počinju odgovori 429.
Sledeći tekst u serijalu: Python skripta koja komunicira sa lokalnim modelom
Povezano:
- AI na Linux serveru — pregled celog serijala
- Nginx reverse proxy i HTTPS — prethodni tekst
- OpenAI-kompatibilan API — struktura zahteva koje štitiš
- firewalld — pravila na nivou sistema
- Udaljeni pristup Linux sistemu — SSH i tunelovanje
Comments
Post a Comment