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' o sudo ufw allow 'Nginx Full'.

Los paquetes que vas a usar son estos:

PaqueteVersión en Ubuntu 24.04Uso
libapache2-mod-security2ModSecurity 2.9.7Módulo para Apache
libnginx-mod-http-modsecurityConector 1.0.3 con libmodsecurity 3.0.12Módulo dinámico para Nginx
modsecurity-crsOWASP CRS 3.3.5Reglas, 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.

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.

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 id y crea una exclusión como en el paso 7.
  • Apache no arranca tras un cambio: ejecuta sudo apache2ctl configtest. Un id duplicado o una barra invertida de continuación seguida de espacios son los errores más comunes.
  • Nginx falla con IncludeOptional: estás cargando owasp-crs.load desde Nginx. Usa los include explícitos del paso 6.
  • Subidas de archivos grandes rechazadas con 413: SecRequestBodyLimit en modsecurity.conf limita 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 SecAuditEngine está en RelevantOnly y no en On.

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.