nginx: HTTPS i TLS

Ciljevi lekcije

Nakon ove lekcije trebalo bi da:

  • izdaš i automatski obnavljaš Let's Encrypt sertifikat
  • napišeš ispravnu TLS konfiguraciju ručno, ne samo kroz certbot
  • podesiš preusmeravanje sa HTTP-a na HTTPS bez petlji
  • razumeš HSTS i rizike koje nosi
  • testiraš postavku alatima umesto da pretpostavljaš da radi

1. Šta ti treba

Za HTTPS su potrebne tri stvari: privatni ključ, sertifikat koji potvrđuje da ključ pripada tvom domenu, i konfiguracija koja ih spaja.

Sertifikate izdaju sertifikaciona tela (CA). Let's Encrypt ih izdaje besplatno i automatizovano, pa je danas podrazumevani izbor za ogromnu većinu sajtova. Komercijalne CA imaju smisla samo kada ti treba nešto što Let's Encrypt ne nudi — OV/EV validacija, sertifikati sa dužim rokom, ili garancija.

Preduslov: domen mora da pokazuje na tvoj server pre nego što pokušaš izdavanje. Let's Encrypt proverava vlasništvo tako što traži da mu odgovoriš na tom domenu. Ako DNS nije podešen, ništa neće raditi.

dig +short example.com

Rezultat mora biti IP adresa tvog servera.

2. Izdavanje sertifikata sa certbot-om

Na Ubuntu 24.04 apt paket je aktuelan i sasvim upotrebljiv:

sudo apt install certbot python3-certbot-nginx

Zatim:

sudo certbot --nginx -d example.com -d www.example.com

Certbot će:

  1. dokazati Let's Encrypt-u da kontrolišeš domen (HTTP-01 izazov — postavlja fajl na /.well-known/acme-challenge/)
  2. preuzeti sertifikat u /etc/letsencrypt/live/example.com/
  3. izmeniti tvoju nginx konfiguraciju — dodati listen 443 ssl, putanje do sertifikata i blok za preusmeravanje
  4. uraditi reload

Da bi ovo prošlo, mora već postojati server blok sa odgovarajućim server_name i port 80 mora biti otvoren.

Ako ne želiš da ti certbot dira konfiguraciju

sudo certbot certonly --webroot -w /var/www/example.com/html -d example.com -d www.example.com

Certbot samo preuzme sertifikat, a ti sam pišeš konfiguraciju. Ovo preporučujem kada imaš uređenu konfiguraciju koju ne želiš da ti automat prepravlja.

Wildcard sertifikat

Za *.example.com HTTP-01 izazov ne radi — mora DNS-01, što znači da certbot mora da upiše TXT zapis u tvoju DNS zonu:

sudo certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d example.com

Ručna varijanta traži da ti sam dodaš zapis i ne može se automatski obnavljati. Ako tvoj DNS provajder ima certbot dodatak (Cloudflare, Route53, DigitalOcean i mnogi drugi), koristi njega — obnova onda radi sama.

Obnova

Sertifikati traju 90 dana. Certbot instalira systemd tajmer koji dvaput dnevno proverava da li je vreme za obnovu:

systemctl list-timers | grep certbot
sudo certbot renew --dry-run

--dry-run prolazi kroz ceo postupak bez stvarnog izdavanja. Pokreni ga odmah nakon podešavanja — ako ovde nešto ne valja, saznaćeš sada, a ne za 89 dana.

Postavi upozorenje za slučaj da obnova zakaže. Let's Encrypt šalje mejl na adresu koju si dao, ali oslanjanje samo na to je rizično. Prosta provera:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate

3. TLS konfiguracija — ručno

Ovo je konfiguracija koju treba da razumeš čak i ako koristiš certbot:

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;

    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    root /var/www/example.com/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Prođimo kroz ono što nije očigledno.

fullchain.pem, ne cert.pem. Prvi sadrži tvoj sertifikat plus lanac do korena. Ako navedeš cert.pem, browser će raditi (jer ima keširan međusertifikat), a mobilne aplikacije i curl će prijavljivati grešku. Klasična greška koja se otkriva kasno.

ssl_protocols TLSv1.2 TLSv1.3. TLSv1 i TLSv1.1 su zastareli i treba ih isključiti — a Ubuntu-ov podrazumevani nginx.conf ih i dalje navodi. Ovo je jedna od prvih stvari koje ispravljaš na svežoj instalaciji.

ssl_prefer_server_ciphers off. Zvuči kontraintuitivno, ali je aktuelna preporuka. Moderni klijenti bolje znaju šta je brzo na njihovom hardveru — telefon bez AES ubrzanja ispravno bira ChaCha20. Uključivanje ove opcije ima smisla samo ako moraš da podržavaš stare klijente i da ih prisiliš na određene šifre.

ssl_session_tickets off. Session tickets ubrzavaju ponovna povezivanja, ali nginx podrazumevano rotira ključ samo pri restartu. Dugotrajni ključ narušava forward secrecy — neko ko kasnije dođe do njega može da dešifruje snimljeni saobraćaj. Ako ti performanse ponovnog povezivanja stvarno trebaju, uključi ih uz rotaciju ssl_session_ticket_key fajla po cron-u.

Nemoj sam sastavljati listu šifri. Koristi generator konfiguracije koji održava Mozilla (ssl-config.mozilla.org), izaberi profil „intermediate" i verziju nginx-a i OpenSSL-a koju imaš. Lista se menja kako se otkrivaju slabosti, pa ono što je bilo dobro pre tri godine danas možda nije.

O ssl_dhparam: starije preporuke su tražile generisanje DH parametara. Sa modernom listom šifri koja koristi samo ECDHE, DHE se uopšte ne koristi i taj parametar ti ne treba.

4. Preusmeravanje sa HTTP-a

Zaseban server blok za port 80, koji ne radi ništa osim preusmeravanja:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    return 301 https://$host$request_uri;
}

Zašto baš ovako:

  • return je brži i jasniji od rewrite — nema regularnih izraza ni provera
  • $host čuva domen sa kojim je korisnik došao (www ostaje www)
  • $request_uri čuva punu putanju sa query stringom; $uri bi ga izgubio
  • 301 je trajno preusmerenje. Koristi 302 dok testiraš, jer browseri 301 keširaju agresivno i teško ga je poništiti.

Ostavi prostor za ACME izazov

Ako koristiš webroot metod za obnovu, preusmeravanje sme da pokupi i verifikacioni zahtev i da obnova zakaže. Bezbednija verzija:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

^~ sprečava da neki regex blok presretne ovu putanju — pravilo iz lekcije 5.

Petlja preusmeravanja

Ako sajt upadne u beskonačno preusmeravanje nakon uključivanja HTTPS-a, uzrok je gotovo uvek aplikacija iza proxy-ja: ona vidi običan HTTP zahtev, misli da korisnik nije na HTTPS-u i preusmerava ga tamo, a nginx to opet prosleđuje kao HTTP...

Rešenje je zaglavlje iz lekcije 7:

proxy_set_header X-Forwarded-Proto $scheme;

Uz odgovarajuće podešavanje u aplikaciji da mu veruje (trust proxy u Express-u, SECURE_PROXY_SSL_HEADER u Django-u, TrustedProxies u Laravel-u).

5. HSTS

Strict-Transport-Security govori browseru da ubuduće ovom domenu pristupa isključivo preko HTTPS-a, bez ijednog HTTP zahteva:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

Ovo štiti od napada u kojima neko presreće prvi, nešifrovani zahtev.

Uvodi ga postepeno i sa razumevanjem posledica. HSTS se ne može poništiti pre isteka max-age — browser jednostavno odbija da otvori sajt preko HTTP-a, i korisnik ne može to da zaobiđe. Ako ti sertifikat istekne, sajt je nedostupan, tačka.

Razuman postupak:

  1. max-age=300 (pet minuta) i pusti nekoliko dana
  2. max-age=86400 (jedan dan), pa nedelju dana
  3. max-age=63072000 (dve godine)

includeSubDomains dodaj tek kada si siguran da svaki poddomen ima valjan sertifikat — uključujući interne alate, staging okruženja i onaj stari poddomen koji je neko napravio pre tri godine.

