Hardening de seguridad de Nginx en Ubuntu y Debian

12 min de lectura·Matthieu·debiannginxubuntutlssecurityhardening|

Refuerza Nginx más allá de su configuración predeterminada con cabeceras de seguridad, TLS 1.3, HSTS, restricción de métodos y controles de acceso. Cada directiva se vincula al ataque concreto que previene.

Una instalación predeterminada de Nginx sirve tráfico. No lo protege. La configuración por defecto expone la versión del servidor, acepta cualquier método HTTP, no envía cabeceras de seguridad y usa los parámetros TLS que OpenSSL proporciona.

Esta guía endurece Nginx en Debian 12 y Ubuntu 24.04. Cada sección nombra primero la amenaza y luego muestra la directiva que la bloquea. Puedes aplicar estos cambios en un servidor en producción sin tiempo de inactividad.

Requisitos previos:

Almacenamos todas las directivas de hardening en un único archivo include. Esto mantiene tus bloques server limpios y facilita las auditorías:

sudo touch /etc/nginx/snippets/security-hardening.conf

Cada bloque server que necesite hardening recibe una sola línea:

include /etc/nginx/snippets/security-hardening.conf;

Después de cada cambio en esta guía, prueba y recarga:

sudo nginx -t && sudo systemctl reload nginx

¿Cómo oculto la versión del servidor Nginx?

Añade server_tokens off; al bloque http en /etc/nginx/nginx.conf. Esto elimina el número de versión de la cabecera de respuesta Server y de las páginas de error predeterminadas. Los atacantes escanean versiones específicas con CVE conocidos. Ocultar la versión no corrige vulnerabilidades, pero aumenta el coste de los ataques dirigidos.

Añade en /etc/nginx/nginx.conf dentro del bloque http:

server_tokens off;

Esto va en la configuración principal, no en el snippet, porque se aplica globalmente.

Tras recargar, comprueba la cabecera de respuesta:

curl -sI https://your-domain.com | grep -i server
Server: nginx

Sin número de versión. Antes de este cambio, mostraba algo como Server: nginx/1.24.0.

¿Cómo bloqueo peticiones a nombres de host desconocidos?

Cuando alguien accede directamente a la IP de tu servidor o usa un nombre de host no reconocido, Nginx sirve el primer bloque server que encuentra. Esto abre la puerta a ataques de DNS rebinding y permite a los escáneres identificar tus servicios.

Un bloque server predeterminado que rechaza todas las peticiones no coincidentes resuelve el problema. Añádelo como el primer bloque server que Nginx carga:

sudo nano /etc/nginx/sites-available/00-default-deny.conf
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;

    server_name _;

    # Self-signed or snakeoil cert just to complete the TLS handshake
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

    return 444;
}

El código de estado 444 es específico de Nginx. Cierra la conexión inmediatamente sin enviar respuesta.

Actívalo:

sudo ln -s /etc/nginx/sites-available/00-default-deny.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

En Debian 12 y Ubuntu 24.04, instala el paquete ssl-cert si falta el certificado snakeoil:

sudo apt install ssl-cert

Pruébalo accediendo directamente por IP:

curl -sI -k https://YOUR_SERVER_IP

La conexión se cierra con una respuesta vacía o un error de curl (código de salida 52). Ningún contenido servido.

¿Qué configuración TLS debo usar para Nginx en 2026?

Usa el perfil Intermediate de Mozilla: TLS 1.2 y 1.3 con suites de cifrado con forward secrecy. Esto cubre clientes hasta Firefox 27 y Android 4.4.2 mientras descarta protocolos inseguros. TLS 1.0 y 1.1 son vulnerables a POODLE y BEAST y están obsoletos desde la RFC 8996 (2021). Si todos tus clientes soportan TLS 1.3, usa el perfil Modern (ssl_protocols TLSv1.3; solamente).

