Mi sitio web se cayó esta mañana. Esto es exactamente lo que pasó — y cómo lo resolví.

A las 4:40 AM mi servidor dejó de responder. Sin alertas, sin aviso, sin razón obvia. Solo un sitio caído y una notificación de Cloudflare sobre un dominio que vence en 25 días — lo cual no tenía nada que ver.

Esto es un incidente real que ocurrió hoy. Aquí está el análisis completo.


Paso 1: Dejar de adivinar y leer los logs

El primer instinto cuando un servidor se cae es reiniciar todo y esperar que funcione. Eso es el movimiento equivocado. Si no sabes por qué crasheó, volverá a crashear.

sudo journalctl -xe --since "10 hours ago"

Los logs mostraron que el servidor se había reiniciado a las 14:25 UTC. No fue un crash de servicio — fue un reboot completo del sistema. Eso acotó el problema inmediatamente.

sudo tail -n 100 /var/log/nginx/error.log

Ahí estaba la historia real.


Paso 2: El ataque que nadie notó

Entre las 00:11 y las 00:11:50 AM — solo 40 segundos — una sola IP de AWS Seúl (3.38.148.216) envió más de 80 requests escaneando archivos .env expuestos:

GET /FRONTEND/.env
GET /.env.production
GET /admin/.env.local
GET /laravel/.env.local
... (80+ variaciones)

Nginx bloqueó cada una con access forbidden by rule. Los bloqueos funcionaron. Pero aquí está el problema: bloquear un request con un 403 no es lo mismo que banear la IP.

  • Un 403 significa que tu servidor recibió el request, lo procesó y respondió.
  • Un ban real significa que la conexión se corta a nivel de firewall — el atacante no recibe nada.

Esos 80+ requests saturaron el pool de workers de PHP-FPM. A las 4:40 AM, los workers estaban en timeout. El OOM killer intervino, empezó a terminar procesos, y el servidor se reinició.


Paso 3: La crisis de recursos oculta

free -h
Mem:    416Mi    192Mi    13Mi
Swap:     0B       0B      0B

416MB RAM. Cero swap.

Una instancia nano de Lightsail (~$3.50/mes) corriendo Nginx + PHP-FPM + una aplicación web — sin espacio de swap configurado. Cuando la memoria se agotó, no había fallback. El OOM killer no tuvo opciones.

Este es un error de configuración que he visto en docenas de servidores. Trivial de corregir, catastrófico de ignorar.


Paso 4: Fail2ban estaba activo pero no hacía nada

El servidor tenía Fail2ban instalado y corriendo — la herramienta que debería banear automáticamente IPs atacantes después de un umbral de requests fallidos. Revisé su estado:

sudo fail2ban-client status nginx-req-limit
Currently banned: 0
Total banned:     0

Cero bans. Durante un ataque de 80 requests.

El filtro estaba configurado para hacer match con:

limiting requests, excess:... by zone

Pero las entradas reales en el log de Nginx decían:

access forbidden by rule

El regex nunca hizo match. Fail2ban observó todo el ataque sin hacer nada — no porque no estuviera corriendo, sino porque el patrón era incorrecto. Una diferencia sutil con consecuencias significativas.


El fix: tres cosas, 10 minutos

1. Agregar swap para que el OOM killer tenga un respaldo:

sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

2. Corregir el regex de Fail2ban:

[Definition]
failregex = access forbidden by rule, client: <HOST>
            limiting requests, excess:.* by zone.*client: <HOST>
ignoreregex =

3. Verificar que realmente funciona:

sudo fail2ban-regex /var/log/nginx/error.log /etc/fail2ban/filter.d/nginx-req-limit.conf

Resultado: 299 matches del ataque de esta mañana. Funcionando como debe.


Lo que esto demuestra en realidad

Esto no fue una falla catastrófica. Fue una cascada de pequeñas configuraciones incorrectas:

  • Una herramienta de seguridad instalada pero mal configurada (Fail2ban con regex incorrecto)
  • Un límite de recursos sin fallback (sin swap en una instancia de poca memoria)
  • Una protección que funcionaba técnicamente pero no operacionalmente (403 de Nginx vs. ban de IP)

Cada una por separado es manejable. Juntas, derribaron un servidor.

Las habilidades que resolvieron esto en menos de una hora — leer logs de kernel y de aplicación, correlacionar eventos en el tiempo, distinguir síntomas de causas raíz — son las mismas que previenen que estos problemas lleguen a producción.


La pregunta que vale la pena hacerse

¿Cuándo fue la última vez que alguien revisó la configuración de tu servidor, tu setup de monitoreo y tus herramientas de seguridad — no solo para instalarlas, sino para verificar que realmente funcionan bajo condiciones reales?

Instalado ≠ configurado. Configurado ≠ probado. Probado ≠ funcionando.

Si tienes infraestructura en producción y no tienes respuestas claras a esas preguntas, vale la pena conversarlo.

Hablemos sobre tu infraestructura.


Rubén Rangel — Senior Full-Stack Developer | 17+ años construyendo y manteniendo aplicaciones web en producción | Seguridad, monitoreo y respuesta a incidentes | Disponible para contratos y posiciones full-time.