SQL Server Audit

Cuando administras instancias de SQL Server en entornos regulados (ISO 27001, GDPR, auditorías internas, etc.), necesitas algo más que “confiar” en que nadie toca nada crítico. Para eso sirve SQL Server Audit: el mecanismo nativo para dejar rastro verificable de cambios administrativos.

Objetivo

El objetivo de SQL Server Audit es registrar y supervisar todas las acciones administrativas relevantes realizadas sobre la instancia y sus bases de datos. En concreto, deja trazabilidad de cambios estructurales y de permisos, como altas/bajas/modificaciones de logins, roles, bases de datos u objetos de esquema.

En resumen: quién hizo qué, cuándo, dónde y con qué comando.

Beneficios principales de SQL Server Audit

  • Trazabilidad completa de acciones de administradores y usuarios privilegiados.
  • Evidencia formal y exportable para auditorías de seguridad.
  • Detección rápida de cambios no autorizados en logins, roles y objetos críticos.
  • Centralización de eventos en una base dedicada («Auditoría») para consulta sencilla e integración con otros sistemas (dashboards, SIEM, etc.).
  • Crecimiento controlado del almacenamiento gracias a rotación automática de ficheros.

Alcance y límites

Incluye (lo que realmente interesa en control administrativo):

  • Cambios a nivel servidor:
    • logins
    • roles de servidor y membresías
    • creación/modificación/borrado de bases de datos
  • Cambios a nivel base de datos:
    • objetos de esquema (tablas, vistas, procedimientos, funciones…)
    • usuarios/roles de BD y membresías Auditoria_SQL Server

SQL Server Audit no incluye por diseño:

  • Actividad de datos del usuario (SELECT/INSERT/UPDATE/DELETE).
  • Ejecución de procedimientos almacenados.
  • Logons fallidos o accesos (se pueden añadir si hiciera falta). Auditoria_SQLServer

Esto es clave: no es auditoría de uso, es auditoría de cambios administrativos.

Implementación de SQL Server Audit

  • Auditoría a fichero (nivel servidor)
USE master;
GO

IF NOT EXISTS (SELECT 1 FROM sys.server_audits WHERE name = N'Audit_Seguridad')
BEGIN
    CREATE SERVER AUDIT [Audit_Seguridad]
    TO FILE (
        FILEPATH = N'E:\SQLAudit\',
        MAXSIZE = 1 GB,
        MAX_ROLLOVER_FILES = 50,
        RESERVE_DISK_SPACE = ON
    )
    WITH (QUEUE_DELAY = 1000, ON_FAILURE = CONTINUE);
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.server_audit_specifications WHERE name = N'Spec_Seguridad_Servidor')
BEGIN
    CREATE SERVER AUDIT SPECIFICATION [Spec_Seguridad_Servidor]
    FOR SERVER AUDIT [Audit_Seguridad]
        ADD (SERVER_PRINCIPAL_CHANGE_GROUP)      -- Logins: create/alter/drop
      , ADD (SERVER_ROLE_MEMBER_CHANGE_GROUP)    -- Miembros de roles de servidor: add/drop
      , ADD (SERVER_OBJECT_CHANGE_GROUP)         -- Objetos a nivel servidor, incl. SERVER ROLE create/drop/alter
      , ADD (DATABASE_CHANGE_GROUP);             -- Create/alter/drop database
END
GO

ALTER SERVER AUDIT [Audit_Seguridad] WITH (STATE = ON);
ALTER SERVER AUDIT SPECIFICATION [Spec_Seguridad_Servidor] WITH (STATE = ON);
GO
  • Auditoría en TODAS las BDs de usuario (objetos de esquema y roles de BD)
USE master;
GO
DECLARE @sql nvarchar(max) = N'';

