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.1
  • map $http_authorization $api_korisnik — ključ po korisniku
  • openssl rand -hex 16 — pravljenje ključa
  • limit_req_zone $api_korisnik — ograničenje po korisniku, ne po adresi
  • limit_conn — važnije od broja zahteva
  • ssh -L 11434:localhost:11434 — najbolje rešenje za jednog korisnika
  • ssl_verify_client on — mTLS
  • location /api/ { return 403; } — zatvaranje svega osim /v1/
  • $api_korisnik u logu — trag ko je šta tražio
  • grep ' 401 ' — neuspeli pokušaji
  • nmap — provera da portovi nisu vidljivi
  • Ollama nema autentifikaciju — proxy je jedini sloj
  • Ključ nije zaštita od prompt injectiona

Vežba

  1. Napravi mapu sa dva ključa i potvrdi da svaki daje svoje ime u logu.
  2. Pošalji zahtev bez ključa i sa pogrešnim ključem, pa uporedi kodove odgovora.
  3. Zatvori Ollamine sopstvene putanje i potvrdi da /api/pull vraća 403.
  4. Postavi SSH tunel i pristupi servisu bez ijednog ključa.
  5. Napiši skriptu koja iz loga računa broj zahteva i ukupno vreme po korisniku.
  6. 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:

Comments

Popular posts from this blog

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

Konverzija tipova podataka u Pythonu

groupadd