Começando a entender o Nginx

18 min

language: ja bn en es hi pt ru zh-cn zh-tw

Olá, o título é um trocadilho com aquilo. É apenas uma música que me veio à mente de repente.

O motivo de haver tantas atualizações de artigos ultimamente é que, uma vez que algo me chama a atenção, não consigo parar, então é apenas essa fase.

Nginx

Até agora, eu o tinha configurado apenas como um servidor de cache e proxy reverso, mas com o suporte ao IPv6, comecei a me interessar, então revisei as configurações, incluindo uma refatoração.

Estrutura de arquivos

Algumas partes estão ocultas, mas está mais ou menos assim:

├── http.d
│   ├── bot_rate_limit.conf
│   ├── gzip.conf
│   ├── proxy_cache_zones.conf
│   └── proxy_common.conf
├── mime.types
├── mime.types-dist
├── nginx.conf
├── scgi_params
├── selfsigned.crt
├── selfsigned.key
├── sites-enabled
│   ├── 1btc.love.conf
│   ├── btclol.xyz.conf
│   ├── damepo.jp.conf
│   ├── git.soulminingrig.com.conf
│   ├── soulminingrig.com.conf
│   ├── starlink.soulminingrig.com.conf
│   ├── stg.api.1btc.love.conf
├── snippets
│   ├── common_error_pages.conf
│   ├── proxy_headers.conf
│   └── ssl_common.conf
├── uwsgi_params
└── win-utf

Fiquei um pouco em dúvida sobre em qual pasta gerenciar os arquivos conf que são carregados por diretiva location, mas após consultar o ChatGPT, ficou assim. O http.d contém os arquivos que são incluídos a partir da diretiva http no nginx.conf, então fiquei satisfeito com o resultado.

Embora eu tenha achado o nome snippets um pouco peculiar, está bom assim.

Vamos dar uma olhada em cada item de configuração.

http.d/bot_rate_limit.conf

Como os crawlers da Meta estavam exagerados, decidi aplicar restrições não apenas com o fail2ban, mas também por User-Agent (UA).

Além disso, em relação ao feed/RSS, recebi um contato educado, mas como não faz sentido aplicar restrições e a resposta é quase sempre via cache, não deve sobrecarregar o Origin, então abri uma exceção.

# Aplicar limite de taxa apenas para bots e crawlers de expansão de links
map $http_user_agent $is_bot {
    default 0;
    ~*bot 1;
    ~*crawler 1;
    ~*spider 1;
    ~*facebookexternalhit 1;
    ~*slackbot 1;
    ~*discordbot 1;
    ~*twitterbot 1;
    ~*linkedinbot 1;
    ~*embedly 1;
    ~*quora 1;
    ~*skypeuripreview 1;
    ~*whatsapp 1;
    ~*telegrambot 1;
    ~*applebot 1;
    ~*pingdom 1;
    ~*uptimerobot 1;
}
# stg.api.1btc.love é para fins de verificação, portanto, mesmo que seja um bot, fica fora do limite de taxa
# Se a chave for uma string vazia, não será contada no limit_req_zone
map $server_name $bot_limit_host_key {
    stg.api.1btc.love "";
    default $binary_remote_addr;
}
# feed.xml / feed.json ficam fora do limite de taxa, mesmo para bots
map $uri $is_feed_path {
    default 0;
    ~*feed\.(xml|json)$ 1;
}
# Usar a chave por IP apenas quando for um bot e não for um caminho de feed
map "$is_bot:$is_feed_path" $bot_limit_key {
    default "";
    "1:0" $bot_limit_host_key;
}
limit_req_zone $bot_limit_key zone=bot:10m rate=1r/s;
limit_req_status 429;
limit_req zone=bot burst=5 nodelay;

Claro, como está aplicado à diretiva http, ele se aplica a tudo por padrão, mas fiz de forma que fosse possível abrir exceções mínimas. Julguei que isso é algo que deveria estar ativado por padrão.

Se houver um ataque DoS óbvio falsificando o UA ou o navegador, ele será bloqueado pelo fail2ban e tratado como drop, impedindo novas requisições por um tempo, então é uma proteção em duas camadas.

http.d/gzip.conf

Antigamente eu usava o suporte ao brotli, mas como exigia builds separadas e era trabalhoso na hora de atualizar as versões, parei. A vantagem de poder atualizar via pkg/apt é grande.

gzip on;
gzip_vary off;
gzip_proxied any;
gzip_min_length 1024;
gzip_comp_level 7;
gzip_http_version 1.1;
gzip_types text/plain
text/xml
text/css
text/javascript
image/gif
image/png
image/svg+xml
application/javascript
application/json
application/xml
application/x-javascript
application/font-woff
application/font-woff2
application/font-ttf
application/octet-stream;