El hardening TLS se volvió más urgente a principios de 2026. CVE-2026-1642 (CVSS 5.9) demostró que el manejo TLS upstream de Nginx tenía una condición de carrera que permitía inyección MitM antes de completar el handshake. La corrección llegó en Nginx 1.28.2 y 1.29.5. Comprueba tu versión con nginx -v y actualiza si es necesario.

Perfil Protocolos Cliente más antiguo Caso de uso
Modern Solo TLS 1.3 Firefox 63, Chrome 70 APIs, aplicaciones web modernas
Intermediate TLS 1.2 + 1.3 Firefox 27, Android 4.4 Servidores de propósito general
Old TLS 1.0 + 1.1 + 1.2 + 1.3 IE 8 en XP Solo compatibilidad legacy

Genera los parámetros DH para las suites de cifrado DHE del perfil Intermediate. Tarda unos minutos:

sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048
sudo chmod 644 /etc/nginx/dhparam.pem

Añade el hardening TLS en /etc/nginx/snippets/security-hardening.conf:

# TLS protocols - Mozilla Intermediate profile
ssl_protocols TLSv1.2 TLSv1.3;

# Cipher suites - Mozilla Intermediate profile (version 5.7)
# TLS 1.3 suites are configured automatically by OpenSSL
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305';

ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparam.pem;

# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

¿Por qué ssl_prefer_server_ciphers off? Con solo cifrados fuertes en la lista, la preferencia del cliente ofrece mejor rendimiento. Los clientes eligen el cifrado que su hardware acelera mejor.

¿Por qué ssl_session_tickets off? Los tickets de sesión usan una clave global del servidor. Si esa clave se filtra, un atacante puede descifrar todas las sesiones grabadas. Sin tickets, la caché de sesión compartida proporciona reanudación solo en el mismo servidor, lo cual es suficiente para configuraciones de servidor único.

Nota sobre OCSP stapling: Let's Encrypt cerró su servicio OCSP en agosto de 2025. Si usas certificados de Let's Encrypt, elimina las directivas ssl_stapling. Causarán advertencias en tus logs. Let's Encrypt ahora publica información de revocación exclusivamente mediante CRL.

Recarga y prueba desde tu máquina local:

openssl s_client -connect your-domain.com:443 -tls1_2 </dev/null 2>/dev/null | grep "Protocol\|Cipher"
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384

Confirma que TLS 1.1 se rechaza:

openssl s_client -connect your-domain.com:443 -tls1_1 </dev/null 2>&1 | head -5

El handshake falla. Para una auditoría completa, envía tu dominio a SSL Labs. Apunta a un grado A o A+.

¿Cómo activo HSTS en Nginx?

HSTS (HTTP Strict Transport Security) indica a los navegadores que solo se conecten por HTTPS durante un periodo determinado. Sin él, un atacante MitM puede interceptar la petición HTTP inicial y degradar la conexión. Una vez que un navegador ve la cabecera HSTS, rechaza conexiones HTTP en claro a tu dominio hasta que expire max-age.

Añade en /etc/nginx/snippets/security-hardening.conf:

# HSTS - 2 years, include subdomains
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

La bandera always asegura que Nginx envía esta cabecera también en respuestas de error (4xx, 5xx). Sin always, una respuesta 403 o 500 no incluiría la cabecera, dejando una ventana para ataques de degradación.

Antes de añadir includeSubDomains, confirma que todos tus subdominios soportan HTTPS. Esta directiva se aplica a cada subdominio. Un subdominio sin certificado válido se vuelve inaccesible.

Precarga HSTS: Añadir preload a la cabecera y enviar tu dominio a hstspreload.org fija la aplicación de HTTPS en los navegadores. Es permanente. La eliminación tarda meses. Solo añade preload cuando estés seguro de que cada subdominio siempre servirá HTTPS.

Tras recargar:

curl -sI https://your-domain.com | grep -i strict
strict-transport-security: max-age=63072000; includeSubDomains

¿Qué cabeceras de seguridad debe tener todo servidor Nginx?

