Skip to content

Instantly share code, notes, and snippets.

@alexishida
Created May 7, 2026 04:12
Show Gist options
  • Select an option

  • Save alexishida/b71bdf593101e8b985c0fc509c6f04f3 to your computer and use it in GitHub Desktop.

Select an option

Save alexishida/b71bdf593101e8b985c0fc509c6f04f3 to your computer and use it in GitHub Desktop.
Arquivo de configuração do nginx focado em segurança para WordPress, com as principais proteções contra ataques comuns.
# Mitigações de ataque
Brute-force de login: rate limit em /wp-login.php (5 req/min) e bloqueio total do xmlrpc.php (vetor clássico de amplificação e brute-force via system.multicall).
Enumeração de usuários: bloqueia ?author=N.
Path traversal / SQLi / XSS via query string: WAF leve com regex.
Upload malicioso: bloqueia execução de .php em wp-content/uploads, wp-includes e cache.
Information disclosure: oculta readme.html, license.txt, wp-config-sample.php, .env, .git, dumps SQL, logs.
Slowloris / DoS leve: timeouts curtos e limit_conn.
Bots ofensivos (wpscan, sqlmap, nikto): bloqueio por User-Agent.
Httpoxy (CVE-2016-5385): HTTP_PROXY "" no fastcgi.
Headers de segurança: HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy.
TLS: só TLS 1.2/1.3, ciphers modernos, OCSP stapling.
Pontos que você precisa ajustar antes de subir:
As 4 zonas de limit_req_zone e os dois map precisam ir no http {} do nginx.conf (deixei comentado no topo).
Caminho do socket PHP-FPM (php8.2-fpm.sock) — confira sua versão.
Domínio, root e certificados.
Se usar Jetpack ou app móvel do WP, descomente a alternativa do xmlrpc.php em vez do bloqueio total.
A CSP está permissiva (unsafe-inline/unsafe-eval) porque WP + plugins quebram com CSP estrita — recomendo subir primeiro como Content-Security-Policy-Report-Only e ir apertando.
Complementos que recomendo fora do nginx, já que isto cobre só a camada de borda:
fail2ban com jail para wordpress.access.log (404 em massa, 401, POST em wp-login).
Permissões de arquivo: wp-config.php 640, owner separado do usuário do PHP-FPM.
DISALLOW_FILE_EDIT e DISALLOW_FILE_MODS no wp-config.php.
Auto-update de core/plugins ou pipeline de patching.
Backup off-site testado.
# =====================================================================
# Configuração Nginx Hardened para WordPress
# Foco: mitigação de ataques comuns (XSS, SQLi, brute-force, enumeração,
# upload malicioso, hotlinking, exploração de plugins/themes, etc.)
#
# Requer: nginx >= 1.18, PHP-FPM, fail2ban (recomendado), certbot/SSL.
# Ajuste os caminhos (root, fastcgi_pass, server_name, certificados).
# =====================================================================
# ---------------------------------------------------------------------
# 1) Rate limiting zones (declarar no contexto http {} do nginx.conf)
# ---------------------------------------------------------------------
# Adicione no /etc/nginx/nginx.conf dentro do bloco http { } :
#
# limit_req_zone $binary_remote_addr zone=wp_login:10m rate=5r/m;
# limit_req_zone $binary_remote_addr zone=wp_general:10m rate=30r/s;
# limit_req_zone $binary_remote_addr zone=wp_xmlrpc:10m rate=1r/m;
# limit_conn_zone $binary_remote_addr zone=wp_conn:10m;
#
# # Mapa para bloquear user-agents indesejados
# map $http_user_agent $bad_bot {
# default 0;
# ~*(nikto|sqlmap|wpscan) 1;
# ~*(masscan|nmap|zgrab) 1;
# ~*(libwww|curl|wget) 1; # remova se usar webhooks legítimos
# "" 1; # UA vazio
# }
#
# # Mapa para bloquear referers de spam
# map $http_referer $bad_referer {
# default 0;
# ~*(viagra|casino|porn) 1;
# }
# ---------------------------------------------------------------------
# 2) Redirect HTTP -> HTTPS
# ---------------------------------------------------------------------
server {
listen 80;
listen [::]:80;
server_name exemplo.com www.exemplo.com;
# Permite ACME challenge (certbot) sem redirect
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type "text/plain";
allow all;
}
location / {
return 301 https://$host$request_uri;
}
}
# ---------------------------------------------------------------------
# 3) Server block principal HTTPS
# ---------------------------------------------------------------------
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name exemplo.com www.exemplo.com;
root /var/www/wordpress;
index index.php;
# ----- SSL/TLS -----
ssl_certificate /etc/letsencrypt/live/exemplo.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/exemplo.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/exemplo.com/chain.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# ----- Logs -----
access_log /var/log/nginx/wordpress.access.log;
error_log /var/log/nginx/wordpress.error.log warn;
# ----- Tamanhos e timeouts (mitigação de slowloris/DoS) -----
client_max_body_size 32m;
client_body_buffer_size 128k;
client_body_timeout 10s;
client_header_timeout 10s;
keepalive_timeout 30s;
send_timeout 10s;
server_tokens off; # esconde versão do nginx
more_clear_headers Server; # requer headers-more-nginx-module (opcional)
# ----- Security headers -----
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# CSP: ajuste conforme plugins/temas. Comece em report-only para não quebrar o site.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'self';" always;
# ----- Bloqueios baseados em mapas -----
if ($bad_bot) { return 444; }
if ($bad_referer) { return 444; }
# Bloqueia métodos HTTP não usados
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|OPTIONS)$) {
return 444;
}
# Limite de conexões simultâneas por IP
limit_conn wp_conn 20;
# =================================================================
# Bloqueios de paths sensíveis
# =================================================================
# Arquivos ocultos (.git, .env, .htaccess, etc.)
location ~ /\.(?!well-known) {
deny all;
access_log off;
log_not_found off;
return 404;
}
# Arquivos de backup, dump, log, configs
location ~* \.(bak|backup|swp|save|old|orig|original|tmp|sql|log|conf|ini|env|yml|yaml)$ {
deny all;
return 404;
}
# Bloqueia README, LICENSE, CHANGELOG do WP/plugins (revelam versões)
location ~* /(readme|license|changelog|wp-config-sample)\.(txt|html|md)$ {
deny all;
return 404;
}
# wp-config.php nunca pode ser servido
location = /wp-config.php { deny all; return 404; }
# Diretórios críticos do WP
location ~* /wp-content/uploads/.*\.(php|phtml|phar|pl|py|jsp|asp|sh|cgi)$ {
deny all;
return 403;
}
location ~* /wp-content/plugins/.*\.(sql|log|txt|md)$ {
deny all;
return 403;
}
location ~* /wp-content/themes/.*\.(sql|log|txt|md)$ {
deny all;
return 403;
}
# Bloqueia execução de PHP em diretórios que não devem executar
location ~* ^/wp-content/uploads/.*\.php$ { deny all; return 403; }
location ~* ^/wp-includes/.*\.php$ { deny all; return 403; }
location ~* ^/wp-content/cache/.*\.php$ { deny all; return 403; }
# Enumeração de usuários via ?author=N
if ($args ~* "author=\d+") {
return 403;
}
# Bloqueia install.php e upgrade.php após instalação (libere temporariamente se precisar)
location = /wp-admin/install.php { deny all; return 403; }
location = /wp-admin/upgrade.php { deny all; return 403; }
# =================================================================
# XML-RPC: principal vetor de brute-force e amplificação
# Recomendado: bloquear totalmente. Se precisar (Jetpack/app móvel),
# comente o "deny all" e use o limit_req abaixo.
# =================================================================
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
return 444;
# --- alternativa com rate limit ---
# limit_req zone=wp_xmlrpc burst=2 nodelay;
# include fastcgi_params;
# fastcgi_pass unix:/run/php/php8.2-fpm.sock;
# fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# =================================================================
# wp-login.php e wp-admin: rate limit + (opcional) restrição de IP
# =================================================================
location = /wp-login.php {
limit_req zone=wp_login burst=2 nodelay;
# Descomente para restringir login a IPs corporativos:
# allow 203.0.113.0/24;
# deny all;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_intercept_errors on;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
location ^~ /wp-admin/ {
limit_req zone=wp_general burst=20 nodelay;
# Descomente para restringir wp-admin por IP:
# allow 203.0.113.0/24;
# deny all;
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY ""; # mitigação httpoxy
}
}
# admin-ajax precisa ficar acessível (muitos plugins usam no front)
location = /wp-admin/admin-ajax.php {
limit_req zone=wp_general burst=50 nodelay;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
}
# =================================================================
# WAF leve via regex em query string e URI
# =================================================================
set $block_sql_injections 0;
if ($query_string ~* "union.*select.*\(") { set $block_sql_injections 1; }
if ($query_string ~* "concat.*\(") { set $block_sql_injections 1; }
if ($query_string ~* "(<|%3C).*script.*(>|%3E)") { set $block_sql_injections 1; }
if ($query_string ~* "base64_(en|de)code\(") { set $block_sql_injections 1; }
if ($query_string ~* "(eval\(|GLOBALS=|REQUEST=)") { set $block_sql_injections 1; }
if ($query_string ~* "\.\./") { set $block_sql_injections 1; }
if ($block_sql_injections = 1) {
return 403;
}
# =================================================================
# Roteamento WordPress padrão
# =================================================================
location / {
limit_req zone=wp_general burst=50 nodelay;
try_files $uri $uri/ /index.php?$args;
}
# PHP handler (somente para arquivos que existem)
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTP_PROXY "";
# Evita execução de PHP via upload com extensão dupla (foo.jpg.php)
fastcgi_param REDIRECT_STATUS 200;
fastcgi_read_timeout 60s;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_intercept_errors on;
}
# =================================================================
# Cache e hardening de assets estáticos
# =================================================================
location ~* \.(jpg|jpeg|png|gif|webp|svg|ico|css|js|woff2?|ttf|eot|otf|mp4|webm)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri =404;
# Anti hotlinking (opcional - descomente e ajuste o domínio)
# valid_referers none blocked exemplo.com *.exemplo.com;
# if ($invalid_referer) { return 403; }
}
# Robots e favicon
location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { log_not_found off; access_log off; allow all; }
# Página de erro genérica (não vaza stack)
error_page 500 502 503 504 /50x.html;
location = /50x.html { internal; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment