Agregar WAF (ModSecurity + OWASP CRS) en Apache

por | Feb 26, 2026 | Defensivo, Hardening, Security

Implementación de Web Application Firewall (WAF) en un servidor Apache utilizando:

  • ModSecurity.
  • OWASP Core Rule Set (CRS).

Cuando exponemos un servidor web a Internet, automáticamente se convierte en objetivo de:

  • SQL Injection.
  • XSS (Cross-Site Scripting).
  • Escaneos masivos.
  • Bots automatizados.
  • Fuerza bruta.
  • Remote Code Execution.

Aunque nuestra aplicación esté bien desarrollada, Apache por sí solo no analiza la intención del tráfico HTTP.

Ahí es donde entra un WAF. Haremos uso de ModSecurity, uno de los WAF open-source más utilizados.

¿Qué es un WAF y cómo funciona?

Un Web Application Firewall (WAF) es una solución de seguridad que inspecciona y filtra el tráfico HTTP/HTTPS entre el cliente y la aplicación web.

ModSecurity no es un firewall tradicional de red (como iptables o un firewall perimetral). Es un módulo de Apache que intercepta el tráfico HTTP en distintas fases del procesamiento de una request.

Apache procesa una solicitud en varias etapas:

  • Request headers.
  • Request body.
  • Fase de autorización.
  • Respuesta.
  • Logging.

ModSecurity puede inspeccionar cada una de esas fases.

Si una regla coincide con un patrón malicioso:

  • Puede bloquear la request (403).
  • Puede registrar el evento.
  • Puede modificar la respuesta.

Es básicamente un motor de inspección profunda de HTTP.

Instalar Apache

Bash
sudo apt install apache2 -y

Configurar un Login en Apache

Nos dirigimos al siguiente directorio:

Bash
cd /var/www/html

Luego borramos el index.html que nos aparece en el directorio y creamos uno:

Bash
rm -rf index.html && touch index.html

Abrimos el editor para colocar el código:

Bash
sudo nano index.html

HTML
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Login Seguro (Simulado)</title>
  <style>
    * {
      box-sizing: border-box;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    }

    body {
      margin: 0;
      height: 100vh;
      background: linear-gradient(-45deg, #1e3c72, #2a5298, #0f2027, #203a43);
      background-size: 400% 400%;
      animation: gradientBG 15s ease infinite;
      display: flex;
      justify-content: center;
      align-items: center;
      color: #fff;
    }

    @keyframes gradientBG {
      0% { background-position: 0% 50%; }
      50% { background-position: 100% 50%; }
      100% { background-position: 0% 50%; }
    }

    .container {
      background: rgba(255, 255, 255, 0.05);
      padding: 40px;
      border-radius: 15px;
      box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
      backdrop-filter: blur(12px);
      width: 100%;
      max-width: 400px;
    }

    h1 {
      text-align: center;
      margin-bottom: 30px;
      font-size: 28px;
      color: #f1c40f;
    }

    label {
      display: block;
      margin-top: 10px;
      margin-bottom: 5px;
      font-weight: 600;
    }

    input[type="text"],
    input[type="password"] {
      width: 100%;
      padding: 12px;
      border: none;
      border-radius: 8px;
      background-color: #ffffff15;
      color: #fff;
      font-size: 16px;
      margin-bottom: 15px;
      transition: background-color 0.3s ease;
    }

    input::placeholder {
      color: #ccc;
    }

    input:focus {
      outline: none;
      background-color: #ffffff25;
    }

    button {
      width: 100%;
      padding: 12px;
      background-color: #f1c40f;
      border: none;
      border-radius: 8px;
      font-size: 16px;
      font-weight: bold;
      color: #000;
      cursor: pointer;
      transition: background-color 0.3s ease;
    }

    button:hover {
      background-color: #e0b90a;
    }

    .message {
      margin-top: 20px;
      padding: 15px;
      border-radius: 10px;
      text-align: center;
      font-weight: bold;
      font-size: 16px;
    }

    .success {
      background-color: #27ae60cc;
      color: #ecf0f1;
    }

    .error {
      background-color: #e74c3ccc;
      color: #fff;
    }

    .note {
      text-align: center;
      margin-top: 30px;
      font-size: 0.85em;
      color: #ccc;
    }

    code {
      background-color: rgba(255,255,255,0.1);
      padding: 2px 6px;
      border-radius: 5px;
      color: #f1c40f;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>Acceso al Sistema</h1>
    <form method="GET" action="">
      <label for="user">Usuario:</label>
      <input type="text" id="user" name="user" placeholder="admin" required />

      <label for="pass">Contraseña:</label>
      <input type="password" id="pass" name="pass" placeholder="123123" required />

      <button type="submit">Iniciar sesión</button>
    </form>

    <div id="mensaje"></div>

    <div class="note">
      <p><strong>⚠️ Este ejemplo es educativo.</strong></p>
      <p>Los datos se reflejan en la URL. No es seguro para producción.</p>
    </div>
  </div>

  <script>
    const params = new URLSearchParams(window.location.search);
    const user = params.get('user');
    const pass = params.get('pass');
    const mensaje = document.getElementById('mensaje');

    if (user && pass) {
      if (user === "admin" && pass === "123123") {
        mensaje.innerHTML = `<div class="message success">✅ Bienvenido, <strong>${user}</strong>. Has iniciado sesión correctamente.</div>`;
      } else {
        mensaje.innerHTML = `<div class="message error">❌ Usuario o contraseña incorrectos.</div>`;
      }
    }
  </script>
</body>

</html>

Si abrimos ahora nuestra IP del servidor en el navegador veremos el Login:

Instalar y configurar ModSecurity

Probamos un payload clásico de SQL Injection en el login y el sistema respondió con (usuario o contraseña incorrectos)

Esto indica que la consulta no fue alterada y que el código no parece vulnerable a una inyección básica.

Aun así, implementar un WAF sigue siendo una buena práctica, ya que proporciona una capa adicional de defensa y permite detectar y registrar intentos de explotación.

Instalamos ModSecurity:

Bash
sudo apt install libapache2-mod-security2 -y

¿Qué ocurre internamente?

  • Se descarga el paquete desde repositorios Debian/Ubuntu.
  • Se instala el módulo dinámico security2.load.
  • Se crean archivos de configuración en:
/etc/modsecurity/
/etc/apache2/mods-available/

El módulo queda disponible, pero todavía no necesariamente activo.

Luego activamos el modulo en Apache:

Bash
sudo a2enmod security2

¿Qué hace realmente a2enmod?

Crea un enlace simbólico desde:

/etc/apache2/mods-available/security2.load

Hacia:

/etc/apache2/mods-enabled/

Apache solo carga módulos que estén en mods-enabled.

Es decir: ahora Apache cargará ModSecurity al iniciar.

Reiniciar Apache

Bash
sudo systemctl restart apache2

Configuración base de ModSecurity

Al instalarlo, ModSecurity viene en modo seguro (no bloquea nada).

Editamos:

Bash
sudo nano /etc/modsecurity/modsecurity.conf-recommended

Por defecto lo tendriamos asi:

Aquí solo está detectando pero no bloqueando, para que detecte y bloquee cambiamos el (SecRuleEngine DetectionOnly) por:

SecRuleEngine On

Ahora:

  • Analiza.
  • Evalúa reglas.
  • Bloquea tráfico malicioso.

Instalar OWASP Core Rule Set

Hasta ahora tenemos un motor…

Pero sin reglas es inútil.

Ahí entra el OWASP CRS.

Es un conjunto de reglas mantenidas por expertos en seguridad que detectan:

  • SQL Injection.
  • XSS.
  • RFI.
  • Command Injection.
  • Bots.
  • Escaneos automatizados.

Clonamos el repositorio oficial:

Bash
cd /etc/modsecurity
sudo git clone https://github.com/coreruleset/coreruleset.git

Esto descarga:

  • crs-setup.conf.example.
  • Carpeta rules/ con cientos de reglas.

Activamos la configuración principal:

Bash
cd coreruleset
sudo cp crs-setup.conf.example crs-setup.conf

Ese archivo define:

  • Configuración general.
  • Ajustes globales.

Integramos CRS en Apache, para eso editamos:

Bash
sudo nano /etc/apache2/mods-enabled/security2.conf

Y agregamos:

IncludeOptional /etc/modsecurity/coreruleset/crs-setup.conf
IncludeOptional /etc/modsecurity/coreruleset/rules/*.conf

¿Qué pasa ahora?

Cuando Apache inicia:

  1. Carga ModSecurity.
  2. Carga su configuración.
  3. Carga el CRS.
  4. Compila todas las reglas en memoria.

Activamos explícitamente ModSecurity en el VirtualHost.

Editamos:

Bash
sudo nano /etc/apache2/sites-available/000-default.conf

Dentro del bloque <VirtualHost *:80> agregamos:

Apache
<IfModule security2_module>
    SecRuleEngine On
</IfModule>

<IfModule security2_module>

Verifica que el módulo ModSecurity esté cargado antes de aplicar la directiva.
Evita errores si el módulo no está disponible.

SecRuleEngine On

Fuerza la activación del motor de reglas dentro de este VirtualHost específico.

¿Qué pasa cuando llega una request?

Supongamos que alguien intenta un SQL Injection.

Apache recibe la request.

Antes de pasarla a PHP:

  • ModSecurity intercepta la request.
  • Evalúa reglas de SQL Injection.
  • Detecta patrón ' OR '1'='1.
  • Bloquea la petición.
  • Devuelve 403.

Todo ocurre antes de que el código de la aplicación se ejecute.

Personalizar (Error 403)

Podemos personalizar a nuestro gusto el redireccionamiento del Error 403, para eso volvemos a:

Bash
cd /var/www/html

Y creamos el archivo:

Bash
sudo nano 403.html

Dentro del archivo colocamos este código de ejemplo:

HTML
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Error 403 - Acceso Prohibido</title>
  <style>
    body {
      margin: 0;
      padding: 0;
      background: linear-gradient(145deg, #1f1f1f, #121212);
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      color: #ffffff;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      height: 100vh;
      overflow: hidden;
      text-align: center;
    }

    .error-container {
      animation: fadeIn 1.2s ease-in-out;
    }

    h1 {
      font-size: 10rem;
      margin: 0;
      color: #ff4c4c;
      text-shadow: 2px 2px 20px rgba(255, 76, 76, 0.4);
    }

    h2 {
      font-size: 2rem;
      margin: 0.5em 0;
      color: #f0db4f;
    }

    p {
      font-size: 1.2rem;
      max-width: 500px;
      margin: 1em auto;
      color: #cccccc;
    }

    .btn {
      display: inline-block;
      margin-top: 20px;
      padding: 12px 24px;
      background: #f0db4f;
      color: #000;
      text-decoration: none;
      font-weight: bold;
      border-radius: 8px;
      transition: all 0.3s ease;
      box-shadow: 0 4px 15px rgba(240, 219, 79, 0.4);
    }

    .btn:hover {
      background: #ffe84f;
      transform: translateY(-3px);
    }

    @keyframes fadeIn {
      from {
        opacity: 0;
        transform: translateY(-30px);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }

    .icon-lock {
      font-size: 5rem;
      margin-bottom: 20px;
      color: #ff4c4c;
    }

  </style>
</head>
<body>
  <div class="error-container">
    <div class="icon-lock">🔒</div>
    <h1>403</h1>
    <h2>Acceso Prohibido</h2>
    <p>No tienes permiso para acceder a este recurso. Si crees que esto es un error, contacta con el administrador del sitio.</p>
    <a href="/" class="btn">Volver al Inicio</a>
  </div>
</body>
</html>

Luego lo enlazamos en:

Bash
sudo nano /etc/apache2/sites-available/000-default.conf

Colocamos:

Apache
ErrorDocument 403 /403.html

Visualización final

Aquí podremos ver como queda finalmente el WAF configurado con sus reglas y con sus estilos agregados al redireccionamiento del Error 403:

Conclusión

Implementar ModSecurity + OWASP CRS transforma Apache en algo más que un simple servidor web.

Lo convierte en:

  • Un filtro inteligente.
  • Un sistema de detección de ataques.
  • Una primera línea real de defensa.

No reemplaza buenas prácticas.

Pero reduce drásticamente la superficie de ataque.

GabrielGR

Categorías

Etiquetas

Te pueden interesar

Te pueden interesar

Categorías

Etiquetas