Cinco cabeceras de respuesta protegen contra ataques comunes del lado del navegador: MIME sniffing, clickjacking, cross-site scripting, filtración del referrer y abuso de APIs del navegador. Añade las cinco con la bandera always para que apliquen también a respuestas de error. La cabecera obsoleta X-XSS-Protection no debe incluirse. Los navegadores modernos han eliminado sus auditores XSS, y la propia cabecera introducía vulnerabilidades en algunos casos.

Añade en /etc/nginx/snippets/security-hardening.conf:

# Prevent MIME type sniffing - stops browsers from interpreting files as a
# different content type than declared, blocking drive-by download attacks
add_header X-Content-Type-Options "nosniff" always;

# Clickjacking protection - prevents your pages from being embedded in
# iframes on other sites
add_header X-Frame-Options "SAMEORIGIN" always;

# Content Security Policy - controls which sources can load scripts, styles,
# images, and other resources. Start restrictive, loosen as needed.
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;

# Referrer Policy - controls how much URL information the browser sends
# when navigating away from your site
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Permissions Policy - disables browser features you don't use, preventing
# compromised scripts from accessing camera, microphone, etc.
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;

interest-cohort=() aparecía en guías antiguas para bloquear Google FLoC. FLoC fue abandonado en 2023. Los navegadores modernos no reconocen esta directiva y muestran advertencias en la consola si la incluyes.

Cabecera Valor Amenaza mitigada
X-Content-Type-Options nosniff Ataques de confusión MIME, descargas furtivas
X-Frame-Options SAMEORIGIN Clickjacking mediante iframe
Content-Security-Policy default-src 'self' XSS, inyección de datos, carga no autorizada de recursos
Referrer-Policy strict-origin-when-cross-origin Filtración de URL a terceros
Permissions-Policy camera=(), microphone=()... Abuso de APIs del navegador por scripts inyectados

Ajuste de CSP: El ejemplo anterior es estricto. La mayoría de aplicaciones necesitan ajustes. Si tu sitio carga scripts desde un CDN, añádelo: script-src 'self' https://cdn.example.com;. Usa la consola de desarrollador del navegador para encontrar violaciones CSP y relaja la política de forma incremental. Nunca uses unsafe-eval sin entender el riesgo.

La trampa de herencia de add_header

Nginx descarta todas las directivas add_header de bloques padres cuando un bloque location hijo define su propio add_header. Si añades una cabecera dentro de un bloque location, todas las cabeceras de seguridad del nivel server o http desaparecen de las respuestas de esa location.

Dos formas de manejar esto:

  1. Nginx 1.29.3+: Usa add_header_inherit merge; en el bloque hijo para conservar las cabeceras padres.
  2. Versiones anteriores: Repite la línea include /etc/nginx/snippets/security-hardening.conf; dentro de cualquier bloque location que añada sus propias cabeceras.

Recarga y comprueba:

sudo nginx -t && sudo systemctl reload nginx
curl -sI https://your-domain.com | grep -iE "x-content-type|x-frame|content-security|referrer-policy|permissions-policy"

Las cinco cabeceras aparecen en la salida. Comprueba también una ruta location específica, no solo la raíz, para detectar problemas de herencia.

¿Cómo desactivo métodos HTTP inseguros en Nginx?

La mayoría de sitios solo necesitan GET, HEAD y POST. Métodos como DELETE, PUT, TRACE y OPTIONS (cuando no se usan) amplían tu superficie de ataque. TRACE en particular puede filtrar cookies y tokens de autenticación mediante ataques XST (cross-site tracing).

El enfoque más limpio en Nginx estándar es limit_except, diseñado específicamente para la restricción de métodos. Colócalo dentro de tus bloques location:

location / {
    limit_except GET POST {
        deny all;
    }

    # ... your existing config
}

limit_except GET permite implícitamente HEAD (HEAD es un GET sin cuerpo según la especificación HTTP). Cualquier método no listado devuelve 403 Forbidden.

Para un enfoque global con map (añade en nginx.conf en el bloque http):

