# Cuando el SQL se pone peligroso: automatizando defensa con ProxySQL y Wazuh

### Introducción

En el blog hemos implementado de todo, pero nunca un **Monitoreo de Actividad de Base de Datos** o un **Firewall de Base de Datos**, que utilizaremos para vigilar y registrar cada acción realizada en un sistema de bases de datos. Esto garantiza el acceso correcto a los datos, detecta actividades no autorizadas o sospechosas, y protege la información confidencial.

Imaginemos un escenario donde hemos sido vulnerados o, en su defecto, un empleado descontento que quiere ejecutar un **DROP**. Vamos a implementar [ProxySQL](https://proxysql.com/) para que esté en el medio entre el cliente y la base de datos y generar reglas para permitir o no comandos.

### Arquitectura

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761059216187/a328a06d-4cfc-437b-8409-b7b4a81a5a9a.png align="center")

**ProxySQL**

Es un **proxy inteligente para bases de datos MySQL/MariaDB**.

En pocas líneas 👇

* Se ubica **entre la aplicación y el servidor MySQL**.
    
* Permite **filtrar, enrutar y balancear queries** (por ejemplo, leer en réplicas y escribir en el master).
    
* Puede **bloquear o reescribir consultas peligrosas** (`DROP`, `TRUNCATE`, etc.).
    
* Mejora el rendimiento con **cacheo de queries** y **pool de conexiones**.
    
* Facilita la **observabilidad y control de tráfico SQL** sin modificar la app.
    

👉 **ProxySQL no puede modificar los datos devueltos por MySQL (como hashearlos u ofuscarlos)**, porque **trabaja a nivel de capa de conexión y enrutamiento**, no de contenido.  
Su función principal es decidir *a dónde va una query*, si se permite o no, o si se reescribe **antes** de ejecutarse — pero **no procesa ni altera los resultados que vienen del servidor**. Ahi tenemos que aplicar Hasheo u ofuscación en el propio **MySQL** o consumir a traves de API que realice ese trabajo.

Vamos a lo nuestro. Todo estará en el repositorio, pero echemos un vistazo a `config/proxysql/proxysql.cnf`.

```bash
# config/proxysql/proxysql.cnf
datadir="/var/lib/proxysql"

admin_variables=
{
    admin_credentials="admin:admin"
    mysql_ifaces="0.0.0.0:6032"
    refresh_interval=2000
}

mysql_variables=
{
    threads=4
    max_connections=2048
    default_query_delay=0
    default_query_timeout=36000000
    have_compress=true
    poll_timeout=2000
    interfaces="0.0.0.0:6033"
    default_schema="information_schema"
    stacksize=1048576
    server_version="8.0.0"
    connect_timeout_server=3000
    monitor_username="monitor"
    monitor_password="monitor"
    monitor_history=600000
    monitor_connect_interval=60000
    monitor_ping_interval=10000
    monitor_read_only_interval=1500
    monitor_read_only_timeout=500
    ping_interval_server_msec=120000
    ping_timeout_server=500
    commands_stats=true
    sessions_sort=true
    connect_retries_on_failure=10
    
    # LOGGING
    eventslog_filename="/var/log/proxysql/queries.log"
    eventslog_default_log=1
    eventslog_format=2
}

# Backend MySQL servers
mysql_servers =
(
    {
        address="mysql-db"
        port=3306
        hostgroup=0
        max_connections=200
    }
)

# Users
mysql_users =
(
    {
        username = "app_user"
        password = "app_password"
        default_hostgroup = 0
        max_connections = 200
        active = 1
    }
)

# Query Rules - AQU BLOQUEAMOS COMANDOS PELIGROSOS
mysql_query_rules =
(
    # BLOQUEAR DROP
    {
        rule_id=1
        active=1
        match_pattern="(?i)^\\s*DROP\\s+(TABLE|DATABASE|SCHEMA)"
        error_msg="ERROR: DROP statements estan bloqueados por politica de seguridad"
        log=1
        apply=1
    },
    # BLOQUEAR TRUNCATE
    {
        rule_id=2
        active=1
        match_pattern="(?i)^\\s*TRUNCATE\\s+TABLE"
        error_msg="ERROR: TRUNCATE TABLE esta bloqueado por politica de seguridad"
        log=1
        apply=1
    },
    # BLOQUEAR DELETE SIN WHERE
    {
        rule_id=3
        active=1
        match_pattern="(?i)^\\s*DELETE\\s+FROM\\s+[a-zA-Z0-9_]+\\s*;?\\s*$"
        error_msg="ERROR: DELETE sin WHERE no esta permitido"
        log=1
        apply=1
    },
    # BLOQUEAR UPDATE SIN WHERE
    {
        rule_id=4
        active=1
        match_pattern="(?i)^\\s*UPDATE\\s+[a-zA-Z0-9_]+\\s+SET\\s+(?!.*WHERE).*$"
        error_msg="ERROR: UPDATE sin WHERE no esta permitido"
        log=1
        apply=1
    },
    # BLOQUEAR ALTER TABLE
    {
        rule_id=5
        active=1
        match_pattern="(?i)^\\s*ALTER\\s+TABLE"
        error_msg="ERROR: ALTER TABLE esta bloqueado - contacte al DBA"
        log=1
        apply=1
    },
    # BLOQUEAR GRANT/REVOKE
    {
        rule_id=6
        active=1
        match_pattern="(?i)^\\s*(GRANT|REVOKE)"
        error_msg="ERROR: Cambios de permisos no permitidos"
        log=1
        apply=1
    },
    # PERMITIR TODO LO DEMAS
    {
        rule_id=100
        active=1
        match_pattern=".*"
        destination_hostgroup=0
        log=1
        apply=1
    }
)
```

Ahora las reglas de Wazuh en `config/wazuh_cluster/proxysql-rules.xml`

```xml
<!-- config/wazuh_cluster/proxysql-rules.xml -->
<group name="proxysql,database,firewall,">

  <!-- Regla base - Query event de ProxySQL -->
  <rule id="100500" level="0">
    <field name="event">COM_QUERY</field>
    <description>ProxySQL query event</description>
  </rule>

  <!-- Query BLOQUEADA (hostgroup_id = -1) -->
  <rule id="100501" level="8">
    <if_sid>100500</if_sid>
    <field name="hostgroup_id">-1</field>
    <description>ProxySQL: Query bloqueada por firewall</description>
    <group>database_security,firewall_block,</group>
  </rule>

  <!-- DROP bloqueado -->
  <rule id="100502" level="12">
    <if_sid>100501</if_sid>
    <match>DROP TABLE</match>
    <description>ProxySQL: DROP TABLE bloqueado</description>
    <group>database_attack,pci_dss_10.2.5,</group>
    <mitre>
      <id>T1485</id>
    </mitre>
  </rule>

  <!-- TRUNCATE bloqueado -->
  <rule id="100503" level="12">
    <if_sid>100501</if_sid>
    <match>TRUNCATE TABLE</match>
    <description>ProxySQL: TRUNCATE TABLE bloqueado</description>
    <group>database_attack,data_loss,</group>
  </rule>

  <!-- DELETE bloqueado -->
  <rule id="100504" level="10">
    <if_sid>100501</if_sid>
    <match>DELETE FROM</match>
    <description>ProxySQL: DELETE bloqueado</description>
    <group>database_security,</group>
  </rule>

  <!-- UPDATE bloqueado -->
  <rule id="100505" level="10">
    <if_sid>100501</if_sid>
    <match>UPDATE</match>
    <description>ProxySQL: UPDATE bloqueado</description>
    <group>database_security,</group>
  </rule>

  <!-- ALTER bloqueado -->
  <rule id="100506" level="10">
    <if_sid>100501</if_sid>
    <match>ALTER TABLE</match>
    <description>ProxySQL: ALTER TABLE bloqueado</description>
    <group>database_security,ddl,</group>
  </rule>

  <!-- Query PERMITIDA (hostgroup_id >= 0) -->
  <rule id="100510" level="2">
    <if_sid>100500</if_sid>
    <field name="hostgroup_id">0</field>
    <description>ProxySQL: Query ejecutada exitosamente</description>
    <group>database_access,</group>
  </rule>

  <!-- SELECT permitido -->
  <rule id="100511" level="2">
    <if_sid>100510</if_sid>
    <match>SELECT</match>
    <description>ProxySQL: SELECT ejecutado</description>
    <group>database_access,query,</group>
  </rule>

  <!-- INSERT permitido -->
  <rule id="100512" level="4">
    <if_sid>100510</if_sid>
    <match>INSERT</match>
    <description>ProxySQL: INSERT ejecutado</description>
    <group>database_access,data_modification,</group>
  </rule>

  <!-- Múltiples bloqueos -->
  <rule id="100520" level="14" frequency="5" timeframe="120">
    <if_matched_sid>100502</if_matched_sid>
    <description>ProxySQL: Múltiples comandos DROP bloqueados</description>
    <group>database_attack,attacks,</group>
  </rule>

</group>
```

Este es mi tree de directorio, con todo lo necesario.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761087703168/d6f537b9-a8c0-4a6d-bb1e-8fd7eb62b26e.png align="center")

> Si tenes dudas de correr Wazuh en Docker, podes revisar post anterior o en referencias.

Dejo el proyecto completo en este [repositorio](https://github.com/safernandez666/ProxySQLAndWazuh).

He añadido N8N para gestionar esa alerta. Las opciones son infinitas; en mi caso, enviaré un correo, pero también podríamos bloquear, notificar por Slack y, por qué no, crear un caso en The Hive.

En `ossec.conf` añadí esto, justo debajo de donde le indico cómo leer los logs de ProxySQL:

```bash
  <!-- ============================================ -->
  <!-- LEER LOGS DE PROXYSQL DIRECTAMENTE             -->
  <!-- ============================================ -->
  <!-- Leer logs de ProxySQL -->
  <localfile>
    <log_format>json</log_format>
    <location>/var/log/proxysql/queries.log</location>
  </localfile>

  <!-- ============================================ -->
  <!-- INTEGRACIÓN CON N8N                          -->
  <!-- ============================================ -->
  <integration>
    <name>custom-n8n</name>
    <hook_url>http://n8n:5678/webhook/wazuh-proxysql</hook_url>
    <level>8</level>
    <rule_id>100502,100503,100504,100505,100506,100507,100508</rule_id>
    <alert_format>json</alert_format>
  </integration>
```

### Prueba

Vamos a ejecutar un comando malicioso para ver cómo reacciona.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761095176257/9a4ccd43-6fbb-49fe-83ed-0346ed9accca.png align="center")

Aca la alarma, en **Wazuh**.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761097988347/cf00b2f0-1618-4244-a9ac-d8bb0be18b7b.png align="center")

Vemos que corrió la integración.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761097692913/783bfad7-50a9-4135-a1ea-481053575a49.png align="center")

Con esta evidencia, el Workflow comenzó a correr. Mientras tanto en **Wazuh** podemos ver la alarma.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761131997279/48ee3539-7f73-4428-bb35-9b3374740d28.png align="center")

### Workflow N8N

Este es sencillo, pero hay miles de opciones! Te recuerdo que en el repositorio tenes la carpeta workflow donde esta el *ndjson* para importar a tu ambiente.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761086392860/b9ffdec3-105f-4e46-86d4-2465b0d74b8b.png align="center")

Aca el email, que genere.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1761086355881/808160fc-99a9-4661-b889-8f12d511c466.png align="center")

Excelente señores ya podemos darle acceso al administrador, a través del proxy, para nuestra seguridad.

Espero que les sirva.

### Comandos Utiles

```bash
# Ver archives
docker exec single-node-wazuh.manager-1 tail -50 /var/ossec/logs/archives/archives.log | tail -10

# Ver alerts (ahora deberían aparecer!)
docker exec single-node-wazuh.manager-1 tail -100 /var/ossec/logs/alerts/alerts.log | grep -i proxysql

# Ver en formato JSON
docker exec single-node-wazuh.manager-1 tail -50 /var/ossec/logs/alerts/alerts.json | grep -i proxysql

# Ver si hay errores de decoder
docker logs single-node-wazuh.manager-1 2>&1 | grep -i "mysql-decoder\|decoder.*mysql"

# Comando Truncate Bloqueado
docker run --rm --network single-node_poc-network mysql:8.0 \
  mysql -h proxysql -P 6033 -u app_user -papp_password produccion \
  -e "TRUNCATE TABLE auditoria;" 2>&1

# Comando Select Habilitado
docker run --rm --network single-node_poc-network mysql:8.0 \
  mysql -h proxysql -P 6033 -u app_user -papp_password produccion \
  -e "SELECT * FROM usuarios LIMIT 3;" 2>/dev/null
```