Não há muito o que dizer, eu não usava antigamente, mas comecei a incluir o gzip_min_length há alguns anos. Foi quando decidi colocar corretamente, pensando que compressões ineficientes seriam um desperdício.

http.d/proxy_cache_zones.conf

Pode parecer que o recuo está um pouco estranho, mas é uma limitação do formatador.

Apaguei o que não era necessário, então as zone começam do 4 ou estão desorganizadas, mas enfim...

Quanto ao inactive, ele expira se não houver hits por 7 dias, e quanto ao use_temp_path, configurei para fazer o cache diretamente no caminho de cache. Parece que, mesmo com configurações como proxy_temp_path /tmp/nginx;, ele faz o cache sem passar por isso, tornando-o mais rápido.

proxy_cache_path /tmp/nginx/zone4 levels=1:2 keys_zone=zone4:10m
inactive=7d
max_size=3g
use_temp_path=off;
proxy_cache_path /tmp/nginx/posts levels=1:2 keys_zone=posts:10m inactive=7d max_size=2g use_temp_path=off; proxy_cache_path /tmp/nginx/git levels=1:2 keys_zone=git:10m inactive=7d max_size=2g use_temp_path=off; proxy_cache_path /tmp/nginx/static levels=1:2 keys_zone=static_cache:10m inactive=7d max_size=1g use_temp_path=off; proxy_cache_path /tmp/nginx/1btc_cache levels=1:2 keys_zone=1btc_cache:10m inactive=7d max_size=512m use_temp_path=off;

http.d/proxy_common.conf

Eu configuro o proxy_cache_valid como uma regra comum e deixo o restante para ser sobrescrito na diretiva location.

Dessa forma, é possível operar o cache mesmo sem inserir configurações de cache específicas.

Ao usar proxy_cache_bypass $http_cookie, evito que requisições com cookies (por exemplo, após o login) acabem exibindo a tela de antes do login.

Além disso, fazer com que redirecionamentos, erros e any retornem respostas em cache é uma medida contra ataques. Isso garante que, no mínimo, uma resposta em cache seja enviada, evitando acessos anormais ao Origin.

O proxy_temp_path idealmente deveria apontar para um caminho persistente, mas para o meu uso básico não há necessidade, então uso /tmp. Em sites com tráfego considerável, fazer isso provavelmente faria com que todo o cache sumisse ao reiniciar o servidor, aumentando a carga no Origin e gerando risco de falhas.

proxy_buffering on;
proxy_cache_bypass $http_cookie;
proxy_cache_background_update on;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_revalidate on;
proxy_cache_use_stale updating;
proxy_connect_timeout 60;
proxy_no_cache $http_cookie;
proxy_read_timeout 90;
proxy_send_timeout 60;
proxy_temp_path /tmp/nginx;
proxy_cache_valid 200 201 60s;
proxy_cache_valid 301 1d;
proxy_cache_valid 302 3h;
proxy_cache_valid 304 1d;
proxy_cache_valid 404 1m;
proxy_cache_valid any 5s;
proxy_cache_lock on;

nginx.conf

Não há muito o que dizer, apenas que configurei para retornar um erro específico caso o IP do registro A seja acessado diretamente.

Ativei o multi_accept porque opero este servidor como proxy reverso e cache em uma instância fraca, sem especificações muito altas.

As configurações que antes eram escritas diretamente na diretiva http agora são incluídas por finalidade via include, o que melhorou muito a visibilidade.

worker_processes auto;
worker_cpu_affinity auto;
worker_rlimit_nofile 65535;
events {
    multi_accept on;
    worker_connections 65535;
}
http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    server_tokens off;
    types_hash_max_size 4096;
    client_max_body_size 16M;
    # MINE
    include mime.types;
    default_type application/octet-stream;
    include ./http.d/bot_rate_limit.conf;
    include ./http.d/proxy_common.conf;
    include ./http.d/proxy_cache_zones.conf;
    include ./http.d/gzip.conf;
    server {
        listen 80;
        listen [::]:80;
        server_name 163.44.113.145 91.98.169.80 2400:8500:2002:3317:163:44:113:145;
        include snippets/common_error_pages.conf;
        return 444;
    }
    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name 163.44.113.145 91.98.169.80 2400:8500:2002:3317:163:44:113:145;
        ssl_certificate ./selfsigned.crt;
        ssl_certificate_key ./selfsigned.key;
        include snippets/common_error_pages.conf;
        # IPアドレスへのアクセスを拒否
        return 444;
    }
    ### Damepo.jp
    include ./sites-enabled/damepo.jp.conf;
    ### Soulminingrig My Blog
    include ./sites-enabled/soulminingrig.com.conf;
# ~~~略~~~~
}

sites-enabled/soulminingrig.com.conf

Vou apresentar apenas um exemplo dentro de site-enabled. É a configuração deste site.

