Nginx moćnik: automatizirajte TLS, osigurajte API-je i ostvarite rad bez zastoja uz ovaj skup alata
Obrnuti proxy postaje infrastruktura onog trenutka kada klijenti ovise o njemu. Tada “Nginx prosljeđuje zahtjeve” nije dovoljno. Certifikati se moraju obnavljati bez nadzora, zlonamjerni klijenti moraju biti ograničeni, neuspjele instance aplikacije moraju prestati primati promet, a promjene konfiguracije ne smiju prekidati aktivne zahtjeve.
Ovaj vodič izgrađuje taj produkcijski skup na Debian ili Ubuntu hostu: dvije male Go API instance vezane na loopback, Nginx otvorenog koda kao javni rub, Certbot koji koristi ACME webroot tijek, pasivne i aktivne provjere zdravlja, ograničenja brzine, strukturirane zapise i provjerena ponovna učitavanja bez prekida rada.
Arhitektura i operativne pretpostavke
Zamijenite api.example.com i [email protected] posvuda. Naziv hosta već mora razrješavati na javnu adresu poslužitelja. Izloženi su samo portovi 80 i 443; API sluša na 127.0.0.1:9001 i 127.0.0.1:9002.
- Nginx završava TLS, primjenjuje ograničenja, zapisuje zahtjeve i uravnotežuje promet.
- Dva API procesa kojima upravlja systemd osiguravaju redundantnost tijekom implementacija.
- Nginx provodi pasivno otkrivanje stanja usluge iz stvarnog prometa.
- systemd timer aktivno ispituje obje instance i prijavljuje kvarove u journal.
- Certbot obnavlja certifikate kroz webroot koji ostaje dostupan putem HTTP-a.
Nginx otvorenog koda ne pruža konfigurabilne aktivne provjere zdravlja uzvodnih poslužitelja koje postoje u Nginx Plusu. Pasivno rukovanje kvarovima štiti promet klijenata, dok timer osigurava proaktivno otkrivanje. Timer namjerno ne prepisuje konfiguraciju Nginxa: automatsko uklanjanje na temelju jedne provjere može pojačati prolazne kvarove.
Preduvjeti i raspored projekta
Koristite trenutačno podržano izdanje Debiana ili Ubuntua sa systemdom, Nginxom, Go 1.22 ili novijim te root pristupom putem sudo. Prije omogućavanja vatrozida hosta, sačuvajte administrativni pristup dopuštanjem stvarnog SSH porta iz pouzdanih mreža. Zatim dopustite dolazni TCP 80 i 443 i u vatrozidu hosta i u sigurnosnoj grupi pružatelja usluge. Nemojte izlagati 9001 ni 9002.
Instalirajte pakete koje podržava distribucija i stvorite eksplicitne direktorije:
sudo apt update
sudo apt install nginx certbot curl
go version
nginx -v
sudo install -d -m 0755 /opt/power-api/src
sudo install -d -m 0755 /opt/power-api/current
sudo install -d -o www-data -g www-data -m 0755 /srv/www/acme
sudo install -d -m 0755 /etc/power-api
sudo useradd --system --home-dir /nonexistent \
--shell /usr/sbin/nologin power-api
Ako račun već postoji, preskočite useradd. Dobivena struktura je:
/opt/power-api/src/main.go
/opt/power-api/current/api
/etc/power-api/1.env
/etc/power-api/2.env
/etc/systemd/system/[email protected]
/etc/nginx/conf.d/00-power-api-global.conf
/etc/nginx/sites-available/api.example.com
/srv/www/acme/.well-known/acme-challenge/
Izgradite ograničen API svjestan gašenja
Stvorite /opt/power-api/src/main.go pomoću sudoedit. Poslužitelj izlaže krajnju točku za provjeru zdravlja i jednu primjerenu API rutu. Njegovi proračuni vremena za vezu i gašenje su konačni, a prestaje oglašavati spremnost čim započne završavanje.
package main
import (
"context"
"encoding/json"
"flag"
"log"
"net/http"
"os"
"os/signal"
"sync/atomic"
"syscall"
"time"
)
func main() {
listen := flag.String("listen", "127.0.0.1:9001", "listen address")
flag.Parse()
var ready atomic.Bool
ready.Store(true)
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
if !ready.Load() {
http.Error(w, "draining", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok\n"))
})
mux.HandleFunc("GET /v1/time", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{
"time": time.Now().UTC().Format(time.RFC3339),
})
})
server := &http.Server{
Addr: *listen,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
log.Printf("listening on %s", *listen)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
<-signals
ready.Store(false)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("graceful shutdown failed: %v", err)
_ = server.Close()
}
}
Izgradite binarnu datoteku, zatim stvorite dvije datoteke okruženja pomoću sudoedit. Njihov sadržaj je LISTEN=127.0.0.1:9001 i LISTEN=127.0.0.1:9002, redom.
sudo go build -trimpath \
-o /opt/power-api/current/api \
/opt/power-api/src/main.go
sudo chown root:root /opt/power-api/current/api
sudo chmod 0755 /opt/power-api/current/api
Pokrenite obje instance pod systemdom
Stvorite /etc/systemd/system/[email protected]:
[Unit]
Description=Power API instance %i
After=network.target
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=simple
User=power-api
Group=power-api
EnvironmentFile=/etc/power-api/%i.env
ExecStart=/opt/power-api/current/api -listen ${LISTEN}
Restart=on-failure
RestartSec=2
TimeoutStopSec=15
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
[Install]
WantedBy=multi-user.target
Učitajte, omogućite i provjerite usluge:
sudo systemctl daemon-reload
sudo systemctl enable --now power-api@1 power-api@2
curl --fail --max-time 2 http://127.0.0.1:9001/healthz
curl --fail --max-time 2 http://127.0.0.1:9002/healthz
sudo systemctl status power-api@1 power-api@2
Konfigurirajte ograničenja, zapisivanje i ponašanje uzvodnih poslužitelja
Stvorite /etc/nginx/conf.d/00-power-api-global.conf. Datoteke pod conf.d uključuju se unutar Nginxova konteksta http na ciljanim distribucijama.
server_tokens off;
limit_req_zone $binary_remote_addr zone=api_per_ip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=connections_per_ip:10m;
log_format api_json escape=json
'{"time":"$time_iso8601",'
'"request_id":"$request_id",'
'"remote_addr":"$remote_addr",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":"$status",'
'"bytes":"$body_bytes_sent",'
'"request_time":"$request_time",'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"upstream_time":"$upstream_response_time"}';
Ograničenja koriste adresu izravno povezanog klijenta. Ako se pouzdani uravnoteživač opterećenja ili CDN kasnije postavi ispred Nginxa, konfigurirajte modul real-IP s točnim, održavanim rasponima adresa tog pružatelja. Nikada nemojte vjerovati proizvoljnom unosu X-Forwarded-For, jer klijenti mogu zaobići ograničenja i krivotvoriti zapise.
Pokrenite HTTP i pribavite certifikat
Prije referenciranja datoteka certifikata koje ne postoje, stvorite privremeni virtualni host samo za HTTP na /etc/nginx/sites-available/api.example.com:
server {
listen 80;
listen [::]:80;
server_name api.example.com;
location ^~ /.well-known/acme-challenge/ {
root /srv/www/acme;
default_type text/plain;
try_files $uri =404;
}
location / {
return 503;
}
}
Omogućite ga tek nakon provjere da postojeća stranica ne koristi isti naziv hosta. Stvaranje simboličke poveznice nije destruktivno; ako cilj već postoji, pregledajte ga umjesto da ga naslijepo zamijenite.
sudo ln -s /etc/nginx/sites-available/api.example.com \
/etc/nginx/sites-enabled/api.example.com
sudo nginx -t
sudo systemctl reload nginx
sudo certbot certonly --webroot \
--webroot-path /srv/www/acme \
--domain api.example.com \
--email [email protected] \
--agree-tos --no-eff-email
Aktivirajte produkcijski obrnuti proxy
Nakon uspješnog izdavanja uredite istu datoteku stranice. Uzvodni skup koristi uravnoteživanje po najmanjem broju veza, trajne uzvodne veze, pasivno evidentiranje kvarova i ograničeno vrijeme ponovnog pokušaja. Nginx neće ponovno pokušati neidempotentne zahtjeve nakon njihova slanja uzvodno jer je non_idempotent namjerno izostavljen.
upstream power_api {
zone power_api 64k;
least_conn;
server 127.0.0.1:9001 max_fails=3 fail_timeout=10s;
server 127.0.0.1:9002 max_fails=3 fail_timeout=10s;
keepalive 32;
}
server {
listen 80;
listen [::]:80;
server_name api.example.com;
location ^~ /.well-known/acme-challenge/ {
root /srv/www/acme;
default_type text/plain;
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:TLS:20m;
ssl_session_timeout 1d;
ssl_session_tickets off;
access_log /var/log/nginx/power-api.access.log api_json;
error_log /var/log/nginx/power-api.error.log warn;
client_max_body_size 1m;
limit_req_status 429;
limit_conn_status 429;
location = /healthz {
access_log off;
proxy_pass http://power_api/healthz;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 2s;
proxy_read_timeout 3s;
}
location /v1/ {
limit_req zone=api_per_ip burst=40 nodelay;
limit_conn connections_per_ip 20;
proxy_pass http://power_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
add_header X-Request-ID $request_id always;
proxy_connect_timeout 2s;
proxy_send_timeout 10s;
proxy_read_timeout 15s;
proxy_next_upstream error timeout invalid_header
http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 3s;
}
location / {
return 404;
}
}
Provjerite prije svakog ponovnog učitavanja. Pri ponovnom učitavanju Nginxov glavni proces pokreće radnike s novom konfiguracijom i traži od starih radnika da isprazne postojeće veze, izbjegavajući prekid veze koji uzrokuje ciklus zaustavljanja i pokretanja.
sudo nginx -t
sudo systemctl reload nginx
curl --fail --max-time 5 https://api.example.com/healthz
curl --fail --max-time 5 https://api.example.com/v1/time
Automatizirajte obnovu i aktivne provjere zdravlja
Stvorite /etc/letsencrypt/renewal-hooks/deploy/reload-nginx sa sljedećim sadržajem, a zatim ga učinite izvršnim. Kuke za implementaciju pokreću se nakon uspješne obnove, a ne kada se nijedan certifikat nije promijenio.
#!/bin/sh
set -eu
/usr/sbin/nginx -t
/usr/bin/systemctl reload nginx
sudo chmod 0755 \
/etc/letsencrypt/renewal-hooks/deploy/reload-nginx
sudo systemctl enable --now certbot.timer
sudo certbot renew --dry-run
Potvrdite izvršne putanje s command -v nginx systemctl ako ih distribucija instalira na drugo mjesto.
Za aktivne provjere stvorite /usr/local/sbin/check-power-api i označite ga kao izvršnog:
#!/bin/sh
set -eu
failed=0
for port in 9001 9002; do
if ! /usr/bin/curl --fail --silent --show-error \
--max-time 2 "http://127.0.0.1:${port}/healthz"; then
/usr/bin/logger -t power-api-health \
"health check failed on port ${port}"
failed=1
fi
done
exit "$failed"
Stvorite power-api-health.service i power-api-health.timer pod /etc/systemd/system:
[Unit]
Description=Check local Power API instances
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/check-power-api
User=nobody
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
# power-api-health.timer
[Unit]
Description=Run Power API health checks
[Timer]
OnBootSec=30s
OnUnitActiveSec=15s
AccuracySec=2s
[Install]
WantedBy=timers.target
Komentar razdvaja dvije datoteke; drugi odjeljak [Unit] nadalje stavite u datoteku timera. Omogućite ga s sudo systemctl daemon-reload, a zatim s sudo systemctl enable --now power-api-health.timer.
Testirajte putanje kvara i ponašanje implementacije
Zaustavite jednu instancu i pošaljite nekoliko zahtjeva. Zahtjevi bi se trebali nastaviti preko preživjele instance, dok polja upstream_addr i upstream_status u pristupnom zapisniku otkrivaju ponašanje pri kvaru i ponovnom pokušaju.
sudo systemctl stop power-api@1
for n in 1 2 3 4 5; do
curl --fail --max-time 5 https://api.example.com/v1/time
done
sudo systemctl start power-api@1
sudo journalctl -u power-api-health.service --since "10 minutes ago"
sudo tail -n 20 /var/log/nginx/power-api.access.log
Testirajte ograničavanje brzine kontroliranim naletom i očekujte neke odgovore 429. Testove opterećenja pokrećite samo s ovlaštenog sustava. Zajednička NAT adresa predstavlja mnogo korisnika, stoga ograničenja po IP-u moraju odražavati populaciju klijenata; autentificirani API-ji često imaju korist od drugog ograničenja vezanog uz provjereni API identitet.
Implementirajte promjene API-ja po jednu instancu: zamijenite binarnu datoteku pomoću atomskog mehanizma izdanja, ponovno pokrenite power-api@1, pričekajte dok njegova izravna provjera zdravlja ne uspije, a tek tada ponovno pokrenite instancu 2. Ako nova izgradnja ne uspije, netaknuta instanca nastavlja posluživati. Za promjene Nginxa uvijek koristite nginx -t, a zatim systemctl reload nginx.
Napomene o sigurnosti, performansama i vidljivosti
- Zadržite loopback usluge privatnima i onemogućite javni pristup njihovim portovima na svakom mrežnom sloju.
- Zaštitite
/etc/letsencrypti njegove privatne ključeve administracijom samo za root korisnika. Nikada ne kopirajte ključeve u direktorije aplikacije. - Dodajte HTTP Strict Transport Security tek nakon potvrde da su naziv hosta i njegov operativni put oporavka trajno spremni za HTTPS.
- Ograničite tijela zahtjeva, broj veza i vremenska ograničenja. Povećavanje svakog vremenskog ograničenja obično pretvara kratkotrajne probleme uzvodnog poslužitelja u iscrpljivanje resursa.
- Šaljite JSON pristupni zapisnik i systemd journal u vanjsku pohranu. Postavite upozorenja za trajne odgovore 5xx, kvarove jedinice za zdravlje, kvarove obnove, veliku latenciju i neočekivane stope 429.
- Pratite deskriptore datoteka, veze radnika, zasićenost CPU-a, memoriju i vrijeme odgovora uzvodnog poslužitelja prije prilagodbe broja radnika ili keepalive skupova.
Česti produkcijski kvarovi
Neuspjeh izdavanja certifikata obično znači da DNS pokazuje drugdje, da je port 80 blokiran ili da drugi virtualni host preuzima izazov. Testirajte datoteku pod /srv/www/acme/.well-known/acme-challenge/ izvan poslužitelja.
502 pokazuje da se Nginx ne može povezati s uzvodnim poslužiteljem, da je proces završio ili da je sustav obvezne kontrole pristupa odbio vezu. Provjerite API journal i Nginxov zapisnik pogrešaka prije povećavanja vremenskih ograničenja. Trajni stari Nginxovi radnici nakon ponovnog učitavanja obično ukazuju na dugotrajne veze; pregledajte ih umjesto da ubijete radnike i prekinete klijente.
Neočekivani odgovori 429 često otkrivaju loš ključ ograničavanja, a ne nedovoljan kapacitet. Suprotno tome, naizgled neučinkovito ograničenje može značiti da Nginx vjeruje zaglavlju za prosljeđivanje koje se može lažirati.
Završni kontrolni popis provjere
- Obje loopback krajnje točke za provjeru zdravlja vraćaju 200 i javno su nedostupne.
- HTTP se uobičajeno preusmjerava, dok putanja ACME izazova ostaje dostupna.
- Klijenti TLS 1.2 i TLS 1.3 mogu se povezati s očekivanim lancem certifikata.
- Zaustavljena instanca API-ja ne prekida rad javne krajnje točke.
- Naleti ograničenja brzine proizvode kontrolirane odgovore 429.
certbot renew --dry-runuspijeva i provjerava Nginx prije ponovnog učitavanja.- Strukturirani zapisi sadržavaju ID-ove zahtjeva, uzvodne adrese, statuse i vremena.
- Svaka promjena konfiguracije prolazi
nginx -tprije ponovnog učitavanja.
Stvarna snaga ovog skupa nije ni u jednoj pojedinačnoj direktivi. Ona je u slijedu zaštićenih prijelaza: ograničeni zahtjevi, vidljivi kvarovi, redundantni uzvodni poslužitelji, certifikati obnovljeni prije isteka i konfiguracije provjerene prije nego što stari radnici prepuste promet. To Nginx pretvara iz praktičnog proxyja u pouzdanu produkcijsku granicu.