map $request_method $method_not_allowed {
    default  1;
    GET      0;
    HEAD     0;
    POST     0;
}

Luego en tu bloque server:

if ($method_not_allowed) {
    return 405;
}

El enfoque limit_except es preferible cuando necesitas control por location. El enfoque map funciona cuando quieres una política general en todo el servidor.

Prueba con un método bloqueado:

curl -sI -X DELETE https://your-domain.com

Devuelve HTTP/1.1 403 Forbidden o HTTP/1.1 405 Not Allowed.

curl -sI -X GET https://your-domain.com

Devuelve HTTP/1.1 200 OK (o tu código de respuesta habitual).

¿Cómo restrinjo el acceso a rutas de administración por dirección IP?

Los paneles de administración, dashboards de monitorización y endpoints de estado no deberían ser accesibles desde internet. Las directivas allow y deny de Nginx restringen el acceso por dirección IP a nivel de location.

location /admin {
    allow 203.0.113.10;      # Your office IP
    allow 198.51.100.0/24;   # Your VPN range
    deny all;

    # ... proxy_pass or other directives
}

location /nginx-status {
    stub_status;
    allow 127.0.0.1;
    allow ::1;
    deny all;
}

El orden importa. Nginx evalúa allow y deny de arriba a abajo y se detiene en la primera coincidencia. Coloca deny all al final.

Si tu IP cambia con frecuencia, considera restringir el acceso a través de tu firewall o usa una VPN. Las ACL por IP en Nginx son una segunda capa, no un sustituto de la autenticación.

Desde una IP permitida:

curl -sI https://your-domain.com/admin

Devuelve tu respuesta habitual (200, 302, etc.).

Desde otra IP (o a través de un proxy), la misma petición devuelve HTTP/1.1 403 Forbidden.

Consulta el log de acceso para las peticiones denegadas:

sudo tail -5 /var/log/nginx/access.log | grep admin

¿Qué límites de buffers y timeouts previenen ataques DoS en Nginx?

Los tamaños de buffer y timeouts predeterminados son generosos. Un atacante puede aprovechar esto enviando cabeceras grandes, peticiones lentas o cuerpos sobredimensionados para ocupar conexiones worker. Ajustar estos valores limita el daño que una sola conexión puede causar.

Añade en /etc/nginx/snippets/security-hardening.conf:

# Maximum allowed request body size - reject uploads larger than 10MB
client_max_body_size 10m;

# Buffer for reading client request body
client_body_buffer_size 16k;

# Buffer for reading large client headers
large_client_header_buffers 4 16k;

# Timeouts - how long Nginx waits for client data
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;
keepalive_timeout 65s;
Directiva Valor Demasiado alto Demasiado bajo
client_max_body_size 10m Permite subidas enormes que llenan el disco Rompe formularios de subida
client_body_buffer_size 16k Desperdicio de memoria por conexión Fuerza archivos temporales para POSTs pequeños
large_client_header_buffers 4 16k Desperdicio de memoria bajo ataques de cabeceras Rompe apps con cookies/URLs grandes
client_body_timeout 30s Conexiones slow loris se mantienen abiertas Desconecta usuarios en redes lentas
client_header_timeout 30s Igual que arriba Igual que arriba
send_timeout 30s Ocupa conexiones para clientes lentos Corta descargas grandes
keepalive_timeout 65s Agotamiento del pool de conexiones Handshakes TLS adicionales

Ajusta client_max_body_size según tu aplicación. Si gestionas subidas de archivos, configúralo según lo que tu app espera. Un valor predeterminado de 10 MB es razonable para la mayoría de aplicaciones web.

Recarga y prueba el límite de tamaño del cuerpo:

dd if=/dev/zero bs=1M count=11 2>/dev/null | curl -s -X POST --data-binary @- -o /dev/null -w "%{http_code}" https://your-domain.com/

Esperado: 413 (Request Entity Too Large).

Para rate limiting y protección DDoS más avanzada, consulta Limitación de tasa en Nginx y protección contra DDoS.

¿Cómo pruebo la configuración de hardening completa?

Una vez realizados todos los cambios, recarga y repasa los puntos clave. Empieza con un test de configuración y recarga:

sudo nginx -t && sudo systemctl reload nginx

Comprueba cabeceras e información de versión con una sola petición:

curl -sI https://your-domain.com

La cabecera Server debe mostrar nginx sin número de versión. Las seis cabeceras de seguridad (HSTS, X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, Referrer-Policy, Permissions-Policy) deben aparecer en la respuesta.

Para TLS, confirma el protocolo y cifrado desde tu máquina local:

openssl s_client -connect your-domain.com:443 </dev/null 2>/dev/null | grep -E "Protocol|Cipher"

Quieres TLSv1.2 o TLSv1.3 con un cifrado GCM o CHACHA20.

Accede directamente a la IP del servidor para confirmar que el bloque de rechazo predeterminado funciona:

curl -sk https://YOUR_SERVER_IP -o /dev/null -w "%{http_code}"

Un código de respuesta 000 significa que la conexión se cerró sin respuesta, lo cual es correcto.

Prueba un método HTTP bloqueado:

curl -sI -X TRACE https://your-domain.com -o /dev/null -w "%{http_code}"

Debería devolver 403 o 405.

Para una auditoría externa, envía tu dominio a SSL Labs (apunta a grado A o A+) y a securityheaders.com para un informe específico de cabeceras.

Snippet de hardening completo

El archivo /etc/nginx/snippets/security-hardening.conf completo con todo lo de esta guía:

# /etc/nginx/snippets/security-hardening.conf
# Nginx security hardening - include in each server block

# --- TLS Hardening (Mozilla Intermediate profile v5.7) ---
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparam.pem;

# TLS session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

# --- HSTS ---
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

# --- Security Headers ---
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;

# --- Buffer Limits ---
client_max_body_size 10m;
client_body_buffer_size 16k;
large_client_header_buffers 4 16k;

# --- Timeouts ---
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;
keepalive_timeout 65s;

Recuerda también configurar server_tokens off; en el bloque http de /etc/nginx/nginx.conf y crear el bloque server de rechazo predeterminado por separado.

Resolución de problemas

Nginx no recarga tras los cambios:

sudo nginx -t

Lee el error. Indica el archivo y número de línea. Causas comunes: puntos y coma faltantes, erratas en nombres de directivas, referencia a un archivo DH param que no existe.

Cabeceras ausentes en algunas rutas:

La trampa de herencia de add_header. Si un bloque location tiene su propio add_header, todas las cabeceras padres se descartan. Incluye el snippet en ese bloque location o usa add_header_inherit merge; en Nginx 1.29.3+.

CSP bloquea recursos legítimos:

Abre la consola de desarrollador del navegador (F12). Busca errores Refused to load. Nombran el recurso bloqueado y la directiva CSP responsable. Añade la fuente a tu política de forma incremental.

Fallos en el handshake TLS:

journalctl -u nginx -f

Observa errores SSL mientras conectas. Verifica que tu cadena de certificados esté completa:

openssl s_client -connect your-domain.com:443 -servername your-domain.com </dev/null 2>/dev/null | grep "Verify return code"

Esperado: Verify return code: 0 (ok).

Ubicación de logs:

# Error log
sudo tail -20 /var/log/nginx/error.log

# Real-time monitoring
sudo journalctl -u nginx -f

Siguientes pasos: Con el servidor endurecido, configura Limitación de tasa en Nginx y protección contra DDoS para gestionar patrones de tráfico abusivos. Para configuraciones por sitio, consulta Server Blocks de Nginx: Aloja Varios Dominios en un VPS.

Relacionado: Administración de Nginx en un VPS | Estructura de los archivos de configuración de Nginx | Configurar SSL/TLS de Let's Encrypt para Nginx en Debian 12 y Ubuntu 24.04