Notice: Due to size constraints and loading performance considerations, scripts referenced in blog posts are not attached directly. To request access, please complete the following form: Script Request Form Note: A Google account is required to access the form.
Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

Never Configure SQL Server Auditing Manually Again

Have you ever been asked to implement comprehensive SQL Server auditing and felt overwhelmed by the complexity? I recently faced this exact challenge when tasked with creating an audit system that would track every critical security event and write them directly to the Windows Application Log.

This seemed like a very manual update, which has been turned into automation where you only need to specify the SQL instance.

What is SQL Server Auditing?

Before diving into the technical implementation, let me explain why SQL Server auditing isn't just a nice-to-have feature – it's absolutely essential for any production environment.

Understanding Audits vs. Audit Specifications

SQL Server's auditing architecture has two key components that work together, and understanding their relationship is crucial:

Audit Object

  • File system locations
  • Windows Event Log (Application or Security)
  • Azure Blob Storage (for Azure SQL)

Audit Specification

  • Login attempts (successful and failed)
  • Permission changes
  • Data access patterns
  • Configuration modifications

You can have multiple audit specifications feeding into a single audit object, allowing you to organize different types of monitoring while centralizing the storage location.

Initially, I considered doing this manually through SQL Server Management Studio, but the thought of repeating this process across dozens of servers made no sense, there had to be a better way.

PowerShell Automation

I decided to build a PowerShell script that would automate the entire process. But here's where it gets interesting – I quickly discovered that not all SQL Server editions support auditing, and the script needed to handle this gracefully.

Step 1: Edition Compatibility Check

The first lesson I learned was that SQL Server Audit is only available in Enterprise, Developer, and Evaluation editions. Standard and Express editions simply don't support it. Rather than letting users discover this after running the script, I built in a preemptive check:

SELECT 
    CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)) AS Edition,
    CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) AS Version,
    CASE 
        WHEN CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)) LIKE '%Enterprise%' THEN 1
        WHEN CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)) LIKE '%Developer%' THEN 1
        WHEN CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)) LIKE '%Evaluation%' THEN 1
        ELSE 0
    END AS SupportsAuditing

This check saved me countless headaches. The script now fails gracefully with helpful alternatives if auditing isn't supported:

if ($editionInfo.SupportsAuditing -eq 0) {
    Write-Host "❌ AUDIT NOT SUPPORTED" -ForegroundColor Red
    Write-Host "SQL Server Audit is only available in:" -ForegroundColor Red
    Write-Host "- Enterprise Edition" -ForegroundColor Red
    Write-Host "- Developer Edition" -ForegroundColor Red
    Write-Host "- Evaluation Edition" -ForegroundColor Red
    exit 1
}

Step 2: Creating the Audit

Once I confirmed the edition supports auditing, creating the actual audit object was straightforward. The key insight was using TO APPLICATION_LOG instead of file-based auditing:

CREATE SERVER AUDIT [Audit]
TO APPLICATION_LOG
WITH 
(
    QUEUE_DELAY = 1000,
    ON_FAILURE = CONTINUE
);

Configuring Application Log Output

The magic happens in this simple line of SQL:

CREATE SERVER AUDIT [Audit]
TO APPLICATION_LOG
WITH 
(
    QUEUE_DELAY = 1000,
    ON_FAILURE = CONTINUE
);

Key configuration choices:

  • TO APPLICATION_LOG: Directs all audit events to Windows Application Log
  • QUEUE_DELAY = 1000: Buffers events for 1 second before writing (improves performance)
  • ON_FAILURE = CONTINUE: If audit writing fails, SQL Server continues operating (vs. shutting down)

This configuration strikes the right balance between security monitoring and system availability. In most environments, you don't want SQL Server to halt operations if the audit system has issues.

Viewing Your Audit Events

Once the script runs successfully, audit events start flowing immediately. To view them:

  1. Open Event Viewer
  2. Navigate to Windows Logs > Application
  3. Look for events from source "MSSQLSERVER" or "MSSQL$INSTANCENAME"
  4. Filter by Event ID 33205 for audit events

You'll see detailed information about every login attempt, permission change, and configuration modification – exactly what you need for security monitoring and compliance.

Step 3: Defining What to Audit

The "SQL Data Collection" specification needed to capture security-critical events without overwhelming the system. After researching various audit groups, I settled on this focused set:

CREATE SERVER AUDIT SPECIFICATION [SQL Data Collection]
FOR SERVER AUDIT [Audit]
ADD (FAILED_LOGIN_GROUP),
ADD (LOGOUT_GROUP),
ADD (SERVER_ROLE_MEMBER_CHANGE_GROUP),
ADD (DATABASE_ROLE_MEMBER_CHANGE_GROUP),
ADD (SERVER_PERMISSION_CHANGE_GROUP),
ADD (DATABASE_PERMISSION_CHANGE_GROUP),
ADD (SCHEMA_OBJECT_PERMISSION_CHANGE_GROUP),
ADD (SERVER_OBJECT_PERMISSION_CHANGE_GROUP),
ADD (DATABASE_OBJECT_OWNERSHIP_CHANGE_GROUP),
ADD (SERVER_OPERATION_GROUP),
ADD (AUDIT_CHANGE_GROUP),
ADD (LOGIN_CHANGE_PASSWORD_GROUP);

Each audit group serves a specific security purpose:

  • FAILED_LOGIN_GROUP: Intrusion detection
  • LOGOUT_GROUP: Session tracking
  • Permission change groups: Unauthorized privilege escalation
  • AUDIT_CHANGE_GROUP: Protecting audit integrity
  • SERVER_OPERATION_GROUP: Configuration tampering

Overcoming SSL Certificate Challenges

During testing, I encountered an unexpected SSL certificate error when PowerShell tried to connect to SQL Server. The error message was misleading – it had nothing to do with the audit functionality itself, just the connection method.

The solution was adding certificate trust parameters to the Invoke-SqlCmd calls:

Invoke-Sqlcmd -ServerInstance $ServerInstance -Query $createAuditSQL -TrustServerCertificate -EncryptConnection

This maintains security while bypassing certificate validation issues in development environments.

Pre-Flight Script

You need to have the SQL management tools installed for this script to work, therefore run this before the script to ensure you can execute the main script without issues:

# Install the SQL Server module
Install-Module -Name SqlServer -Force -AllowClobber

# Import the module
Import-Module SqlServer

# Verify installation
Get-Module SqlServer -ListAvailable
# Events are written to the Windows Application Log
Script : AuditScript.ps1
# Sentinel Audit Creation Script
# This script creates a comprehensive audit/audit specfication
# Events are written to the Windows Application Log

param(
    [Parameter(Mandatory=$true)]
    [string]$ServerInstance
)

# Import SQL Server module
Import-Module SqlServer -Force

# Check SQL Server edition first
Write-Host "Checking SQL Server edition compatibility..." -ForegroundColor Yellow

$editionCheckSQL = @"
SELECT 
    CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)) AS Edition,
    CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) AS Version,
    CAST(SERVERPROPERTY('ProductLevel') AS NVARCHAR(128)) AS ServicePack,
    CASE 
        WHEN CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)) LIKE '%Enterprise%' THEN 1
        WHEN CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)) LIKE '%Developer%' THEN 1
        WHEN CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)) LIKE '%Evaluation%' THEN 1
        ELSE 0
    END AS SupportsAuditing
"@

try {
    $editionInfo = Invoke-Sqlcmd -ServerInstance $ServerInstance -Query $editionCheckSQL -TrustServerCertificate -EncryptConnection -ErrorAction Stop
    
    Write-Host "SQL Server Details:" -ForegroundColor Cyan
    Write-Host "- Edition: $($editionInfo.Edition)" -ForegroundColor White
    Write-Host "- Version: $($editionInfo.Version)" -ForegroundColor White
    Write-Host "- Service Pack: $($editionInfo.ServicePack)" -ForegroundColor White
    
    if ($editionInfo.SupportsAuditing -eq 0) {
        Write-Host "`n❌ AUDIT NOT SUPPORTED" -ForegroundColor Red
        Write-Host "SQL Server Audit is only available in:" -ForegroundColor Red
        Write-Host "- Enterprise Edition" -ForegroundColor Red
        Write-Host "- Developer Edition" -ForegroundColor Red
        Write-Host "- Evaluation Edition" -ForegroundColor Red
        Write-Host "`nYour current edition ($($editionInfo.Edition)) does not support SQL Server Audit." -ForegroundColor Red
        Write-Host "`nAlternatives:" -ForegroundColor Yellow
        Write-Host "1. Upgrade to Enterprise/Developer edition" -ForegroundColor White
        Write-Host "2. Use Extended Events (available in all editions)" -ForegroundColor White
        Write-Host "3. Use SQL Server on Azure (includes audit features)" -ForegroundColor White
        exit 1
    }
    
    Write-Host "✓ SQL Server Audit is supported on this edition!" -ForegroundColor Green
    
} catch {
    Write-Error "Failed to connect to SQL Server: $($_.Exception.Message)"
    Write-Host "Please ensure:" -ForegroundColor Red
    Write-Host "1. SQL Server instance name is correct: $ServerInstance" -ForegroundColor Red
    Write-Host "2. SQL Server is running and accessible" -ForegroundColor Red
    Write-Host "3. You have permission to connect" -ForegroundColor Red
    exit 1
}

