ModSecurity es un cortafuegos de aplicaciones web (WAF) de código abierto que inspecciona cada petición HTTP antes de que llegue a tu aplicación. Por sí solo no bloquea nada: la protección la aporta el OWASP Core Rule Set (CRS), un conjunto de reglas que detecta inyección SQL, XSS, inclusión de archivos, ejecución de comandos y escáneres conocidos. En este tutorial instalarás ModSecurity y el CRS desde los repositorios de Ubuntu 24.04, primero en Apache y después en Nginx, empezarás en modo solo detección, activarás el bloqueo y aprenderás a ajustar los falsos positivos.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
- Un usuario no root con privilegios
sudo. - Apache o Nginx instalado o por instalar. Sigue la sección de tu servidor web: los pasos 1 a 5 son para Apache y el paso 6 para Nginx. Los pasos 7 y 8 sirven para ambos.
- El puerto 80 (y el 443 si usas HTTPS) abierto en UFW:
sudo ufw allow 'Apache Full'osudo ufw allow 'Nginx Full'.
Los paquetes que vas a usar son estos:
| Paquete | Versión en Ubuntu 24.04 | Uso |
|---|---|---|
libapache2-mod-security2 | ModSecurity 2.9.7 | Módulo para Apache |
libnginx-mod-http-modsecurity | Conector 1.0.3 con libmodsecurity 3.0.12 | Módulo dinámico para Nginx |
modsecurity-crs | OWASP CRS 3.3.5 | Reglas, compartidas por ambos |
Cómo funciona el CRS
El CRS usa un modelo de puntuación de anomalías. Cada regla que coincide suma puntos a la petición (5 por una coincidencia crítica, 4 por una de error, 3 por un aviso). Al final de la fase de petición, la regla 949110 compara el total con un umbral, 5 por defecto, y si lo alcanza bloquea la petición con un 403 Forbidden. Así una sola regla crítica basta para bloquear, pero coincidencias menores aisladas no.
El nivel de paranoia (1 a 4) decide cuántas reglas se activan. El nivel 1, el predeterminado, tiene pocos falsos positivos y es el adecuado para empezar.
Paso 1: Instalar ModSecurity en Apache
Instala Apache, el módulo y el CRS:
sudo apt update
sudo apt install apache2 libapache2-mod-security2 modsecurity-crs
El paquete activa el módulo security2 automáticamente. Compruébalo:
sudo apache2ctl -M | grep security2
security2_module (shared)
El archivo /etc/apache2/mods-enabled/security2.conf carga todos los *.conf de /etc/modsecurity/ y después el CRS desde /usr/share/modsecurity-crs/owasp-crs.load. Por tanto, lo que coloques en /etc/modsecurity/ se aplica antes que las reglas del CRS.
Paso 2: Activar la configuración en modo detección
El paquete incluye una configuración recomendada que hay que copiar para activarla:
sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
Ábrela y revisa la directiva principal:
sudo nano /etc/modsecurity/modsecurity.conf
SecRuleEngine DetectionOnly
Déjala de momento en DetectionOnly: ModSecurity evaluará todas las reglas y registrará lo que habría bloqueado, pero no bloqueará nada. Es la forma segura de empezar con una aplicación que ya está en producción. En el mismo archivo verás que el registro de auditoría se guarda en /var/log/apache2/modsec_audit.log.
Comprueba la sintaxis y recarga Apache:
sudo apache2ctl configtest
sudo systemctl reload apache2
Syntax OK
Paso 3: Comprobar la detección
Envía una petición que intenta leer /etc/passwd, un patrón típico de inclusión de archivos locales:
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost/?file=/etc/passwd"
200
La petición pasa porque estás en modo detección, pero ModSecurity la ha registrado en el log de errores de Apache:
sudo grep ModSecurity /var/log/apache2/error.log | tail -n 3
[security2:error] [client 127.0.0.1] ModSecurity: Warning. Matched phrase "etc/passwd" at ARGS:file. [file "/usr/share/modsecurity-crs/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [id "930120"] [msg "OS File Access Attempt"] ...
[security2:error] [client 127.0.0.1] ModSecurity: Warning. Operator GE matched 5 at TX:anomaly_score. [file "/usr/share/modsecurity-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 10)"] ...
Fíjate en el id de cada regla: es lo que usarás para ajustar falsos positivos. Deja el sitio en modo detección un tiempo (unos días con tráfico real) y revisa qué reglas saltan con peticiones legítimas antes de pasar al paso 4.
Este comando resume qué reglas se han disparado más veces:
sudo grep -o '\[id "[0-9]*"\]' /var/log/apache2/error.log | sort | uniq -c | sort -rn | head
Paso 4: Activar el bloqueo
Cuando hayas ajustado los falsos positivos (paso 7), cambia la directiva en /etc/modsecurity/modsecurity.conf:
SecRuleEngine On
Recarga Apache y repite la prueba:
sudo apache2ctl configtest
sudo systemctl reload apache2
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost/?file=/etc/passwd"
403
Prueba también un intento de inyección SQL y el agente de usuario de un escáner conocido:
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost/?id=1%27%20OR%20%271%27=%271"
curl -s -o /dev/null -w "%{http_code}\n" -A "Nikto" http://localhost/
403
403
Y confirma que una petición normal sigue funcionando:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost/
200
Paso 5: Revisar el registro de auditoría
El registro de auditoría guarda la petición completa (cabeceras y cuerpo) de cada transacción relevante, útil para investigar un bloqueo:
sudo tail -n 40 /var/log/apache2/modsec_audit.log
Cada entrada está dividida en secciones marcadas como --xxxxxxxx-A--, -B-- (cabeceras de la petición), -C-- (cuerpo), -F-- (cabeceras de la respuesta) y -H-- (reglas que coincidieron). Ubuntu rota este archivo junto con el resto de /var/log/apache2/*.log mediante logrotate.
Notael registro de auditoría puede contener datos sensibles enviados por los usuarios, como contraseñas en formularios. Restringe el acceso al archivo y no lo compartas sin revisarlo.
Paso 6: Instalar ModSecurity en Nginx
Si usas Nginx en lugar de Apache, instala el módulo dinámico y el CRS:
sudo apt update
sudo apt install nginx libnginx-mod-http-modsecurity modsecurity-crs
El paquete carga el módulo mediante /etc/nginx/modules-enabled/50-mod-http-modsecurity.conf e instala dos archivos de configuración: /etc/nginx/modsecurity.conf (equivalente al modsecurity.conf de Apache, en DetectionOnly y con el registro de auditoría en /var/log/nginx/modsec_audit.log) y /etc/nginx/modsecurity_includes.conf, que indica qué reglas cargar.
libmodsecurity 3 no admite la directiva IncludeOptional que usa el cargador del CRS, así que incluye los archivos del CRS directamente. Edita el archivo de includes:
sudo nano /etc/nginx/modsecurity_includes.conf
Sustituye su contenido por este:
include /etc/nginx/modsecurity.conf
include /etc/modsecurity/crs/crs-setup.conf
include /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
include /usr/share/modsecurity-crs/rules/*.conf
include /etc/modsecurity/crs/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
Activa ModSecurity en el bloque server de tu sitio, por ejemplo el predeterminado:
sudo nano /etc/nginx/sites-available/default
Añade estas dos líneas dentro del bloque server { ... }, antes de los bloques location:
modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity_includes.conf;
Comprueba la configuración y recarga Nginx:
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Repite las pruebas del paso 3. En modo detección, las coincidencias aparecen en /var/log/nginx/error.log con el texto ModSecurity: Warning:
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost/?file=/etc/passwd"
sudo grep ModSecurity /var/log/nginx/error.log | tail -n 2
Para activar el bloqueo, cambia SecRuleEngine DetectionOnly por SecRuleEngine On en /etc/nginx/modsecurity.conf, ejecuta sudo nginx -t y recarga Nginx. La misma prueba debe devolver 403.
Paso 7: Ajustar falsos positivos
Es normal que algunas reglas salten con peticiones legítimas: un CMS que acepta HTML en un campo, una API que recibe JSON con comillas, un webhook con un cuerpo inusual. La solución nunca es desactivar ModSecurity, sino crear una exclusión lo más precisa posible.
Las exclusiones que deben aplicarse antes de evaluar las reglas van en /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf, que tanto Apache como Nginx cargan antes del CRS:
sudo nano /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
Ejemplo 1: la regla 942100 (inyección SQL detectada por libinjection) salta en el parámetro comment del formulario de /contacto. Excluye solo ese parámetro de esa regla en esa ruta:
SecRule REQUEST_URI "@beginsWith /contacto" \
"id:1000,\
phase:1,\
pass,\
nolog,\
ctl:ruleRemoveTargetById=942100;ARGS:comment"
Ejemplo 2: un webhook en /api/webhook recibe cuerpos que disparan varias reglas de protocolo. Desactiva solo esas reglas para esa ruta:
SecRule REQUEST_URI "@beginsWith /api/webhook" \
"id:1001,\
phase:1,\
pass,\
nolog,\
ctl:ruleRemoveById=920170,\
ctl:ruleRemoveById=920180"
Cada regla necesita un id único. Usa el rango 1 a 99999, reservado para reglas locales. Recarga el servidor web después de cada cambio (sudo systemctl reload apache2 o sudo systemctl reload nginx) y repite la petición que fallaba.
Consejoel CRS incluye exclusiones ya preparadas para WordPress, Drupal, Nextcloud y otras aplicaciones. Se activan en
/etc/modsecurity/crs/crs-setup.confdescomentando la regla 900130 y poniendo a 1 la variable de tu aplicación, por ejemplotx.crs_exclusions_wordpress=1.
Paso 8: Ajustar el nivel de paranoia y los umbrales
Los ajustes globales del CRS están en /etc/modsecurity/crs/crs-setup.conf, compartido por Apache y Nginx:
sudo nano /etc/modsecurity/crs/crs-setup.conf
Para subir el nivel de paranoia, busca la regla con id:900000, descoméntala y cambia el valor:
SecAction \
"id:900000,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.paranoia_level=2"
Súbelo solo si tu aplicación lo necesita y después de volver a pasar por el modo detección: cada nivel añade reglas y falsos positivos.
Los umbrales de anomalía se controlan con la regla id:900110. Durante la puesta en marcha puedes usar un umbral alto para bloquear solo los ataques más evidentes e ir bajándolo hasta el valor por defecto (5) a medida que ajustas exclusiones:
SecAction \
"id:900110,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.inbound_anomaly_score_threshold=10,\
setvar:tx.outbound_anomaly_score_threshold=4"
Recarga el servidor web y repite las pruebas del paso 4 para comprobar que los ataques siguen bloqueados.
Solución de problemas
- Una petición legítima devuelve 403: busca en el log de errores la línea de la regla 949110 de esa petición y las reglas anteriores con la misma IP y hora. Anota sus
idy crea una exclusión como en el paso 7. - Apache no arranca tras un cambio: ejecuta
sudo apache2ctl configtest. Unidduplicado o una barra invertida de continuación seguida de espacios son los errores más comunes. - Nginx falla con
IncludeOptional: estás cargandoowasp-crs.loaddesde Nginx. Usa losincludeexplícitos del paso 6. - Subidas de archivos grandes rechazadas con 413:
SecRequestBodyLimitenmodsecurity.conflimita el tamaño del cuerpo. Súbelo solo lo necesario para tu aplicación. - El registro de auditoría crece muy rápido: comprueba que
SecAuditEngineestá enRelevantOnlyy no enOn.
Conclusión
Has instalado ModSecurity con el OWASP Core Rule Set en Apache o Nginx, has comprobado la detección, has pasado a modo bloqueo y has visto cómo ajustar falsos positivos con exclusiones precisas y cómo cambiar el nivel de paranoia. Como siguientes pasos, puedes activar HTTPS con Let's Encrypt en el mismo servidor, combinar el WAF con Fail2ban para bloquear en el cortafuegos las IP que acumulan bloqueos y revisar periódicamente el registro de auditoría para detectar nuevos patrones de ataque.
