Tutoriales de VPS
Refuerzo de seguridad de Nginx en VPS (2026): TLS, HSTS, CSP, cabeceras de seguridad, limitación de velocidad, lista de verificación WAF
Que Nginx funcione no significa que sea seguro. Los riesgos habituales en un VPS incluyen configuración TLS obsoleta, falta de cabeceras de seguridad como HSTS/CSP, IP de origen expuesta directamente, ausencia de limitación de velocidad de peticiones, rutas de administración sin protección y registros de errores que nadie revisa. Este artículo ofrece una lista de verificación de refuerzo de seguridad de Nginx adecuada para sitios pequeños y servicios autoalojados, en orden de puesta en producción.

Nginx es el punto de entrada web más común en un VPS: WordPress, Next.js, n8n, Gitea, paneles, servicios API; la mayoría acaban detrás de Nginx.
Pero muchas configuraciones de Nginx en VPS solo logran "que sea accesible": 80 redirige a 443, proxy inverso al backend, el certificado se renueva. Una vez realmente en producción, los riesgos suelen esconderse en estos lugares: versiones de TLS demasiado antiguas, cabeceras de seguridad ausentes, rutas de administración sin limitación de velocidad, IP de origen expuesta, páginas de error que filtran información, registros de acceso que nadie revisa, Cloudflare activado pero el origen todavía se puede conectar directamente.
Este artículo no explica cómo instalar Nginx, ni repite la resolución de problemas de 502, 521 o renovación de certificados. Solo hay un objetivo: reforzar Nginx en un VPS normal desde "funciona" hasta "más adecuado para exposición pública a largo plazo".
Antes de poner en producción, se recomienda revisar en este orden:
- Abrir solo 80/443; los servicios backend no deben exponerse directamente a internet;
- Conservar solo TLS 1.2 / TLS 1.3;
- Activar HSTS, pero no pasar directamente a preload;
- Añadir cabeceras de seguridad básicas: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy;
- Ocultar el número de versión de Nginx;
- Aplicar limitación de velocidad a inicio de sesión, búsqueda, API y Webhook;
- Limitar el tamaño de subida y los tiempos de espera;
- Si usas Cloudflare, permitir solo las IP de origen de Cloudflare;
- Los registros deben permitir ver ataques, limitaciones de velocidad y códigos de estado anómalos;
- Verificar externamente con SSL Labs, Mozilla Observatory y SecurityHeaders.
No busques maximizar todas las cabeceras de seguridad de una vez. El objetivo de la configuración de seguridad no es "copiar la configuración más extrema", sino endurecer gradualmente sin afectar al negocio.
Mucha gente cree que solo ha expuesto Nginx, cuando en realidad Docker, Node, bases de datos y Redis también están escuchando en internet.
Primero revisa los puertos en escucha en el VPS:
ss -tulpen
Fíjate en 0.0.0.0 y ::. Si ves esto, hay que alertar:
0.0.0.0:3000 # 应用端口直接暴露
0.0.0.0:5432 # PostgreSQL 暴露
0.0.0.0:6379 # Redis 暴露
0.0.0.0:3306 # MySQL 暴露
En producción se recomienda más bien:
- Nginx escucha en 80/443 públicos;
- Las aplicaciones escuchan solo en
127.0.0.1o en la red interna de Docker; - Las bases de datos y Redis no mapean puertos públicos;
- El panel de administración va por VPN, túnel SSH, lista blanca o al menos con Basic Auth.
En Docker Compose no hagas esto:
ports:
- "3000:3000"
- "5432:5432"
Una forma más segura es exponer el backend solo a Nginx:
services:
app:
expose:
- "3000"
O enlazar solo a localhost:
ports:
- "127.0.0.1:3000:3000"
Si la base de datos ya está expuesta a internet, lee primero esto:
Seguir usando TLS 1.0 / 1.1 en 2026 ya no es adecuado. La mayoría de sitios pequeños pueden simplemente conservar TLS 1.2 y 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 qué ssl_prefer_server_ciphers off? Porque la configuración TLS moderna depende más de la negociación entre cliente y servidor, sobre todo porque TLS 1.3 ya no funciona con el orden de cifrados antiguo. Salvo que conozcas muy bien los requisitos de compatibilidad, no copies una lista larga de cifrados de hace diez años.
Si quieres generar una configuración más estándar, puedes consultar el Mozilla SSL Configuration Generator (ahora migrado a TLSRef Configurator). Al generarla, elige según la audiencia del sitio:
| Escenario | Nivel recomendado |
|---|---|
| Blogs normales, SaaS, sitios de herramientas | Intermediate |
| Solo navegadores modernos | Modern |
| Debe ser compatible con dispositivos muy antiguos | Old, pero no recomendado |
El fallo de renovación de certificados no es un problema de refuerzo, pero hace que el sitio quede inaccesible directamente. Si certbot falla a menudo, consulta:
HSTS le dice al navegador: a partir de ahora, este dominio debe accederse por HTTPS, evitando ataques de degradación.
Configuración básica:
add_header Strict-Transport-Security "max-age=31536000" always;
Tras confirmar que todos los subdominios admiten HTTPS, considera:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
No añadas preload de inmediato. Una vez que entras en la lista de preload del navegador, si más adelante algún subdominio no puede usar HTTPS temporalmente, el proceso de eliminación es muy lento y el coste de resolución de problemas es muy alto.
El proceso correcto es:
- Primero asegúrate de que el dominio principal funciona bien con HTTPS;
- Activa
max-age=300y prueba unos minutos; - Luego súbelo a un día, una semana;
- Finalmente súbelo a un año o dos años;
- Solo tras confirmar que todos los subdominios admiten HTTPS, considera
includeSubDomains.
Si ya tienes un bucle de redirección HTTPS, revisa primero aquí:
El OWASP Secure Headers Project recomienda usar cabeceras de respuesta HTTP para reducir riesgos del lado del navegador. La sintaxis habitual en Nginx es la siguiente:
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;
Función de estas cabeceras:
| Header | Función | Riesgo |
|---|---|---|
X-Content-Type-Options | Evita el sniffing MIME del navegador | Normalmente seguro |
X-Frame-Options | Reduce el riesgo de clickjacking | Cuidado con negocios que incrustan iframes |
Referrer-Policy | Reduce la fuga de Referer | Los sistemas de analítica pueden verse afectados |
Permissions-Policy | Desactiva capacidades del navegador innecesarias | Los negocios con WebRTC/geolocalización deben abrirlas por separado |
Ten en cuenta que always es importante. En la documentación oficial de Nginx, add_header por defecto solo surte efecto en algunos códigos de estado; al añadir always, las respuestas de error también llevan estas cabeceras.
Content-Security-Policy es potente, pero también es lo que más fácilmente rompe un sitio web. Puede restringir desde dónde se cargan scripts, imágenes, fuentes e iframes, pero los plugins de WordPress, scripts de analítica, scripts publicitarios y sistemas de comentarios pueden quedar bloqueados por ella.
Se recomienda empezar con una versión 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;
No es la CSP más estricta, pero sirve como primera versión. Tras ponerla en producción, revisa en la consola del navegador si hay recursos bloqueados y luego endurece gradualmente.
Un enfoque más estable es usar primero Report-Only:
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;
Tras confirmar que no hay falsos positivos, cambia a la CSP formal.
Ocultar el número de versión de Nginx:
server_tokens off;
Reduce la exposición de información, pero no es una defensa central. Los atacantes no dejan de escanear solo porque no ven el número de versión. Lo realmente importante es actualizar el sistema y Nginx a tiempo:
apt update
apt list --upgradable | grep nginx
Si usas Baota, OpenResty o Nginx Proxy Manager, también debes prestar atención a su propio ritmo de actualización, no solo a los paquetes del sistema.
Las páginas de inicio de sesión, páginas de búsqueda, API, Webhook, envío de comentarios y endpoints de restablecimiento de contraseña son adecuados para limitación de velocidad.
Define la zona de limitación en el bloque 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;
Úsala en locations concretas:
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;
}
El limit_req de Nginx usa el algoritmo de cubo con fugas. rate=5r/m significa una media de 5 veces por minuto, burst=5 permite ráfagas cortas, pero al superarlo se retrasará o rechazará.
Si no estás seguro del umbral, puedes hacer primero un dry-run:
limit_req_dry_run on;
limit_req_log_level notice;
Observa primero los registros y luego aplica el bloqueo. De lo contrario, es fácil bloquear también a usuarios reales.
Si ya sufres un ataque CC, consulta:
Muchos problemas de VPS no se deben a ataques sofisticados, sino a subidas, conexiones lentas y tiempos de espera sin límites.
Configuración básica:
client_max_body_size 20m;
client_body_timeout 15s;
client_header_timeout 15s;
keepalive_timeout 30s;
send_timeout 30s;
Si son subidas de imágenes desde el panel, puedes relajar solo la ruta de subida:
location /admin/upload {
client_max_body_size 100m;
proxy_pass http://127.0.0.1:3000;
}
No pongas client_max_body_size 500m sin criterio en todo el sitio. Cuanto mayor sea la subida, más fácil es abusar de ella y más fácil es provocar presión de disco y memoria.
Si encuentras un 413, consulta:
Mucha gente activa Cloudflare WAF y cree que el origen está seguro. El problema es: si el atacante conoce la IP de tu VPS, puede saltarse Cloudflare y atacar el origen directamente.
Enfoque de refuerzo:
- El DNS solo expone registros proxy de Cloudflare;
- El firewall del origen solo permite a las IP de Cloudflare acceder a 80/443;
- El puerto SSH solo permite tu propia IP o va por VPN;
- No filtres la IP de origen en correos, DNS históricos o páginas de error.
Ejemplo 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
Los rangos de IP de Cloudflare cambian; en producción se recomienda escribir un script para sincronizar periódicamente la lista oficial de IP, en lugar de mantenerla manualmente a largo plazo.
A continuación, una plantilla de partida adecuada para aplicaciones de proxy inverso en un VPS normal:
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;
}
}
Debes probar antes de poner en producción:
nginx -t
systemctl reload nginx
curl -I https://example.com
El refuerzo no termina al escribir la configuración. Valida al menos con estas herramientas:
| Herramienta | Qué revisar |
|---|---|
| SSL Labs Server Test | Versiones TLS, cadena de certificados, calificación |
| Mozilla Observatory | Cabeceras de seguridad, CSP, HSTS |
| SecurityHeaders.com | Si faltan cabeceras |
| curl -I | Si las respuestas reales llevan cabeceras |
| nginx -t | Si la sintaxis de la configuración es correcta |
Comprobación local:
curl -I https://example.com | grep -Ei "strict|content-security|x-frame|referrer|permissions"
Si usas Cloudflare, prueba a la vez:
- El dominio accedido a través de Cloudflare;
- La conexión directa a la IP de origen (si puedes conectar directamente, la protección del origen aún no está hecha);
- Páginas reales del negocio, no solo la página de inicio.
No. Las cabeceras deben servir al negocio. Cabeceras como CSP, COOP y COEP pueden afectar a scripts de terceros, iframes, pagos, analítica e inicio de sesión. Si no entiendes el impacto, empieza con cabeceras básicas y endurece gradualmente.
Sí. Cloudflare es la primera capa; Nginx es la última puerta del origen. Especialmente en rutas de negocio como panel, API y Webhook, la limitación de velocidad local de Nginx evita lagunas en las reglas o conexiones directas al origen.
En sitios pequeños de VPS normales no se recomienda activarlo desde el principio. Primero confirma que todos los subdominios tienen HTTPS y luego aumenta gradualmente max-age. Una vez que preload entra por error en la lista, la recuperación es muy lenta.
Solo reduce la exposición de información; no sustituye las actualizaciones. Lo realmente importante es actualizar a tiempo Nginx, OpenSSL, el kernel del sistema y las aplicaciones upstream.
Caddy hace muy bien el HTTPS automático, pero las cabeceras de seguridad, la limitación de velocidad, la protección del origen y el control de acceso al panel siguen necesitando configuración según el negocio. Lo que Caddy ahorra es la gestión de certificados, no todo el trabajo de seguridad.
El refuerzo de seguridad de Nginx en un VPS no consiste en copiar la "configuración más fuerte" de internet, sino en cerrar por orden de riesgo: primero reducir la superficie de exposición, luego corregir TLS, después añadir cabeceras de seguridad, luego limitar la velocidad en rutas de alto riesgo y finalmente validar con herramientas externas.
Para la mayoría de sitios pequeños, herramientas autoalojadas y desarrolladores independientes, hacer esto ya evita una gran cantidad de escaneos de bajo coste, configuraciones erróneas y riesgos de conexión directa al origen. Un VPS realmente estable a largo plazo no es el que nunca es atacado, sino aquel en el que cada capa tiene límites, registros y margen de reversión.