Tutoriais de VPS
Hardening de segurança do Nginx em VPS (2026): TLS, HSTS, CSP, cabeçalhos de segurança, limitação de taxa, checklist de WAF
O Nginx estar rodando não significa que está seguro. Os riscos comuns em uma VPS incluem configuração de TLS desatualizada, falta de cabeçalhos de segurança como HSTS/CSP, IP de origem exposto diretamente, ausência de limitação de taxa de requisições, caminhos de administração sem proteção e logs de erro que ninguém lê. Este artigo traz um checklist de hardening de segurança do Nginx adequado a sites pequenos e serviços auto-hospedados, na ordem de entrada em produção.

O Nginx é o ponto de entrada web mais comum em uma VPS: WordPress, Next.js, n8n, Gitea, painéis, serviços de API — a maioria acaba atrás do Nginx.
Mas muitas configurações de Nginx em VPS só alcançam "dá para acessar": 80 redireciona para 443, proxy reverso para o backend, o certificado renova. Depois de realmente entrar em produção, os riscos costumam se esconder nestes lugares: versões de TLS antigas demais, cabeçalhos de segurança ausentes, caminhos de administração sem limitação de taxa, IP de origem exposto, páginas de erro vazando informações, logs de acesso que ninguém lê, Cloudflare ativado mas a origem ainda pode ser acessada diretamente.
Este artigo não ensina como instalar o Nginx, nem repete a solução de problemas de 502, 521 ou renovação de certificado. Há apenas um objetivo: levar o Nginx de uma VPS comum de "funciona" para "mais adequado à exposição pública de longo prazo".
Antes de entrar em produção, recomenda-se verificar nesta ordem:
- Abrir apenas 80/443; os serviços de backend não devem ser expostos diretamente à internet;
- Manter apenas TLS 1.2 / TLS 1.3;
- Ativar HSTS, mas não ir direto para preload;
- Adicionar cabeçalhos de segurança básicos: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy;
- Ocultar o número da versão do Nginx;
- Aplicar limitação de taxa em login, busca, API e Webhook;
- Limitar o tamanho de upload e os timeouts;
- Se usar Cloudflare, permitir apenas os IPs de origem da Cloudflare;
- Os logs devem permitir identificar ataques, limitações de taxa e códigos de status anormais;
- Validar externamente com SSL Labs, Mozilla Observatory e SecurityHeaders.
Não busque maximizar todos os cabeçalhos de segurança de uma vez. O objetivo da configuração de segurança não é "copiar a configuração mais extrema", mas endurecer gradualmente sem afetar o negócio.
Muita gente acha que só expôs o Nginx, quando na verdade Docker, Node, bancos de dados e Redis também estão escutando na internet.
Primeiro verifique as portas em escuta na VPS:
ss -tulpen
Preste atenção em 0.0.0.0 e ::. Se aparecer isto, é preciso ficar alerta:
0.0.0.0:3000 # 应用端口直接暴露
0.0.0.0:5432 # PostgreSQL 暴露
0.0.0.0:6379 # Redis 暴露
0.0.0.0:3306 # MySQL 暴露
Em produção, recomenda-se mais:
- O Nginx escuta em 80/443 públicos;
- As aplicações escutam apenas em
127.0.0.1ou na rede interna do Docker; - Bancos de dados e Redis não mapeiam portas públicas;
- O painel de administração passa por VPN, túnel SSH, lista de permissões ou, no mínimo, Basic Auth.
No Docker Compose, não faça assim:
ports:
- "3000:3000"
- "5432:5432"
Uma forma mais segura é expor o backend apenas para o Nginx:
services:
app:
expose:
- "3000"
Ou vincular apenas ao localhost:
ports:
- "127.0.0.1:3000:3000"
Se o banco de dados já está exposto à internet, leia primeiro isto:
Ainda usar TLS 1.0 / 1.1 em 2026 não é adequado. A maioria dos sites pequenos pode simplesmente manter TLS 1.2 e TLS 1.3:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
Por que ssl_prefer_server_ciphers off? Porque a configuração moderna de TLS depende mais da negociação entre cliente e servidor, especialmente porque o TLS 1.3 não funciona mais com a ordem antiga de cifras. A menos que você conheça muito bem os requisitos de compatibilidade, não copie uma lista longa de cifras de dez anos atrás.
Se quiser gerar uma configuração mais padrão, consulte o Mozilla SSL Configuration Generator (agora migrado para o TLSRef Configurator). Ao gerar, escolha conforme o público do site:
| Cenário | Nível recomendado |
|---|---|
| Blogs comuns, SaaS, sites de ferramentas | Intermediate |
| Apenas navegadores modernos | Modern |
| Precisa ser compatível com dispositivos muito antigos | Old, mas não recomendado |
A falha na renovação do certificado não é um problema de hardening, mas deixa o site indisponível diretamente. Se o certbot falha com frequência, veja:
O HSTS diz ao navegador: a partir de agora, este domínio deve ser acessado via HTTPS, evitando ataques de downgrade.
Configuração básica:
add_header Strict-Transport-Security "max-age=31536000" always;
Depois de confirmar que todos os subdomínios suportam HTTPS, considere:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
Não adicione preload de imediato. Uma vez que você entra na lista de preload do navegador, se algum subdomínio não puder usar HTTPS temporariamente no futuro, o processo de remoção é muito lento e o custo de solução de problemas é muito alto.
O processo correto é:
- Primeiro garanta que o domínio principal funciona bem com HTTPS;
- Ative
max-age=300e teste por alguns minutos; - Depois aumente para um dia, uma semana;
- Por fim aumente para um ano ou dois anos;
- Só depois de confirmar que todos os subdomínios suportam HTTPS, considere
includeSubDomains.
Se você já tem um loop de redirecionamento HTTPS, verifique primeiro aqui:
O OWASP Secure Headers Project recomenda usar cabeçalhos de resposta HTTP para reduzir riscos do lado do navegador. A sintaxe comum no Nginx é a seguinte:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
A função destes cabeçalhos:
| Header | Função | Risco |
|---|---|---|
X-Content-Type-Options | Evita o sniffing de MIME do navegador | Normalmente seguro |
X-Frame-Options | Reduz o risco de clickjacking | Cuidado com negócios que incorporam iframes |
Referrer-Policy | Reduz o vazamento de Referer | Sistemas de analytics podem ser afetados |
Permissions-Policy | Desativa recursos do navegador desnecessários | Negócios com WebRTC/geolocalização precisam liberá-los separadamente |
Observe que always é importante. Na documentação oficial do Nginx, add_header por padrão só tem efeito em alguns códigos de status; ao adicionar always, as respostas de erro também carregam esses cabeçalhos.
O Content-Security-Policy é poderoso, mas também é o que mais facilmente quebra um site. Ele pode restringir de onde scripts, imagens, fontes e iframes são carregados, mas plugins do WordPress, scripts de analytics, scripts de anúncios e sistemas de comentários podem ser bloqueados por ele.
Recomenda-se começar com uma versão moderada:
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:; font-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'self'; upgrade-insecure-requests" always;
Não é o CSP mais rígido, mas serve como primeira versão. Depois de entrar em produção, verifique no console do navegador se há recursos bloqueados e então endureça gradualmente.
Uma abordagem mais estável é usar primeiro o Report-Only:
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;
Depois de confirmar que não há falsos positivos, mude para o CSP formal.
Ocultar o número da versão do Nginx:
server_tokens off;
Reduz a exposição de informações, mas não é uma defesa central. Os atacantes não param de escanear só porque não veem o número da versão. O que realmente importa é atualizar o sistema e o Nginx em tempo:
apt update
apt list --upgradable | grep nginx
Se você usa Baota, OpenResty ou Nginx Proxy Manager, também precisa acompanhar o ritmo de atualização deles, não apenas os pacotes do sistema.
Páginas de login, páginas de busca, APIs, Webhooks, envio de comentários e endpoints de redefinição de senha são adequados para limitação de taxa.
Defina a zona de limitação no bloco http:
limit_req_zone $binary_remote_addr zone=login_zone:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api_zone:10m rate=10r/s;
Use em locations específicas:
location /login {
limit_req zone=login_zone burst=5 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
limit_req zone=api_zone burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
O limit_req do Nginx usa o algoritmo de balde com vazamento. rate=5r/m significa uma média de 5 vezes por minuto, burst=5 permite rajadas curtas, mas ao ultrapassar será atrasado ou rejeitado.
Se você não tem certeza do limite, pode fazer primeiro um dry-run:
limit_req_dry_run on;
limit_req_log_level notice;
Observe os logs primeiro e depois aplique o bloqueio. Caso contrário, é fácil bloquear também usuários reais.
Se você já está sofrendo um ataque CC, veja:
Muitos problemas na VPS não são causados por ataques sofisticados, mas por uploads, conexões lentas e timeouts sem limites.
Configuração básica:
client_max_body_size 20m;
client_body_timeout 15s;
client_header_timeout 15s;
keepalive_timeout 30s;
send_timeout 30s;
Se forem uploads de imagens pelo painel, você pode relaxar apenas o caminho de upload:
location /admin/upload {
client_max_body_size 100m;
proxy_pass http://127.0.0.1:3000;
}
Não defina client_max_body_size 500m sem critério em todo o site. Quanto maior o upload, mais fácil é abusar dele e mais fácil é provocar pressão de disco e memória.
Se encontrar 413, veja:
Muita gente ativa o Cloudflare WAF e acha que a origem está segura. O problema é: se o atacante souber o IP da sua VPS, pode contornar a Cloudflare e atacar a origem diretamente.
Abordagem de hardening:
- O DNS expõe apenas registros proxy da Cloudflare;
- O firewall da origem permite apenas que IPs da Cloudflare acessem 80/443;
- A porta SSH permite apenas o seu próprio IP ou passa por VPN;
- Não vaze o IP de origem em e-mails, DNS histórico ou páginas de erro.
Exemplo de UFW:
ufw default deny incoming
ufw allow OpenSSH
ufw allow from 173.245.48.0/20 to any port 443 proto tcp
ufw allow from 103.21.244.0/22 to any port 443 proto tcp
ufw enable
As faixas de IP da Cloudflare mudam; em produção recomenda-se escrever um script para sincronizar periodicamente a lista oficial de IPs, em vez de mantê-la manualmente a longo prazo.
Abaixo está um modelo inicial adequado para aplicações de proxy reverso em uma VPS comum:
server_tokens off;
limit_req_zone $binary_remote_addr zone=login_zone:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api_zone:10m rate=10r/s;
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name 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_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:; font-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'self'; upgrade-insecure-requests" always;
client_max_body_size 20m;
client_body_timeout 15s;
client_header_timeout 15s;
keepalive_timeout 30s;
send_timeout 30s;
location /login {
limit_req zone=login_zone burst=5 nodelay;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/ {
limit_req zone=api_zone burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
É preciso testar antes de entrar em produção:
nginx -t
systemctl reload nginx
curl -I https://example.com
O hardening não termina quando a configuração é escrita. Valide pelo menos com estas ferramentas:
| Ferramenta | O que verificar |
|---|---|
| SSL Labs Server Test | Versões de TLS, cadeia de certificados, classificação |
| Mozilla Observatory | Cabeçalhos de segurança, CSP, HSTS |
| SecurityHeaders.com | Se faltam cabeçalhos |
| curl -I | Se as respostas reais carregam cabeçalhos |
| nginx -t | Se a sintaxe da configuração está correta |
Verificação local:
curl -I https://example.com | grep -Ei "strict|content-security|x-frame|referrer|permissions"
Se você usa Cloudflare, teste ao mesmo tempo:
- O domínio acessado pela Cloudflare;
- A conexão direta ao IP de origem (se conseguir conectar diretamente, a proteção da origem ainda não está feita);
- Páginas reais do negócio, não apenas a página inicial.
Não. Os cabeçalhos devem servir ao negócio. Cabeçalhos como CSP, COOP e COEP podem afetar scripts de terceiros, iframes, pagamentos, analytics e login. Se você não entende o impacto, comece com cabeçalhos básicos e endureça gradualmente.
Sim. A Cloudflare é a primeira camada; o Nginx é a última porta da origem. Especialmente em caminhos de negócio como painel, API e Webhook, a limitação de taxa local do Nginx evita lacunas nas regras ou conexões diretas à origem.
Em sites pequenos de VPS comuns, não se recomenda ativar desde o início. Primeiro confirme que todos os subdomínios têm HTTPS e depois aumente gradualmente o max-age. Uma vez que o preload entra por engano na lista, a recuperação é muito lenta.
Apenas reduz a exposição de informações; não substitui as atualizações. O que realmente importa é atualizar em tempo o Nginx, o OpenSSL, o kernel do sistema e as aplicações upstream.
O Caddy faz HTTPS automático muito bem, mas cabeçalhos de segurança, limitação de taxa, proteção da origem e controle de acesso ao painel ainda precisam ser configurados conforme o negócio. O que o Caddy economiza é o gerenciamento de certificados, não todo o trabalho de segurança.
O hardening de segurança do Nginx em uma VPS não consiste em copiar a "configuração mais forte" da internet, mas em fechar por ordem de risco: primeiro reduzir a superfície de exposição, depois corrigir o TLS, em seguida adicionar cabeçalhos de segurança, depois limitar a taxa em caminhos de alto risco e, por fim, validar com ferramentas externas.
Para a maioria dos sites pequenos, ferramentas auto-hospedadas e desenvolvedores independentes, fazer isso já evita uma grande quantidade de varreduras de baixo custo, configurações erradas e riscos de conexão direta à origem. Uma VPS realmente estável a longo prazo não é a que nunca é atacada, mas aquela em que cada camada tem limites, logs e margem para reverter.