Logs, seguridad y hardening
En produccion Nginx es el borde: registra quien entra, limita abuso, oculta versiones y reduce superficie de ataque. Este capitulo cierra el manual con operacion y endurecimiento.
Formatos de log
Formato combinado tipico (ya suele venir definido):
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';Formato con timings y upstream (util en proxy):
log_format json_analytics escape=json
'{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"body_bytes":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_addr":"$upstream_addr",'
'"upstream_time":"$upstream_response_time",'
'"cache":"$upstream_cache_status",'
'"host":"$host",'
'"ua":"$http_user_agent"'
'}';
access_log /var/log/nginx/access.json json_analytics;
error_log /var/log/nginx/error.log warn;sudo nginx -t && sudo systemctl reload nginx
sudo tail -f /var/log/nginx/access.jsonNiveles de error_log: debug, info, notice, warn, error, crit. En prod suele bastar warn o error.
Rotacion
Con logrotate (Debian/Ubuntu suelen traer /etc/logrotate.d/nginx):
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
if [ -f /run/nginx.pid ]; then
kill -USR1 $(cat /run/nginx.pid)
fi
endscript
}USR1 reabre logs sin downtime. Sin eso, Nginx sigue escribiendo al inode viejo tras rotar.
Ocultar version y tokens
http {
server_tokens off;
}Evita Server: nginx/1.24.0 en respuestas. No es seguridad fuerte, pero reduce fingerprinting trivial.
Restringir metodos
location / {
limit_except GET HEAD POST {
deny all;
}
proxy_pass http://127.0.0.1:3000;
}O denegar TRACE/TRACK de forma global con if (usar con cuidado) o en el WAF/firewall.
Rate limiting
# en http {}
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
location /login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
limit_req zone=api burst=60 nodelay;
limit_conn addr 20;
proxy_pass http://127.0.0.1:8080/;
}
}rate=5r/m: media de 5 peticiones por minuto por IP.burst: cola/pico permitido.nodelay: no suaviza el burst; excedentes fallan al momento.limit_conn: conexiones simultaneas por IP.
Detras de Cloudflare/ALB, la IP real no es $remote_addr. Usa real_ip:
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;Ajusta set_real_ip_from a las redes del proxy de confianza.
Allow / deny
location /admin/ {
allow 10.0.0.0/8;
allow 203.0.113.10;
deny all;
proxy_pass http://127.0.0.1:8080;
}Para basic auth:
sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd ops
sudo chmod 640 /etc/nginx/.htpasswd
sudo chown root:www-data /etc/nginx/.htpasswdlocation /admin/ {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://127.0.0.1:8080;
}Tamano de cuerpos y buffers
client_max_body_size 10m;
client_body_timeout 15s;
client_header_timeout 15s;
large_client_header_buffers 4 16k;Limita uploads enormes y slowloris basico. APIs de ficheros grandes suben client_max_body_size solo en ese location.
TLS y headers (repaso de hardening)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
server_tokens off;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
add_header Content-Security-Policy "default-src 'self'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;HSTS preload solo si entiendes el compromiso (inclusion en lista de navegadores).
Desactivar sites y locaciones peligrosas
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}444 cierra la conexion sin respuesta: util para peticiones al IP crudo o Host desconocido.
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~* \.(php|aspx|jsp)$ {
# si no sirves esos runtimes:
return 404;
}Permisos y usuario
# config solo root escribe
sudo chown -R root:root /etc/nginx
sudo find /etc/nginx -type f -exec chmod 644 {} \;
sudo chmod 600 /etc/nginx/ssl/*.key 2>/dev/null || true
sudo chmod 640 /etc/nginx/.htpasswdWorkers corren como www-data/nginx. No ejecutes la app como root ni compartas la clave TLS con ese usuario si puedes evitarlo (el master lee la clave al arrancar).
Firewall
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full' # 80 + 443
sudo ufw enable
sudo ufw statusBackends solo en localhost:
ss -tlnp | grep -E ':3000|:8080'
# debe mostrar 127.0.0.1, no 0.0.0.0Checklist de hardening
[ ] server_tokens off
[ ] TLS 1.2+ only, redirect HTTP->HTTPS
[ ] default_server que no sirva el site real
[ ] rate limit en login y APIs publicas
[ ] client_max_body_size acotado
[ ] .git / .env denegados
[ ] logs rotados + retencion definida
[ ] backends en 127.0.0.1 o red privada
[ ] nginx -t en CI antes de desplegar config
[ ] certificados monitorizados (caducidad)Ejemplo de server endurecido (esqueleto)
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
server_tokens off;
client_max_body_size 5m;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Strict-Transport-Security "max-age=31536000" always;
access_log /var/log/nginx/app.access.json json_analytics;
error_log /var/log/nginx/app.error.log warn;
location /api/ {
limit_req zone=api burst=40 nodelay;
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
root /var/www/app;
try_files $uri /index.html;
}
location ~ /\. {
deny all;
}
}Buenas practicas
- Logs JSON a un colector (Fluent Bit, Vector, Loki); no solo disco local.
- Rate limits distintos por ruta critica (
/login,/api). default_serverde rechazo + servers conserver_nameexplicitos.- Revisar
error.logtras cada deploy. - Config en git +
nginx -ten pipeline.
Errores habituales
- Rate limit por
$remote_addrdetras de CDN sinreal_ip(todas las peticiones comparten una IP). client_max_body_sizedefault 1m y uploads fallan con413.- Dejar el site default de la distro como catch-all publico.
- Basic auth con
.htpasswdworld-readable. - Debug logging en prod (
error_log ... debug) llenando disco.
Ejercicios
- Define un
log_formatcon$request_timey$upstream_response_time; genera trafico y lee una linea. - Pon
limit_reqa1r/sen un location de prueba y observa429con un buclecurl. - Configura un
default_serverconreturn 444y un server nombrado; comparacurlal IP vs al Host correcto. - Activa
server_tokens offy verifica concurl -Ique no aparece la version.
Cierre del manual
Con estos siete capitulos tienes el ciclo habitual de Nginx en el borde:
instalar -> static -> reverse proxy -> TLS/HTTP2
-> load balancing -> cache/gzip/headers -> logs/hardeningSiguiente lectura recomendada en el repo: Traefik si trabajas discovery dinamico con Docker/Kubernetes, o Docker Compose para montar Nginx junto a tus apps en local.