# SQL Script to create Sentinel audit
$createAuditSQL = @"
-- Step 1: Create the Sentinel Server Audit (Writing to Application Log)
IF EXISTS (SELECT * FROM sys.server_audits WHERE name = 'Sentinel')
BEGIN
    ALTER SERVER AUDIT [Sentinel] WITH (STATE = OFF);
    DROP SERVER AUDIT [Sentinel];
    PRINT 'Dropped existing Sentinel audit';
END

CREATE SERVER AUDIT [Sentinel]
TO APPLICATION_LOG
WITH 
(
    QUEUE_DELAY = 1000,
    ON_FAILURE = CONTINUE
);

PRINT 'Created Sentinel server audit';

-- Step 2: Create SQL Data Collection Server Audit Specification
IF EXISTS (SELECT * FROM sys.server_audit_specifications WHERE name = 'SQL Data Collection')
BEGIN
    ALTER SERVER AUDIT SPECIFICATION [SQL Data Collection] WITH (STATE = OFF);
    DROP SERVER AUDIT SPECIFICATION [SQL Data Collection];
    PRINT 'Dropped existing SQL Data Collection specification';
END

CREATE SERVER AUDIT SPECIFICATION [SQL Data Collection]
FOR SERVER AUDIT [Sentinel]
ADD (FAILED_LOGIN_GROUP),
ADD (LOGOUT_GROUP),
ADD (SERVER_ROLE_MEMBER_CHANGE_GROUP),
ADD (DATABASE_ROLE_MEMBER_CHANGE_GROUP),
ADD (SERVER_PERMISSION_CHANGE_GROUP),
ADD (DATABASE_PERMISSION_CHANGE_GROUP),
ADD (SCHEMA_OBJECT_PERMISSION_CHANGE_GROUP),
ADD (SERVER_OBJECT_PERMISSION_CHANGE_GROUP),
ADD (DATABASE_OBJECT_OWNERSHIP_CHANGE_GROUP),
ADD (SERVER_OPERATION_GROUP),
ADD (AUDIT_CHANGE_GROUP),
ADD (LOGIN_CHANGE_PASSWORD_GROUP);

PRINT 'Created SQL Data Collection server audit specification';

-- Step 3: Enable the audit and specification
ALTER SERVER AUDIT [Sentinel] WITH (STATE = ON);
PRINT 'Enabled Sentinel server audit';

ALTER SERVER AUDIT SPECIFICATION [SQL Data Collection] WITH (STATE = ON);
PRINT 'Enabled SQL Data Collection server audit specification';

-- Step 4: Verify audit is working
SELECT 
    a.name AS audit_name,
    a.create_date,
    a.modify_date,
    a.is_state_enabled,
    'APPLICATION_LOG' AS audit_destination
FROM sys.server_audits a
WHERE a.name = 'Sentinel';

SELECT 
    name AS specification_name,
    create_date,
    modify_date,
    is_state_enabled
FROM sys.server_audit_specifications
WHERE name = 'SQL Data Collection';
"@