Preload listu izbegavaj dok nisi potpuno siguran. Dodavanje na hstspreload.org ugrađuje tvoj domen direktno u browsere; uklanjanje traje mesecima i zahteva nova izdanja browsera.

Ne zaboravi always — bez njega zaglavlje ne ide uz stranice grešaka. I podsetnik: jedan add_header u užem bloku poništava sve iz šireg (lekcija 3).

6. OCSP stapling — napomena o aktuelnom stanju

Stariji vodiči preporučuju:

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 127.0.0.53 valid=300s;

Ideja je da server sam pribavi potvrdu da sertifikat nije opozvan i priloži je uz rukovanje, pa klijent ne mora da pita CA.

Za Let's Encrypt sertifikate ovo više nema efekta. Let's Encrypt je tokom 2025. ugasio OCSP infrastrukturu i prešao na CRL, uz uvođenje kratkoročnih sertifikata. Njihovi sertifikati više ne sadrže OCSP URL, pa ssl_stapling on prosto nema šta da priloži — ne pravi štetu, ali ni korist.

Ako koristiš drugog CA koji i dalje nudi OCSP, direktive imaju smisla. Proveri:

openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -ocsp_uri

Prazan izlaz znači da OCSP-a nema. Ovo područje se menja, pa proveri aktuelno stanje kod svog CA pre nego što nešto podesiš.

7. Testiranje

Osnovna provera:

curl -vI https://example.com 2>&1 | grep -E "SSL|subject|issuer|expire"

Detalji rukovanja:

openssl s_client -connect example.com:443 -servername example.com < /dev/null

Parametar -servername je bitan — bez njega se ne šalje SNI i dobićeš sertifikat podrazumevanog server bloka, što zbunjuje pri dijagnostici.

Provera da su stari protokoli zaista isključeni:

openssl s_client -connect example.com:443 -tls1_1 < /dev/null

Očekuješ neuspeh. Ako prođe, ssl_protocols nije primenjen tamo gde misliš.

Provera datuma isteka:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates

Sveobuhvatni alati:

  • testssl.sh — skripta koja lokalno proverava sve; radi i na internim serverima
  • SSL Labs (ssllabs.com/ssltest) — najdetaljniji izveštaj, ali zahteva javno dostupan sajt i rezultati se objavljuju
git clone --depth 1 https://github.com/drwetter/testssl.sh
./testssl.sh/testssl.sh example.com

Provera da lanac sertifikata nije krnj — najlakše kroz klijenta koji nema keširane međusertifikate:

curl -v https://example.com 2>&1 | grep -i "certificate"

8. Više domena na jednom serveru

SNI omogućava da svaki domen ima svoj sertifikat na istoj IP adresi. Svaki server blok jednostavno navodi svoj:

server {
    listen 443 ssl;
    server_name prvi.rs;
    ssl_certificate /etc/letsencrypt/live/prvi.rs/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/prvi.rs/privkey.pem;
    # ...
}

server {
    listen 443 ssl;
    server_name drugi.rs;
    ssl_certificate /etc/letsencrypt/live/drugi.rs/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/drugi.rs/privkey.pem;
    # ...
}

Zajedničke TLS parametre izdvoji u snippet da se ne dupliraju:

# /etc/nginx/snippets/tls.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:...;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

Zatim include snippets/tls.conf; u svakom bloku. Kada se preporuke promene, menjaš jedno mesto.

Podrazumevani HTTPS blok — zahtevi na 443 sa nepoznatim imenom moraju negde da padnu, a bez eksplicitnog default_server padaju na prvi blok po redu, sa njegovim sertifikatom:

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    return 444;
}

Sertifikat je ovde obavezan iako blok ništa ne servira — TLS rukovanje se odvija pre nego što nginx sazna koje ime klijent traži.


Praktična vežba

Za vežbu ti treba stvarni domen koji pokazuje na server. Ako ga nemaš, prva dva koraka rade sa samopotpisanim sertifikatom.

1. Samopotpisani sertifikat za lokalnu vežbu:

sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 30 -newkey rsa:2048 \
  -keyout /etc/nginx/ssl/test.key \
  -out /etc/nginx/ssl/test.crt \
  -subj "/CN=tls.local"

Podesi server blok sa ovim sertifikatom i proveri (curl -k zaobilazi proveru poverenja):

curl -k -I https://localhost -H "Host: tls.local"

2. Uporedi cert.pem i fullchain.pem. Ako imaš pravi sertifikat, privremeno postavi cert.pem i pokreni:

openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>&1 | head -20

Uporedi broj sertifikata u lancu sa oba podešavanja. Vrati na fullchain.pem.

3. Podesi preusmeravanje sa HTTP-a i proveri da query string preživi:

curl -sI "http://localhost/putanja?a=1&b=2" -H "Host: tls.local" | grep -i location

U Location zaglavlju moraju biti i putanja i parametri. Zatim zameni $request_uri sa $uri i vidi šta se izgubi.

4. Isključi stare protokole i dokaži da su isključeni:

openssl s_client -connect localhost:443 -servername tls.local -tls1_1 < /dev/null
openssl s_client -connect localhost:443 -servername tls.local -tls1_2 < /dev/null

Prvi mora da padne, drugi da prođe.

5. Napravi snippet sa TLS parametrima i prebaci sve server blokove na include. Proveri sa nginx -T da se parametri zaista pojavljuju u svakom bloku.

6. Ako imaš pravi domen: pokreni sudo certbot renew --dry-run i pročitaj izlaz pažljivo. Zatim pogledaj šta je certbot dodao u tvoju konfiguraciju:

sudo grep -rn "managed by Certbot" /etc/nginx/

7. Postavi HSTS sa max-age=300 i proveri da zaglavlje stiže:

curl -kIs https://localhost -H "Host: tls.local" | grep -i strict

Zatim ukloni always, izazovi 404 i vidi da zaglavlja više nema na stranici greške.


Rezime

  • Domen mora pokazivati na server pre izdavanja sertifikata.
  • certbot --nginx je najbrži put; certbot certonly --webroot ostavlja tvoju konfiguraciju netaknutom.
  • Koristi fullchain.pem, ne cert.pem — inače lanac puca za klijente van browsera.
  • ssl_protocols TLSv1.2 TLSv1.3 — Ubuntu-ov podrazumevani config i dalje dozvoljava zastarele verzije.
  • ssl_prefer_server_ciphers off je aktuelna preporuka; listu šifri uzmi iz Mozilla generatora, ne sastavljaj je sam.
  • Preusmeravaj sa return 301 https://$host$request_uri; i ostavi izuzetak za /.well-known/acme-challenge/.
  • Petlja preusmeravanja nakon uključivanja HTTPS-a znači da nedostaje X-Forwarded-Proto.
  • HSTS uvodi postepeno; includeSubDomains i preload lista se teško poništavaju.
  • OCSP stapling sa Let's Encrypt sertifikatima više nema efekta — proveri sa openssl x509 -ocsp_uri.
  • Testiraj openssl s_client-om uz -servername i alatom testssl.sh.

Pitanja za proveru

  1. Zašto browser prikazuje sajt bez greške, a curl prijavljuje problem sa sertifikatom?
  2. Zašto se u preusmeravanju koristi $request_uri a ne $uri?
  3. Nakon uključivanja HTTPS-a sajt upada u beskonačnu petlju preusmeravanja. Šta je uzrok i gde ga popravljaš?
  4. Zašto je ssl_prefer_server_ciphers off bolji izbor od on?
  5. Šta se dešava ako postaviš includeSubDomains u HSTS, a jedan poddomen nema valjan sertifikat?
  6. Zašto podrazumevani HTTPS server blok mora imati sertifikat iako vraća 444?
  7. Kako proveravaš da je TLSv1.1 zaista isključen?

Sledeća lekcija: HTTP/2 i HTTP/3 — šta donose, kako se uključuju i zašto se sintaksa razlikuje između nginx verzija.

Comments

Popular posts from this blog

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

Konverzija tipova podataka u Pythonu

groupadd