powershelldba.de · Uwe Janke

Auditing SQL Server: Logins, Database Create/Drop, and Metadata Changes

Audit requirements keep getting less optional: who connected and whether it succeeded, who created or dropped a database, who changed a table's schema or a permission. SQL Server has four genuinely different ways to answer those questions, not one obvious best answer, each with a different failure mode when it goes wrong. Here's what each one actually does, and how they compare.

Scope: What This Covers, and What It Doesn't

Three categories, in SQL Server's own vocabulary:

Deliberately out of scope: row-level DML auditing (who changed this specific value in this specific row). That's a heavier, different problem, solved by temporal tables, Change Data Capture, or Audit's SELECT/INSERT/UPDATE/DELETE action groups, worth its own article rather than a bullet point in this one.

Method 1: AuditLevel and the Error Log (the Baseline Everyone Already Has)

SSMS's Server Properties → Security → "Login auditing" setting is really just a registry value, AuditLevel, read by SQL Server at startup: 0 = none, 1 = successful only, 2 = failed only (the default), 3 = both. Whatever it's set to, matching events land as plain text in the SQL Server error log, message 18453 (successful, non-trusted), 18454 (successful, trusted), 18456 (failed).

This costs nothing to turn on and is already partially on by default, which is exactly why it's the baseline, not the answer. It has no coverage for database create/drop or metadata changes at all, it's a text log subject to rotation, and it's not tamper-evident: anyone with access to the file (or a sysadmin session) can edit or delete it with nothing to say it happened.

sqmSQLTool: Get-sqmErrorLog -FailedLogins -SuccessfulLogins queries exactly this, language-neutrally. See Every SQL Server Log for where this sits among everything else SQL Server writes.

Method 2: SQL Server Audit

The feature actually built for this, since SQL Server 2008, and in Standard Edition (not just Enterprise) since SQL Server 2016 SP1. Three pieces: a Server Audit (the destination, a binary .sqlaudit file, the Windows Application log, or the Windows Security log), a Server Audit Specification (server-scoped events, logins and CREATE/ALTER/DROP DATABASE among them), and optionally a Database Audit Specification per database (schema-level DDL, permission changes, scoped to that database).

