Nginx Моќник: Автоматизирајте TLS, Обезбедете API-ја и Постигнете Нулто Време на Прекин со Овој Стек
Обратниот прокси станува инфраструктура во моментот кога клиентите зависат од него. Во тој момент, „Nginx ги препраќа барањата“ не е доволно. Сертификатите мора да се обновуваат без надзор, злоупотребувачките клиенти мора да се ограничат, неуспешните инстанци на апликацијата мора да престанат да примаат сообраќај, а промените во конфигурацијата не смеат да ги прекинат активните барања.
Ова упатство го гради тој продукциски стек на Debian или Ubuntu домаќин: две мали Go API инстанци поврзани на loopback, Nginx со отворен код како јавна рабна точка, Certbot со ACME webroot текот, пасивни и активни здравствени проверки, ограничувања на стапката, структурирани логови и потврдени повторни вчитувања без прекин.
Архитектура и оперативни претпоставки
Заменете ги api.example.com и [email protected] насекаде. Името на домаќинот веќе мора да се разрешува до јавната адреса на серверот. Изложени се само портите 80 и 443; API-то слуша на 127.0.0.1:9001 и 127.0.0.1:9002.
- Nginx завршува TLS, применува ограничувања, евидентира барања и го балансира сообраќајот.
- Два API процеси управувани од systemd обезбедуваат редундантност при распоредувања.
- Nginx врши пасивно откривање на здравствената состојба од реалниот сообраќај.
- systemd тајмер активно ги проверува двете инстанци и пријавува неуспеси во журналот.
- Certbot ги обновува сертификатите преку webroot што останува достапен преку HTTP.
Nginx со отворен код не ги обезбедува конфигурливите активни upstream здравствени проверки што ги има Nginx Plus. Пасивното справување со неуспеси го штити клиентскиот сообраќај, додека тајмерот обезбедува проактивно откривање. Тајмерот намерно не ја препишува конфигурацијата на Nginx: автоматското отстранување врз основа на една проверка може да ги засили привремените неуспеси.
Предуслови и распоред на проектот
Користете актуелно поддржано издание на Debian или Ubuntu со systemd, Nginx, Go 1.22 или понов и root пристап преку sudo. Пред да овозможите firewall на домаќинот, зачувајте го административниот пристап со дозволување на вистинската SSH порта од доверливи мрежи. Потоа дозволете влезен TCP 80 и 443 и во firewall-от на домаќинот и во која било безбедносна група на провајдерот. Не ги изложувајте 9001 или 9002.
Инсталирајте ги пакетите поддржани од дистрибуцијата и создадете експлицитни директориуми:
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
Ако сметката веќе постои, прескокнете useradd. Добиената структура е:
/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/
Изградете ограничено API што е свесно за исклучување
Создадете /opt/power-api/src/main.go со sudoedit. Серверот изложува здравствена крајна точка и една примерна API рута. Неговите буџети за конекција и исклучување се конечни, а тој престанува да рекламира подготвеност веднаш штом започне прекинувањето.
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()
}
}
Изградете ја бинарната датотека, потоа создадете ги двете датотеки со околински променливи користејќи sudoedit. Нивната содржина е LISTEN=127.0.0.1:9001 и LISTEN=127.0.0.1:9002, соодветно.
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
Извршете ги двете инстанци под systemd
Создадете /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
Вчитајте, овозможете и потврдете ги услугите:
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
Конфигурирајте ограничувања, евидентирање и upstream однесување
Создадете /etc/nginx/conf.d/00-power-api-global.conf. Датотеките под conf.d се вклучуваат во контекстот http на Nginx во целните дистрибуции.
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"}';
Ограничувањата ја користат адресата на директно поврзаниот клиент. Ако подоцна пред Nginx се постави доверлив load balancer или CDN, конфигурирајте го модулот real-IP со точните, одржувани опсези на адреси на тој провајдер. Никогаш не верувајте на произволен влез X-Forwarded-For, бидејќи клиентите можат да ги заобиколат ограничувањата и да фалсификуваат логови.
Почетно поставете HTTP и добијте го сертификатот
Пред да се повикате на датотеки со сертификати што не постојат, создадете привремен виртуелен домаќин само за HTTP во /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;
}
}
Овозможете го само откако ќе проверите дека постоечка страница не го користи истото име на домаќин. Создавањето симболичка врска не е деструктивно; ако целта веќе постои, прегледајте ја наместо слепо да ја замените.
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
Активирајте го продукцискиот обратен прокси
Откако издавањето ќе успее, уредете ја истата датотека за страницата. Upstream-от користи балансирање со најмалку конекции, постојани upstream конекции, пасивно евидентирање на неуспеси и ограничено време за повторни обиди. Nginx нема повторно да прави обиди за неидемпотентни барања откако ќе ги испрати upstream, бидејќи non_idempotent намерно отсуствува.
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;
}
}
Потврдете пред секое повторно вчитување. При повторно вчитување, Nginx master-от стартува workers со новата конфигурација и бара старите workers да ги исцрпат постојните конекции, со што се избегнува прекинот на конекциите предизвикан од циклус на запирање и стартување.
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
Автоматизирајте обновување и активни здравствени проверки
Создадете /etc/letsencrypt/renewal-hooks/deploy/reload-nginx со следнава содржина, потоа направете ја извршна. Deploy hooks се извршуваат по успешно обновување, а не кога ниеден сертификат не е променет.
#!/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
Потврдете ги извршните патеки со command -v nginx systemctl ако дистрибуцијата ги инсталира на друго место.
За активни проверки, создадете /usr/local/sbin/check-power-api и означете ја како извршна:
#!/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"
Создадете power-api-health.service и power-api-health.timer под /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
Коментарот ги раздвојува двете датотеки; поставете го вториот дел [Unit] и натаму во датотеката за тајмерот. Овозможете го со sudo systemctl daemon-reload, по што следи sudo systemctl enable --now power-api-health.timer.
Тестирајте патеки на неуспех и однесување при распоредување
Запрете една инстанца и направете неколку барања. Барањата треба да продолжат преку преживеаната инстанца, додека полињата upstream_addr и upstream_status во access log-от откриваат однесување при неуспех и повторен обид.
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
Тестирајте го ограничувањето на стапката со контролиран налет и очекувајте некои одговори 429. Извршувајте тестови на оптоварување само од овластен систем. Заедничка NAT адреса претставува многу корисници, па ограничувањата по IP мора да ја одразуваат популацијата на клиенти; автентицираните API-а често имаат корист од второ ограничување засновано на потврден API идентитет.
Распоредувајте API промени по една инстанца: заменете ја бинарната датотека користејќи механизам за атомско издание, рестартирајте power-api@1, почекајте додека нејзината директна здравствена проверка не успее и дури потоа рестартирајте ја инстанцата 2. Ако новата верзија не успее, недопрената инстанца продолжува да опслужува. За промени во Nginx, секогаш користете nginx -t проследено со systemctl reload nginx.
Белешки за безбедност, перформанси и набљудливост
- Чувајте ги loopback услугите приватни и одбијте јавен пристап до нивните порти на секој мрежен слој.
- Заштитете го
/etc/letsencryptи неговите приватни клучеви со администрација достапна само за root. Никогаш не копирајте клучеви во директориуми на апликацијата. - Додајте HTTP Strict Transport Security само откако ќе потврдите дека името на домаќинот и неговата оперативна патека за опоравување се трајно подготвени за HTTPS.
- Чувајте ги телата на барањата, бројот на конекции и временските ограничувања ограничени. Зголемувањето на секое временско ограничување обично претвора кратки upstream проблеми во исцрпување на ресурси.
- Испраќајте го JSON access log-от и systemd журналот во надворешно складиште. Поставете предупредувања за трајни 5xx одговори, неуспеси на health единицата, неуспеси на обновување, висока латентност и неочекувани стапки на 429.
- Следете ги file descriptors, worker конекциите, заситеноста на CPU, меморијата и времето на одговор на upstream пред да ги прилагодувате бројот на workers или keepalive базените.
Вообичаени продукциски неуспеси
Неуспех при издавање сертификат обично значи дека DNS покажува на друго место, портата 80 е блокирана или друг виртуелен домаќин го презема предизвикот. Тестирајте датотека под /srv/www/acme/.well-known/acme-challenge/ надвор од серверот.
502 покажува дека Nginx не може да се поврзе со upstream, процесот завршил или систем за задолжителна контрола на пристап ја одбил конекцијата. Проверете ги API журналот и Nginx error log-от пред да ги зголемите временските ограничувања. Упорните стари Nginx workers по повторно вчитување обично укажуваат на долготрајни конекции; прегледајте ги наместо да ги убивате workers и да ги прекинувате клиентите.
Неочекуваните одговори 429 често откриваат лош клуч за ограничување, а не недоволен капацитет. Спротивно на тоа, навидум неефикасно ограничување може да значи дека Nginx верува на фалсификувачки forwarding заглавие.
Конечна листа за потврда
- Двете loopback здравствени крајни точки враќаат 200 и не се јавно достапни.
- HTTP нормално пренасочува, додека патеката за ACME предизвикот останува достапна.
- Клиентите со TLS 1.2 и TLS 1.3 можат да се поврзат со очекуваниот синџир на сертификати.
- Запрена API инстанца не ја става јавната крајна точка надвор од функција.
- Налетите на ограничување на стапката произведуваат контролирани одговори 429.
certbot renew --dry-runуспева и го потврдува Nginx пред повторно вчитување.- Структурираните логови содржат ID на барања, upstream адреси, статуси и времиња.
- Секоја промена на конфигурацијата поминува
nginx -tпред повторно вчитување.
Вистинската моќ на овој стек не е во ниту една поединечна директива. Таа е во низата на заштитени премини: ограничени барања, набљудливи неуспеси, редундантни upstream-и, сертификати обновени пред истекување и конфигурации потврдени пред старите workers да го предадат сообраќајот. Тоа го претвора Nginx од практичен прокси во сигурна продукциска граница.