try {
    Write-Host "Connecting to SQL Server: $ServerInstance" -ForegroundColor Yellow
    Write-Host "Creating Sentinel audit with SQL Data Collection specification..." -ForegroundColor Yellow
    
    # Execute the audit creation script with SSL bypass
    Invoke-Sqlcmd -ServerInstance $ServerInstance -Query $createAuditSQL -QueryTimeout 300 -TrustServerCertificate -EncryptConnection
    
    Write-Host "✓ Sentinel audit created successfully and writing to Application Log!" -ForegroundColor Green
    Write-Host "✓ SQL Data Collection specification created and enabled!" -ForegroundColor Green
    
    Write-Host "`nAudit Details:" -ForegroundColor Cyan
    Write-Host "- Audit Name: Sentinel" -ForegroundColor White
    Write-Host "- Specification Name: SQL Data Collection" -ForegroundColor White
    Write-Host "- Audit Destination: Windows Application Log" -ForegroundColor White
    Write-Host "- Covers: Login attempts, permission changes, data access, configuration changes" -ForegroundColor White
    
    # Query to show what's being audited
    $verifySQL = @"
SELECT 
    sas.name AS SpecificationName,
    sasd.audit_action_name AS ActionName,
    sasd.audited_result AS AuditedResult
FROM sys.server_audit_specifications sas
JOIN sys.server_audit_specification_details sasd ON sas.server_specification_id = sasd.server_specification_id
WHERE sas.name = 'SQL Data Collection'
ORDER BY ActionName;
"@
    
    Write-Host "`nVerifying audit configuration..." -ForegroundColor Yellow
    $auditConfig = Invoke-Sqlcmd -ServerInstance $ServerInstance -Query $verifySQL -TrustServerCertificate -EncryptConnection
    
    Write-Host "`nAudit Actions Being Tracked:" -ForegroundColor Cyan
    $auditConfig | Format-Table -AutoSize
    
} catch {
    Write-Error "Failed to create Sentinel audit: $($_.Exception.Message)"
    Write-Host "Please ensure:" -ForegroundColor Red
    Write-Host "1. You have sysadmin privileges on the SQL Server" -ForegroundColor Red
    Write-Host "2. The SQL Server instance name is correct" -ForegroundColor Red
    Write-Host "3. SQL Server is running and accessible" -ForegroundColor Red
}

# Usage example at the end
Write-Host "`n" -NoNewline
Write-Host "USAGE EXAMPLE:" -ForegroundColor Magenta
Write-Host ".\SentinelAuditScript.ps1 -ServerInstance 'YourServerName\InstanceName'" -ForegroundColor White
Write-Host "or" -ForegroundColor Gray
Write-Host ".\SentinelAuditScript.ps1 -ServerInstance 'localhost'" -ForegroundColor White
Write-Host "`nTo view audit events in Application Log:" -ForegroundColor Yellow
Write-Host "Open Event Viewer > Windows Logs > Application" -ForegroundColor White
Write-Host "Look for events with Source: 'MSSQL`$INSTANCENAME' or 'MSSQLSERVER'" -ForegroundColor White

Here's how simple it is to use:

.\AuditScript.ps1 -ServerInstance "YourServerName\InstanceName"

What do the Audit Event look like?

Here's a real example of what gets captured when someone attempts an unauthorized login:

Event Details:

Event ID: 33205
Source: MSSQLSERVER
Level: Information
Date: 2025-01-15 14:23:17

event_time:2025-01-15 14:23:17.1234567
sequence_number:1
action_id:LGIF
succeeded:false
session_id:52
server_principal_name:BEAR\suspicious_user
database_principal_name:
target_server_principal_name:
target_database_principal_name:
object_name:
class_type:LG
session_server_principal_name:BEAR\suspicious_user
database_name:
schema_name:
object_id:
permission_bitmask:
is_column_permission:false
statement:LOGIN
additional_information:<TSQLStmt>LOGIN</TSQLStmt>
application_name:Microsoft SQL Server Management Studio - Query
client_ip:192.168.1.100
data_sensitivity_information:

What this tells us:

  • Who: BEAR\suspicious_user attempted access
  • What: LOGIN action that failed (succeeded:false)
  • When: Precise timestamp down to microseconds
  • Where: From IP address 192.168.1.100
  • How: Using SQL Server Management Studio

This level of detail is invaluable for security analysis. In a real incident, I could quickly identify:

  • Failed login patterns indicating brute force attacks
  • Successful logins from unusual IP addresses or applications
  • Permission escalation attempts
  • Data access outside normal business hours

Another example - Permission Change Event:

Event ID: 33205
Source: MSSQLSERVER
Level: Information

action_id:SHCM
succeeded:true
server_principal_name:BEAR\admin_user
statement:ALTER ROLE db_owner ADD MEMBER [BEAR\regular_user]
target_database_principal_name:BEAR\regular_user
database_name:CustomerDB
additional_information:<TSQLStmt>ALTER ROLE db_owner ADD MEMBER [BEAR\regular_user]</TSQLStmt>

This shows exactly when someone was granted db_owner privileges, who granted it, and in which database – critical information for security auditing.

Visual Results

This is the SQL management studio before the script is run remotely, notice that the "Audit" and "Server Audit Specifications" are empty:


When the script has been run, these same folders now look like this, with enabled Audit and Audit Specifications as below:


Conclusion

What started as a manual auditing task transformed into a robust automation solution. The "Sentinel" audit system now provides comprehensive security monitoring with minimal operational overhead.

The key insight was recognizing that good automation isn't just about eliminating manual work – it's about eliminating the potential for human error while making complex tasks repeatable and reliable.

Previous Post Next Post

نموذج الاتصال