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

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



