Mi base de datos ahora se defiende sola

Un sistema que detecta y bloquea ataques de hackers dentro de SQL Server, sin herramientas externas

Defensa nativa contra fuerza bruta en SQL Server — un Auto-Blocker 100% T-SQL
Seguridad · SQL Server

SQL Server con su propio sistema inmunológico

Cómo detectar y bloquear ataques de fuerza bruta dentro del motor, en T-SQL puro — sin firewall, sin PowerShell, sin xp_cmdshell.

Casi todo SQL Server expuesto a internet recibe ataques de fuerza bruta contra la cuenta sa. La defensa estándar vive fuera del motor: firewall, VPN, IPSec o herramientas externas. El problema es que esa capa externa puede fallar — una regla mal configurada, un puerto que queda abierto, una migración — y en ese momento el SQL Server queda completamente desnudo. Este artículo le da al motor su propia defensa interna.

01 / EL PROBLEMAEl log que se repite todos los días

Si un SQL Server tiene autenticación SQL habilitada y el puerto 1433 accesible desde internet, su error log se ve así, todos los días:

SQL Server ERRORLOG
Login failed for user 'sa'.    Reason: Password did not match... [CLIENT: 186.170.16.92]
Login failed for user 'sa'.    Reason: Password did not match... [CLIENT: 186.170.16.92]
Login failed for user 'mssqla'. Reason: Could not find a login... [CLIENT: 186.170.16.92]
Login failed for user 'sa'.    Reason: Password did not match... [CLIENT: 186.170.16.92]
...

En un caso real de producción, una sola IP atacante generó más de 60 intentos en 50 segundos — alrededor de 1.3 intentos por segundo. Eso no es un humano: es un script automatizado que recorre internet buscando puertos 1433 abiertos y dispara diccionarios de contraseñas contra sa.

La recomendación universal — y correcta — es no exponer SQL Server a internet. Pero la realidad operativa es más complicada:

  • Muchos sistemas legacy de punto de venta y ERP conectan directo por cadena de conexión a una IP pública.
  • Las migraciones entre proveedores de hosting dejan ventanas donde el puerto queda expuesto.
  • Una regla de firewall mal aplicada puede abrir el 1433 sin que nadie se dé cuenta.
  • En entornos multi-tenant con muchos clientes y usuarios, cerrar todo de golpe rompe la operación.

La pregunta entonces no es «¿debería estar expuesto?» (la respuesta es no), sino: «si por cualquier razón el puerto queda expuesto, ¿cómo se defiende el SQL Server por sí mismo?»

02 / ESTADO DEL ARTELo que ya existe, y por qué no alcanza

Antes de construir nada, revisé exhaustivamente qué hay publicado. El panorama se divide en dos mitades que nadie ha unido.

Mitad A — detectar leyendo el error log

Existen artículos que usan T-SQL y xp_readerrorlog para detectar los intentos fallidos. El más completo es una serie de CodeProject de 2018. Pero el bloqueo lo realizan invocando xp_cmdshell para llamar a netsh y crear reglas en el Windows Firewall. Esto tiene dos problemas graves: requiere habilitar xp_cmdshell — precisamente la funcionalidad que los atacantes buscan activar para ejecutar comandos en el sistema operativo — y depende de un componente externo al motor.

Mitad B — bloquear con un LOGON trigger

Desde 2009 existen artículos que muestran cómo crear una tabla de IPs y un LOGON trigger que rechaza las conexiones que coincidan. Pero en todos los casos la lista es estática y manual: las IPs se insertan a mano. No hay detección automática que alimente esa tabla.

El hueco

CaracterísticaCodeProject 2018LOGON trigger 2009-13fail2ban / GitHub
Detección automáticaNo (manual)
Bloqueo dentro del motorNo (firewall)No (firewall)
Sin xp_cmdshellNo
Sin PowerShellNoNo
Sin dependencias externasNoNo

Nadie combina las dos mitades en un sistema nativo. La detección automática siempre «sale» del motor hacia una herramienta externa; el bloqueo por trigger siempre es manual. Incluso Microsoft, en su documentación histórica, afirma que SQL Server no puede bloquear conexiones por IP de forma automática y recomienda usar firewall o IPSec. Las herramientas más usadas de la comunidad — sp_WhoIsActive, el First Responder Kit, dbatools — son de monitoreo y performance; ninguna hace bloqueo de IPs por fuerza bruta.