Além disso, o motivo de eu não usar links simbólicos é apenas para poder deletar rapidamente o que não for necessário.

Por favor, note que ainda existem algumas partes codificadas de forma rígida (hardcoded), pois ainda estou fazendo ajustes.

Recentemente mudei para www.soulminingrig.com, mas como antes eu distribuía pelo domínio raiz sem www, atualmente ainda aceito o domínio raiz sem redirecionamento.

Como período de teste, configurei para retornar cabeçalhos que tornam fácil identificar se a resposta veio do cache do servidor ou do cache do cliente.

E fico pensando se não haveria uma forma melhor de gerenciar as regras de cache para imagens, fontes, CSS, etc... Será que não tem jeito... isso aqui...

Configurei o upstream para poder lidar rapidamente caso o número de backends aumente. Bem, no momento só existe um, mas tê-lo no topo facilita identificar para qual backend a configuração está apontando.

upstream backend_sm {
    server 10.1.0.228:8888 max_fails=3 fail_timeout=3s;
    keepalive 16;
    keepalive_timeout 30s;
}
map $uri $static_cache {
    ~\.(jpg|jpeg|png|webp|gif|mp4|css|js|ico|woff2)(\?.*)?$ "public, max-age=604800";
    ~\.html$ "public, max-age=600";
    default "public, max-age=600";
}
map $upstream_cache_status $server_cache_status {
    default $upstream_cache_status;
    "" "NONE";
}
map "$http_if_none_match:$http_if_modified_since" $client_cache_request {
    default "MISS";
    "~.+:.+" "REVALIDATE";
    "~.+:" "REVALIDATE";
    "~:.+" "REVALIDATE";
}
server {
    listen 80;
    listen [::]:80;
    server_name soulminingrig.com www.soulminingrig.com;
    return 301 https://$host$request_uri;
}
server {
    listen 443 ssl reuseport backlog=65535 rcvbuf=256k sndbuf=256k fastopen=256 so_keepalive=on;
    listen [::]:443 ssl reuseport backlog=65535 rcvbuf=256k sndbuf=256k fastopen=256 so_keepalive=on ipv6only=on;
    listen 443 quic reuseport;
    listen [::]:443 quic reuseport;
    http2 on;
    http3 on;
    server_name soulminingrig.com www.soulminingrig.com;
    client_max_body_size 50M;
    location ~* \.(jpg|jpeg|png|webp|gif|ico|mp4|js|css|woff2)(\?.*)?$ {
        proxy_pass http://backend_sm;
        include snippets/proxy_headers.conf;
        proxy_http_version 1.1;
        proxy_redirect off;
        proxy_cache static_cache;
        proxy_cache_valid 200 301 302 7d;
        proxy_cache_valid 404 1m;
        proxy_cache_revalidate on;
        proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        proxy_connect_timeout 3s;
        proxy_read_timeout 15s;
        expires 7d;
        add_header X-Cache-Status $upstream_cache_status always;
        add_header X-Server-Cache-Status $server_cache_status always;
        add_header X-Client-Cache-Request $client_cache_request always;
        add_header X-Client-Cache-Policy "public, max-age=604800" always;
        add_header Cache-Control "public, max-age=604800" always;
    }
    location / {
        proxy_pass http://backend_sm/;
        include snippets/proxy_headers.conf;
        proxy_http_version 1.1;
        proxy_redirect off;
        proxy_cache posts;
        proxy_cache_key $scheme$host$request_uri;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 301 1h;
        proxy_cache_valid 404 1m;
        proxy_cache_revalidate on;
        proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504 http_403 http_404;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        proxy_connect_timeout 3s;
        proxy_read_timeout 15s;
        expires $static_cache;
        add_header X-Cache-Status $upstream_cache_status always;
        add_header X-Server-Cache-Status $server_cache_status always;
        add_header X-Client-Cache-Request $client_cache_request always;
        add_header X-Client-Cache-Policy $static_cache always;
        add_header Cache-Control $static_cache always;
    }
    include snippets/common_error_pages.conf;
    include snippets/ssl_common.conf;
    ssl_certificate /hoge/fullchain.pem; # gerenciado pelo Certbot
    ssl_certificate_key /hoge/oulminingrig.com/privkey.pem;
    # gerenciado pelo Certbot
}

Bônus: nginxfmt.py

Isso é muito bom.

É um formatador, mas sinto que é o que tem a melhor qualidade.

Está disponível no AUR.

yay -S nginx-config-formatter

O uso é nginxfmt.py example.conf para formatar, então:

find . -name "*conf" | xargs -I{} nginxfmt.py {}

ele formata tudo em lote.

Até pouco tempo atrás eu usava o nginxbeautifier, mas ele quebrava a sintaxe relacionada a redirecionamentos, então acabei trocando.

Related Posts