Skip to main content

Command Palette

Search for a command to run...

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

De la query al incidente: detección y respuesta automatizada con ProxySQL, Wazuh y n8n

Updated
5 min read
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 para que esté en el medio entre el cliente y la base de datos y generar reglas para permitir o no comandos.

Arquitectura

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.

# 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

<!-- 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.

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

Dejo el proyecto completo en este repositorio.

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:

  <!-- ============================================ -->
  <!-- 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.

Aca la alarma, en Wazuh.

Vemos que corrió la integración.

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

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.

Aca el email, que genere.

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

Espero que les sirva.

Comandos Utiles

# 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