-- 1. Destination
CREATE SERVER AUDIT AG_Compliance_Audit
TO FILE (FILEPATH = 'D:\Audit\', MAXSIZE = 500 MB, MAX_ROLLOVER_FILES = 20)
WITH (ON_FAILURE = FAIL_OPERATION, QUEUE_DELAY = 1000);

ALTER SERVER AUDIT AG_Compliance_Audit WITH (STATE = ON);

-- 2. Server-scoped events: logins, database create/drop, and who touches the audit itself
CREATE SERVER AUDIT SPECIFICATION AG_Server_Audit_Spec
FOR SERVER AUDIT AG_Compliance_Audit
    ADD (FAILED_LOGIN_GROUP),
    ADD (SUCCESSFUL_LOGIN_GROUP),
    ADD (DATABASE_CHANGE_GROUP),
    ADD (AUDIT_CHANGE_GROUP)
WITH (STATE = ON);

-- 3. Database-scoped: schema/metadata changes, per database
CREATE DATABASE AUDIT SPECIFICATION AG_Db_Audit_Spec
FOR SERVER AUDIT AG_Compliance_Audit
    ADD (SCHEMA_OBJECT_CHANGE_GROUP)
WITH (STATE = ON);

Query the result with sys.fn_get_audit_file, or read it through SSMS's Log File Viewer:

SELECT event_time, action_id, succeeded, server_principal_name,
       database_name, object_name, statement
FROM sys.fn_get_audit_file('D:\Audit\AG_Compliance_Audit*.sqlaudit', DEFAULT, DEFAULT)
ORDER BY event_time DESC;

AUDIT_CHANGE_GROUP above is not decoration: it's what answers the auditor's next question, who's watching the watcher, by recording changes to the audit configuration itself.

ON_FAILURE = SHUTDOWN is a real outage vector, not just a strict setting ON_FAILURE has three values: CONTINUE (keeps running, silently stops auditing, defeats the point), FAIL_OPERATION (blocks the audited action until the destination works again), and SHUTDOWN (stops the SQL Server service rather than let anything go unrecorded). SHUTDOWN is the strictest compliance posture and also means a full share, a broken permission, or a network blip on the audit destination takes the whole instance down with it. Choose it deliberately, with monitoring on the destination's free space and reachability, not as the "most secure-sounding" default.

Writing to the Windows Security log instead of a file needs two things most people forget on the first attempt: Object Access auditing enabled via local/group security policy, and the Generate security audits user right granted to the SQL Server service account. Skip either and the audit silently produces nothing there, with no error on the SQL Server side to say so.

In an Availability Group, provision the Server Audit shell on every replica ahead of time A Database Audit Specification lives inside the database, so it moves with it through failover and through an AG add, the same way a contained database's users do. The Server Audit object it depends on does not: it's server-scoped, outside any database, exactly like the server logins covered in AlwaysOn with Standard vs. Contained Databases. Create a matching Server Audit on every replica in advance, or the specification has nothing to attach to the moment that replica becomes primary.

Method 3: DDL Triggers and Logon Triggers

The pre-2008 approach, still fully current: a trigger that fires on the DDL event itself, inline with the transaction, with the DDL captured as XML via EVENTDATA(). Database-scoped DDL uses DDL_DATABASE_LEVEL_EVENTS; CREATE/ALTER/DROP DATABASE, being server-scoped, needs ON ALL SERVER FOR DDL_SERVER_LEVEL_EVENTS instead. Logins get their own separate mechanism, a logon trigger:

CREATE TRIGGER trg_AuditLogon
ON ALL SERVER FOR LOGON
AS
BEGIN
    INSERT INTO master.dbo.LoginAuditLog (LoginName, LoginTime, ClientHost)
    SELECT ORIGINAL_LOGIN(), GETDATE(), HOST_NAME();
END;

The one thing neither SQL Server Audit nor Extended Events can do: a logon trigger can ROLLBACK and actually reject the connection, and a DDL trigger can roll back the DDL statement it just watched happen. That makes triggers the only method on this list that's preventive rather than purely observational.

A broken logon trigger can lock out the entire instance Because it runs on every single connection attempt, an erroring or infinite-looping logon trigger blocks every login, including sysadmin's, through normal channels. The documented way back in is the Dedicated Admin Connection (sqlcmd -A), which bypasses logon triggers specifically for this scenario. Test a logon trigger's failure path, not just its success path, before it goes anywhere near production.

Triggers also aren't independent oversight by default: anyone with enough permission to alter the trigger or the logging table can quietly remove their own trail, unless those objects are locked down with permissions separate from the DBA's own. SQL Server Audit's configuration objects have the same theoretical exposure, but Audit at least has AUDIT_CHANGE_GROUP to catch it happening; a hand-rolled trigger and log table have whatever protection you built yourself, and no more.

Method 4: Targeted Extended Events

Instead of one of Audit's broad action groups, a custom Extended Events session can subscribe to exactly the events wanted: sqlserver.login for successful connections, error_reported filtered to error number 18456 for failures, database_created/database_dropped for database-level DDL, and object_created/object_altered/object_deleted for schema-level metadata changes (filtered to ddl_phase = 1, since each fires twice per statement: once on start, once on commit or rollback).

This is the lowest-overhead structured option and works on every edition back to 2008, but it comes with none of Audit's packaging: no fn_get_audit_file equivalent (read the event file target's XML yourself), no separate audit-configuration permission model, and a session that silently stops existing after a restart unless STARTUP_STATE = ON was set when it was created, a common, quiet gap. One asymmetry worth knowing before relying on it: database_created/database_dropped cannot be scoped to a specific database name by a session-level predicate, at the moment either fires, neither the collected connection context nor the event's own field resolves to the database being created or dropped, so these two are always instance-wide; object_created/object_altered/object_deleted scope to specific databases correctly.