03 / FILOSOFÍADefensa en capas, no reemplazo

Honestidad técnica

Este sistema no reemplaza al firewall. Un LOGON trigger no bloquea tráfico a nivel de red: para cuando ejecuta el ROLLBACK, el paquete ya llegó y ya se consumió un hilo. Bajo un ataque volumétrico masivo, esa carga afecta al motor aunque rechace todo.

Por eso el diseño correcto es de tres capas, donde el Auto-Blocker es la segunda — una red de seguridad, no la defensa principal:

Capa 1 — Red:     Firewall / VPN / IPSec / TSplus
                  (bloquea el tráfico antes de llegar al SQL Server)

Capa 2 — Motor:   SQL Auto-Blocker  ◄── este artículo
                  (se defiende solo si el puerto queda expuesto)

Capa 3 — Túnel:   Túnel cifrado que permite cerrar el 1433 público
                  (acceso solo por canal autenticado)

El valor de la Capa 2 es exactamente para los momentos en que la Capa 1 falla: la migración, la regla mal puesta, el puerto que quedó abierto. En ese hueco, el SQL Server deja de estar indefenso.

04 / ARQUITECTURAUn ciclo cerrado dentro del motor

El sistema es un ciclo de cuatro pasos, todo dentro de SQL Server:

          ┌─────────────────────────────┐
          │   Error Log de SQL Server   │
          │   (intentos "Login failed") │
          └──────────────┬──────────────┘
                         │ cada 1 min
                         ▼
          ┌─────────────────────────────┐
          │   SQL Agent Job             │
          │   EXEC sp_check_brute_force │
          └──────────────┬──────────────┘
                         │ xp_readerrorlog
                         ▼
          ┌─────────────────────────────┐
          │   Detección por umbral      │
          │   ¿IP con N fallos en M seg?│
          └──────────────┬──────────────┘
                         │ si supera el umbral
                         ▼
          ┌─────────────────────────────┐
          │   Tabla blocked_ips         │
          │   (INSERT de la IP)         │
          └──────────────┬──────────────┘
                         │ consultada en cada login
                         ▼
          ┌─────────────────────────────┐
          │   LOGON Trigger             │
          │   ¿IP bloqueada? → ROLLBACK │
          └─────────────────────────────┘

Cuatro componentes: tablas de control (configuración, lista de bloqueos y auditoría), un procedimiento de detección, un LOGON trigger que rechaza las IPs bloqueadas, y un SQL Agent Job que ejecuta el procedimiento periódicamente.

05 / IMPLEMENTACIÓNDel log a la IP, y de la IP al bloqueo

5.1 — Detección

El corazón de la detección es xp_readerrorlog, filtrando por Login failed:

SQL
CREATE TABLE #errorlog (
    LogDate     DATETIME,
    ProcessInfo VARCHAR(50),
    LogText     VARCHAR(MAX)
)

INSERT INTO #errorlog
EXEC xp_readerrorlog 0, 1, N'Login failed'

La IP del atacante viene embebida en el texto del log, entre [CLIENT: y ]. Se extrae con manipulación de cadenas — sin parsear XML, un punto de eficiencia que veremos más adelante:

SQL
SELECT
    SUBSTRING(
        LogText,
        CHARINDEX('[CLIENT: ', LogText) + 9,
        CHARINDEX(']', LogText, CHARINDEX('[CLIENT: ', LogText))
            - CHARINDEX('[CLIENT: ', LogText) - 9
    ) AS ip_address,
    COUNT(*) AS attempt_count
FROM #errorlog
WHERE LogDate >= DATEADD(SECOND, -@ventana_seg, GETDATE())
  AND CHARINDEX('[CLIENT: ', LogText) > 0
GROUP BY /* misma expresión de IP */
HAVING COUNT(*) >= @max_intentos

La lógica es simple y robusta: ¿esta IP falló más de N veces en los últimos M segundos? Los valores por defecto (10 intentos en 60 segundos) se calibraron contra ataques reales.

5.2 — La calibración del umbral importa

La diferencia entre bloquear bots y no molestar usuarios reales está en el umbral:

OrigenComportamiento¿Cruza 10 en 60s?
Atacante automatizado60-900 intentos a ~1.3/segSí, en ~8 segundos
Ser humano o equivalente2-4 intentos y se detieneNo

Cualquier IP que dispare 10 intentos en 60 segundos es un bot por definición. Un humano real no alcanza esa velocidad. Por eso el bloqueo automático puede ser agresivo (incluso permanente) sin riesgo de afectar usuarios legítimos.

5.3 — El bloqueo: el LOGON trigger

El trigger se ejecuta antes de establecer la sesión. Aquí están las decisiones de diseño que lo hacen seguro y eficiente:

SQL · LOGON Trigger
CREATE TRIGGER trg_autoblocker_logon
ON ALL SERVER
FOR LOGON
AS
BEGIN
    BEGIN TRY
        DECLARE @client_ip VARCHAR(48)
        SET @client_ip = CAST(CONNECTIONPROPERTY('client_net_address') AS VARCHAR(48))

        -- SIEMPRE permitir: localhost, red local, VPN
        IF @client_ip IN ('127.0.0.1', '<local>', '::1')
            RETURN
        IF @client_ip LIKE '10.%'   -- red local
            RETURN
        IF @client_ip LIKE '25.%'   -- rango VPN (clientes legítimos)
            RETURN

        -- ¿Trigger activo? (permite modo pánico)
        DECLARE @activo BIT
        SELECT @activo = CAST(config_value AS BIT)
        FROM master.dbo.autoblocker_config
        WHERE config_key = 'trigger_activo'

        IF ISNULL(@activo, 0) = 0
            RETURN

        -- ¿IP bloqueada y no expirada?
        IF EXISTS (
            SELECT 1 FROM master.dbo.blocked_ips
            WHERE ip_address = @client_ip
              AND is_active = 1
              AND expires_at > GETDATE()
        )
        BEGIN
            RAISERROR('Conexion bloqueada por Auto-Blocker.', 16, 1)
            ROLLBACK
        END
    END TRY
    BEGIN CATCH
        IF ERROR_NUMBER() = 50000
        BEGIN
            RAISERROR('Conexion bloqueada por Auto-Blocker.', 16, 1)
            ROLLBACK
        END
        -- Cualquier OTRO error: fail-open (permitir conexion)
    END CATCH
END

Tres decisiones clave en este trigger:

1. Usa CONNECTIONPROPERTY, no EVENTDATA(). La mayoría de los ejemplos publicados extraen la IP parseando el XML de EVENTDATA() en cada login, lo que añade sobrecarga de CPU en cada conexión. CONNECTIONPROPERTY('client_net_address') es una lectura directa, mucho más liviana.

2. Bloquea por IP, no por rol. Un error común en ejemplos publicados es escribir IF IS_SRVROLEMEMBER('sysadmin') = 1 RETURN; al inicio. Eso deja pasar a cualquiera que se autentique como sysadmin antes de revisar el bloqueo — pero el ataque es justamente contra sa, que es sysadmin. Si el atacante adivina la contraseña, ese diseño lo deja entrar. Bloquear por IP cierra ese agujero.

3. Diseño fail-open. Si ocurre cualquier error inesperado dentro del trigger, el CATCH permite la conexión. Es preferible dejar pasar una IP maliciosa por error que bloquear a todos los usuarios legítimos, incluido el administrador.

06 / SEGURIDADEl riesgo más grande, resuelto

El peligro real de cualquier LOGON trigger es dejar a todos afuera, incluido el administrador. Este sistema lo mitiga en cinco niveles:

  1. localhost siempre pasa — el trigger nunca bloquea 127.0.0.1 ni ::1.
  2. Exclusión de subredes — las redes locales (10.x) y de VPN se excluyen tanto en el detector como en el trigger.
  3. Diseño fail-open — un error en el trigger permite la conexión, no la bloquea.
  4. Modo pánico — un procedimiento desactiva todo de un solo comando.
  5. Conexión de emergencia vía DAC — la Dedicated Admin Connection no dispara LOGON triggers.
SQL · Modo pánico
CREATE PROCEDURE dbo.sp_autoblocker_panic
AS
BEGIN
    UPDATE dbo.autoblocker_config SET config_value = '0'
        WHERE config_key = 'trigger_activo'
    UPDATE dbo.blocked_ips SET is_active = 0,
        unblocked_by = 'PANIC_MODE', unblocked_at = GETDATE()
        WHERE is_active = 1
END

Si todo lo demás falla, siempre es posible entrar y desactivar el sistema por el canal de administración dedicado:

CMD · DAC
sqlcmd -S 127.0.0.1 -A -U sa -P tupassword
EXEC master.dbo.sp_autoblocker_panic

Este último punto es la red de seguridad definitiva: por diseño de SQL Server, el DAC ignora los LOGON triggers, así que es imposible quedar bloqueado de forma permanente.

07 / OBJECIONESRespondiendo las críticas de frente

Un sistema de seguridad serio debe anticipar sus críticas. Estas son las objeciones legítimas y cómo el diseño las atiende:

Objeción

«Un LOGON trigger no para el ataque a nivel de red, consume recursos.»

Correcto. Por eso este es un sistema de Capa 2, complementario al firewall, no su reemplazo. Su valor es defender el motor en los momentos en que la capa de red falla. No está pensado para absorber un ataque volumétrico — para eso está el firewall.

Objeción

«Parsear EVENTDATA() en cada login degrada el rendimiento.»

Por eso este diseño usa CONNECTIONPROPERTY('client_net_address'), una lectura directa, en lugar de parsear XML en cada conexión.

Objeción

«El administrador puede quedar bloqueado y recuperar el acceso requiere modo -m con downtime.»

Por eso existen cinco capas de protección, incluido el modo pánico y el DAC, que por diseño ignora los triggers. Recuperar el acceso es inmediato, sin reiniciar el servicio.

Objeción

«Publicar el código le dice al atacante cómo desarmarlo.»

Para desactivar el sistema hace falta acceso sysadmin al motor. Si el atacante ya lo tiene, no necesita fuerza bruta — ya ganó. Esto es el Principio de Kerckhoffs: un sistema debe ser seguro aunque el atacante conozca cómo funciona. fail2ban es open source y sigue siendo efectivo en millones de servidores.

08 / COMPARACIÓNFrente a todo lo demás

CaracterísticaFirewallfail2banCodeProjectTrigger manualEste sistema
Detección automáticaNo
Bloqueo en el motorNoNoNo
Sin xp_cmdshellNo
Sin PowerShellNo
Sin dependencias externasNoNoNo
Detección por comportamientoNoNo
Auto-expiraciónNoNo
Modo pánico / recuperaciónNoNo
Windows y LinuxDependeSolo LinuxSolo WinAmbosAmbos
Auditoría integradaParcialParcialNo

09 / CONCLUSIÓNLa segunda capa que casi nadie tiene

Las piezas individuales de este sistema — LOGON triggers, xp_readerrorlog, SQL Agent — existen y están documentadas desde 2005-2009. La contribución de este trabajo no es inventar una característica nueva del motor, sino combinarlas en una implementación nativa y auto-contenida que no depende de ningún componente externo: ni PowerShell, ni xp_cmdshell, ni Windows Firewall, ni fail2ban.

La idea central

Todas las soluciones publicadas dependen de al menos un componente externo. Este es el primer ciclo cerrado de detección y bloqueo enteramente en T-SQL: detecta, bloquea, registra y expira, sin salir nunca del SQL Server.

No reemplaza al firewall. Es la segunda capa — la que defiende al motor en el momento exacto en que la primera capa falla. Y en un mundo donde demasiados SQL Server quedan expuestos por error de configuración, esa segunda capa puede ser la diferencia entre un log lleno de intentos rechazados y una base de datos comprometida.

El script completo de instalación incluye las tres tablas, los procedimientos de detección, desbloqueo, estado y pánico, el LOGON trigger y el SQL Agent Job, listo para ejecutar en una sola corrida sobre la base master. Probado en producción contra ataques reales.
Nestor A. Molina V. Nestor A. Molina V. CEO de Innova Soft Latinoamérica
// Escrito y probado en producción desde Panamá 🇵🇦 — por un desarrollador latinoamericano, para la comunidad.

¿Te sirvió este artículo?

Sigue a Innova Soft Latinoamérica para más contenido sobre SQL Server, ciberseguridad y desarrollo. Compartimos lo que aprendemos en producción.

Facebook
WhatsApp
Twitter
LinkedIn
Pinterest