;WITH DBs AS (
  SELECT name
  FROM sys.databases
  WHERE database_id > 4 AND state = 0  -- solo BDs de usuario ONLINE
)
SELECT @sql = STRING_AGG(CONVERT(nvarchar(max), N'
USE ' + QUOTENAME(name) + N';
IF EXISTS (SELECT 1 FROM sys.database_audit_specifications WHERE name = N''Spec_Seguridad_DB'')
BEGIN
    ALTER DATABASE AUDIT SPECIFICATION [Spec_Seguridad_DB] WITH (STATE = OFF);
    DROP DATABASE AUDIT SPECIFICATION [Spec_Seguridad_DB];
    PRINT ''Spec_Seguridad_DB eliminada para recreación.'';
END;
CREATE DATABASE AUDIT SPECIFICATION [Spec_Seguridad_DB]
FOR SERVER AUDIT [Audit_Seguridad]
    ADD (SCHEMA_OBJECT_CHANGE_GROUP)         -- create/alter/drop table/objeto esquema
  , ADD (DATABASE_PRINCIPAL_CHANGE_GROUP)    -- create/alter/drop user/role de BD
  , ADD (DATABASE_ROLE_MEMBER_CHANGE_GROUP); -- add/drop miembros de roles BD
ALTER DATABASE AUDIT SPECIFICATION [Spec_Seguridad_DB] WITH (STATE = ON);
PRINT ''Spec_Seguridad_DB habilitada en ' + name + N''';
'), NCHAR(10))
FROM DBs;

EXEC sys.sp_executesql @sql;
PRINT 'Especificaciones de auditoría creadas/habilitadas en todas las BDs de usuario.';
GO
  • Objetos en «Auditoria»: esquema, vista y tabla de ingestión
USE Auditoria;
GO
IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = N'audit')
BEGIN
    EXEC('CREATE SCHEMA audit AUTHORIZATION dbo;');
    PRINT 'Esquema [audit] creado en Auditoria.';
END
ELSE
    PRINT 'Esquema [audit] ya existe en Auditoria.';
GO

-- Vista que lee todos los .sqlaudit generados por la auditoría
IF OBJECT_ID(N'audit.v_auditoria_all', N'V') IS NOT NULL
    DROP VIEW audit.v_auditoria_all;
GO
CREATE VIEW audit.v_auditoria_all AS
SELECT *
FROM sys.fn_get_audit_file(N'E:\SQLAudit\Audit_Seguridad_*.sqlaudit', DEFAULT, DEFAULT);
GO
PRINT 'Vista [audit].[v_auditoria_all] creada.';
GO

-- Tabla “materializada” para consultas rápidas y/o envío a SIEM
IF OBJECT_ID(N'audit.AUDIT_EVENTS', N'U') IS NULL
BEGIN
    CREATE TABLE audit.AUDIT_EVENTS
    (
        event_time               datetime2(7)   NOT NULL,
        sequence_number          bigint         NOT NULL,
        action_id                nvarchar(4)    NULL,
        succeeded                bit            NULL,
        server_principal_name    sysname        NULL,
        server_instance_name     sysname        NULL,
        database_name            sysname        NULL,
        schema_name              sysname        NULL,
        object_name              sysname        NULL,
        statement                nvarchar(max)  NULL,
        additional_information   nvarchar(max)  NULL,
        session_id               int            NULL,
        application_name         nvarchar(128)  NULL,
        client_ip                nvarchar(128)  NULL
        -- añade más columnas si las necesitas del conjunto devuelto por fn_get_audit_file
        , CONSTRAINT UQ_AUDIT_EVENTS UNIQUE
(
    event_time,
    sequence_number,
    action_id,
    server_instance_name
) WITH (IGNORE_DUP_KEY = ON ));
    PRINT 'Tabla [audit].[AUDIT_EVENTS] creada.';
END
ELSE
    PRINT 'Tabla [audit].[AUDIT_EVENTS] ya existe.';
GO

-- Índices de apoyo
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AUDIT_EVENTS_login' AND object_id = OBJECT_ID(N'audit.AUDIT_EVENTS'))
BEGIN
    CREATE INDEX IX_AUDIT_EVENTS_login ON audit.AUDIT_EVENTS(server_principal_name, event_time DESC);
    PRINT 'Índice IX_AUDIT_EVENTS_login creado.';
END
ELSE
    PRINT 'Índice IX_AUDIT_EVENTS_login ya existe.';
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AUDIT_EVENTS_dbobj' AND object_id = OBJECT_ID(N'audit.AUDIT_EVENTS'))
BEGIN
    CREATE INDEX IX_AUDIT_EVENTS_dbobj ON audit.AUDIT_EVENTS(database_name, schema_name, object_name, event_time DESC);
    PRINT 'Índice IX_AUDIT_EVENTS_dbobj creado.';
END
ELSE
    PRINT 'Índice IX_AUDIT_EVENTS_dbobj ya existe.';
GO
  • Procedimiento de ingestión desde los .sqlaudit → tabla
USE Auditoria;
GO
IF OBJECT_ID(N'audit.usp_audit_ingest', N'P') IS NOT NULL
  DROP PROCEDURE audit.usp_audit_ingest;
GO
CREATE PROCEDURE audit.usp_audit_ingest
AS
BEGIN
  SET NOCOUNT ON;

  ;WITH src AS
  (
    SELECT 
      event_time,
      sequence_number,
      action_id,
      succeeded,
      server_principal_name,
      server_instance_name,
      database_name,
      schema_name,
      object_name,
      statement,
      additional_information,
      session_id,
      application_name,
      client_ip
    FROM sys.fn_get_audit_file(N'E:\SQLAudit\Audit_Seguridad_*.sqlaudit', DEFAULT, DEFAULT)
  )
  INSERT INTO audit.AUDIT_EVENTS
  (
    event_time, sequence_number, action_id, succeeded,
    server_principal_name, server_instance_name, database_name,
    schema_name, object_name, statement, additional_information,
    session_id, application_name, client_ip
  )
  SELECT s.*
  FROM src s
  WHERE NOT EXISTS
  (
    SELECT 1
    FROM audit.AUDIT_EVENTS t
    WHERE t.event_time            = s.event_time
      AND t.sequence_number       = s.sequence_number
      AND t.action_id             = s.action_id
      AND ISNULL(t.server_instance_name,'') = ISNULL(s.server_instance_name,'')
  );

  PRINT 'Ingesta de auditoría finalizada.';
END
GO
PRINT 'Procedimiento [audit].[usp_audit_ingest] creado.';
GO
  • Job de SQL Agent para automatizar la ingesta (cada 5 min)
USE msdb;
GO
-- Job
IF NOT EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = N'Audit - Ingest .sqlaudit to Auditoria')
BEGIN
  EXEC msdb.dbo.sp_add_job
    @job_name = N'Audit - Ingest .sqlaudit to Auditoria',
    @enabled = 1,
    @description = N'Ingresa eventos de auditoría desde archivos .sqlaudit a Auditoria.audit.AUDIT_EVENTS.';
  PRINT 'Job creado.';