sqmSQLTool: Register-sqmAuditSession sets up exactly this: -FailedLogins, -SuccessfulLogins, -DatabaseCreated, -DatabaseDropped, -MetadataChanges (optionally scoped with -TargetDatabase), each independently selectable, or -All. Reading it back needs no new function, Invoke-sqmExtendedEvents -Read -SessionName sqm_AuditSession already surfaces the right fields.

Comparison

Aspect AuditLevel SQL Server Audit DDL/Logon triggers Extended Events
Edition requirement All Standard 2016 SP1+, or Enterprise All, incl. Express All, incl. Express
Captures failed logins Yes (text only) Yes, structured Only if the trigger itself checks and rejects Yes, via error_reported
Captures DB create/drop No Yes (DATABASE_CHANGE_GROUP) Yes (server DDL trigger) Yes, but always instance-wide (can't scope by DB name)
Captures schema/DDL changes No Yes (SCHEMA_OBJECT_CHANGE_GROUP) Yes (database DDL trigger) Yes (object_created/altered/deleted)
Can block the action, not just log it No No (FAIL_OPERATION blocks on write failure, not on content) Yes No
Built-in retention/query tooling Log rotation only Yes (fn_get_audit_file, MAX_ROLLOVER_FILES) None, you build it Event file rotation, no reader convenience
Misconfiguration risk Low (just a logging gap) SHUTDOWN can take the instance offline A broken logon trigger can lock out all logins Low, mainly a silent logging gap

sqmSQLTool: What's Covered, What Isn't

Worth being precise here, because the word "audit" already means two different things in this module:

Invoke-sqmLoginAudit audits logins as objects, not login attempts It checks password policy, password age, inactivity, orphaned SIDs, exactly the kind of posture review a security team asks for on the login catalog itself. It has nothing to do with the "who connected, when, success or failure" question this article covers, that's Get-sqmErrorLog (Method 1) or SQL Server Audit's login groups (Method 2). Same word, genuinely different question.
What's actually wrapped today: Get-sqmErrorLog -FailedLogins -SuccessfulLogins for Method 1. Register-sqmAuditSession for Method 4, set up and read back the exact categories this article covers without hand-writing the Extended Events DDL. Nothing yet wraps SQL Server Audit configuration/reading or DDL/logon trigger deployment directly, those are still run as plain T-SQL, same as the examples above. Full reference: sqmSQLTool commands.

Practical Guidance

Questions People Actually Ask

Q: What is the difference between SQL Server Audit and a DDL trigger? SQL Server Audit is passive and purpose-built for logging: it records action groups to a durable target with built-in read tooling and, optionally, tamper-resistant configuration. A DDL trigger runs inline with the transaction and can actively reject or roll back the operation it is watching, something Audit cannot do, at the cost of every event now depending on that trigger's own reliability.
Q: Can I audit logins and database changes without SQL Server Enterprise Edition? Yes. SQL Server Audit has been available in Standard Edition since SQL Server 2016 SP1, not just Enterprise. On older Standard Edition instances, DDL triggers, logon triggers, and targeted Extended Events sessions all work on every edition, including Express.
Q: What happens if the SQL Server Audit destination becomes unavailable? It depends entirely on the audit's ON_FAILURE setting. CONTINUE lets the instance keep running and silently stops auditing. FAIL_OPERATION blocks the specific action being audited until the destination is reachable again. SHUTDOWN stops the SQL Server service outright rather than let an audited action go unrecorded, which is the strictest compliance posture and also a genuine, self-inflicted outage risk if the audit file share fills up or a permission changes unexpectedly.
Q: Does Invoke-sqmLoginAudit audit login attempts? No, and the shared word is a common source of confusion. Invoke-sqmLoginAudit audits the logins themselves as objects: password policy, password age, inactivity, orphaned SIDs. Auditing actual login attempts (who connected, when, success or failure) is a different concern, covered by AuditLevel plus Get-sqmErrorLog, or by SQL Server Audit's login action groups.