END
ELSE
  PRINT 'Job ya existía.';
GO

-- Paso del Job
IF NOT EXISTS (SELECT 1 FROM msdb.dbo.sysjobsteps WHERE step_name = N'Run ingest' AND job_id IN (SELECT job_id FROM msdb.dbo.sysjobs WHERE name = N'Audit - Ingest .sqlaudit to Auditoria'))
BEGIN
  EXEC msdb.dbo.sp_add_jobstep
    @job_name = N'Audit - Ingest .sqlaudit to Auditoria',
    @step_name = N'Run ingest',
    @subsystem = N'TSQL',
    @database_name = N'Auditoria',
    @command = N'EXEC audit.usp_audit_ingest;',
    @on_success_action = 1,  -- Quit
    @on_fail_action = 2;     -- Quit with failure
  PRINT 'Paso del job creado.';
END
ELSE
  PRINT 'Paso del job ya existía.';
GO

-- Schedule cada 5 minutos
IF NOT EXISTS (SELECT 1 FROM msdb.dbo.sysschedules WHERE name = N'Cada 5 minutos - Audit Ingest')
BEGIN
  EXEC msdb.dbo.sp_add_schedule
    @schedule_name = N'Cada 5 minutos - Audit Ingest',
    @freq_type = 4,            -- daily
    @freq_interval = 1,
    @freq_subday_type = 4,     -- minutes
    @freq_subday_interval = 5,
    @active_start_time = 0;    -- 00:00
  PRINT 'Schedule creado.';
END
ELSE
  PRINT 'Schedule ya existía.';
GO

-- Vincular schedule y server
EXEC msdb.dbo.sp_attach_schedule
  @job_name = N'Audit - Ingest .sqlaudit to Auditoria',
  @schedule_name = N'Cada 5 minutos - Audit Ingest';
GO
EXEC msdb.dbo.sp_add_jobserver
  @job_name = N'Audit - Ingest .sqlaudit to Auditoria';
GO
PRINT 'Job programado y habilitado.';
GO

Consultas de “evidencia” en Auditoria

-- Ultimos 30 minutos (TOP 200)
USE Auditoria;
GO
SELECT TOP (200)
       event_time,
       server_principal_name AS login,
       database_name         AS dbname,
       action_id,
       object_name,
       statement
FROM   audit.AUDIT_EVENTS
WHERE  event_time >= DATEADD(MINUTE, -30, SYSUTCDATETIME())
ORDER  BY event_time DESC;

-- Últimos 7 días
USE Auditoria;
GO
SELECT
       event_time,
       server_principal_name AS login,
       database_name         AS dbname,
       action_id,
       object_name,
       statement
FROM   audit.AUDIT_EVENTS
WHERE  event_time >= DATEADD(DAY, -7, SYSUTCDATETIME())
ORDER  BY event_time DESC;

En caso de mover la ruta almacenada.

Una vez que SQL Server Audit exista y quieras mover la salida a otra unidad (por ejemplo de E:\ a F:\Auditoria\) se hará de la siguiente manera:

  • Desactivar la auditoría:
ALTER SERVER AUDIT [Audit_Seguridad] WITH (STATE = OFF);
  • Modificar el destino:
ALTER SERVER AUDIT [Audit_Seguridad]
TO FILE (FILEPATH = N'F:\Auditoria\');
  • Volver a activar la auditoría:
ALTER SERVER AUDIT [Audit_Seguridad] WITH (STATE = ON);

De esta manera no se pierden los archivos ya existentes (quedarán en la carpeta anterior) y los nuevos se generarán en la nueva ruta.

Esta configuración proporciona una auditoría completa de cambios administrativos en SQL Server 2022, con bajo impacto en rendimiento y almacenamiento controlado.

Si quieres que revisemos tu entorno SQL Server, contacta con nosotros sin compromiso.

¿Aún no conoces Query Performance? Descubre cómo puede ayudarte en tu entorno Oracle. Más información en su página de LinkedIn.

Sígue a GPS en LinkedIn

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *