AUTOMATESQL
Back to all guides
November 4, 2026•23 min

DBA Challenge: Deploying a SQL Server Audit Across Every Instance

1 Server vs. 50

If Security came to you today and said every SQL Server instance needs a baseline audit capturing failed logins, successful logins, and server role changes before the next audit, how would you roll it out?

Doing it on one server in SSMS takes about five minutes.

Doing it across 50 servers, where some servers are brand new and others already have an older audit running with different settings, is where things get interesting.

In this challenge, you'll implement the baseline audit on one instance first, and then come up with a plan to roll it out across all of your instances. Your solution doesn't have to look like mine; there are plenty of ways to solve this challenge.

What You Need to Run This Challenge

To run this scenario hands-on, you only need a non-production sandbox with:

  • At least one SQL Server instance on Windows Server (SQL Server 2016 or newer, Developer or Evaluation Edition) where you have local Windows Administrator and SQL Server sysadmin rights. Having a second instance is helpful if you want to test rolling your solution out across multiple servers.
  • SSMS and PowerShell for Step 1, plus your preferred automation tool for Step 2 (in the walkthrough below, I use an Ansible control node with the ansible.windows collection connected to Windows hosts over WinRM or OpenSSH).

Throughout the walkthrough, I use SQL1 (sql1.sandbox.local) and SQL2 (sql2.sandbox.local) built on my Enterprise Sandbox Engine Windows Server and Active Directory lab, with SQL Server installed on both hosts and AC1 (an Ubuntu Ansible control node) added for Step 2. If your test server uses a different non-system drive letter than T:, a named instance, or a domain service account instead of NT SERVICE\MSSQLSERVER, simply adjust the folder path and service account name to match your environment.

DBA Challenge: Deploying a SQL Server Audit

Step 1 — Implement It on One Instance:

  1. On a single test SQL Server instance, create and enable a Server Audit and Server Audit Specification with the following requirements:
    • Server Audit (Baseline_Security_Audit):
      • Write .sqlaudit files to T:\SQLAudit\ (keeping it off C:\ is the goal).
      • Set a 100 MB max file size with 10 rollover files (1 GB total so it can never fill the volume).
      • Set ON_FAILURE = CONTINUE so an audit I/O issue never blocks production queries (we're opting for maximum availability over audit coverage).
    • Server Audit Specification (Baseline_Security_Spec):
      • Linked to Baseline_Security_Audit and enabled (STATE = ON).
      • Capture SUCCESSFUL_LOGIN_GROUP, FAILED_LOGIN_GROUP, and SERVER_ROLE_MEMBER_CHANGE_GROUP.
  2. Verify that your audit is actively recording login and role-change events by running this query against sys.fn_get_audit_file:

Step 2 — Plan the Rollout Across 50 Instances:

  1. Think about every step you just performed manually to get that audit working on one instance: creating the Windows folder on disk, making sure the SQL Server service account has permissions to write to it, creating the Server Audit and Server Audit Specification in T-SQL, and enabling both objects.
  2. Now assume you need to implement and maintain this audit across 50 instances. Some instances are brand new (with no T:\SQLAudit\ folder yet), while others already have an older version of Baseline_Security_Audit running that doesn't match the standard (for example, MAX_ROLLOVER_FILES = UNLIMITED, a disabled specification, or missing action groups).
  3. Using your preferred automation tool (PowerShell, dbatools, Ansible, or CMS), how would you detect which instances are out of compliance and enforce the standard across all 50 servers without enabling xp_cmdshell or unnecessarily restarting running audits that already meet the requirements?

Full Walkthrough: How I Solved the Challenge

Setting up a SQL Server Audit on one box in SSMS can be done in a few minutes. Converting vague audit findings into clear technical requirements usually takes the longest time. Turning that manual setup into a repeatable standard across 50 servers, however, quickly exposes the boundary between what you can do with T-SQL and Windows OS configuration.

Let's walk through implementing the audit on a single instance first, the exact SQL Server error messages and gotchas you'll run into along the way, and how to automate the entire lifecycle cleanly across every instance.


Step 1: Implementing the Audit on One Instance

When you build this audit from scratch on a single instance, you have to coordinate three separate layers: the Windows file system, the Server Audit object (where the logs go), and the Server Audit Specification object (which events get captured).

1. The Windows OS Folder and Permissions Boundary (Msg 33072 & Msg 33222)

If you open SSMS on a clean server where T:\SQLAudit\ doesn't exist yet and immediately run CREATE SERVER AUDIT, SQL Server stops you right away with Msg 33072:

SQL Server validates that the FILEPATH directory exists on disk at the moment you run CREATE SERVER AUDIT, and pure T-SQL has no supported way to create a Windows folder without enabling xp_cmdshell.

There is also a second catch at the OS layer: SQL Server only checks directory existence during CREATE SERVER AUDIT, and waits until you enable the audit with ALTER SERVER AUDIT ... WITH (STATE = ON) to test write permissions. If the folder exists on Windows, but the SQL Server service account (NT SERVICE\MSSQLSERVER for a default instance, or your domain service account) does not have NTFS Modify permissions on that folder, CREATE SERVER AUDIT succeeds and enabling it fails with Msg 33222:

If you check the SQL Server Error Log (EXEC xp_readerrorlog 0, 1, N'Audit'), you'll see OS Error 5 (Access is denied).

Note

Why STATE = ON May Succeed Without Explicit Permissions

By default, a newly created folder at the root of a Windows drive (T:\SQLAudit) inherits write permissions for BUILTIN\Users, which includes the SQL Server service account. In a hardened environment where BUILTIN\Users has been removed so standard users cannot read .sqlaudit files, enabling the audit fails with Msg 33222 unless you explicitly grant Modify permissions to the SQL Server service account.

Before running the T-SQL script on a single server, we can query the SQL Server Windows service account dynamically, create T:\SQLAudit, and grant NTFS Modify permissions in PowerShell:

2. Creating and Enabling Both the Audit and Specification

Once T:\SQLAudit\ exists and the service account has Modify permissions, we create and enable both the Server Audit and the Server Audit Specification with our three audit action groups:

Watch out for a common blind spot here: both CREATE SERVER AUDIT and CREATE SERVER AUDIT SPECIFICATION default to STATE = OFF if you omit WITH (STATE = ON).

If someone enables the Server Audit (STATE = ON) but forgets to enable the Server Audit Specification, SQL Server still creates the .sqlaudit file in T:\SQLAudit\ and writes the initial audit session startup row (action_id = 'AU'). Looking at Windows Explorer or Security → Audits in SSMS makes the audit look healthy, even though it is capturing zero login or role-change events.

You can verify that both objects are enabled and inspect their exact settings at any time with this catalog query:

3. Why Updating Existing Audits Is Tricky (Msg 33071, Msg 33229, & Msg 33230)

When you start thinking about running your T-SQL script across 50 existing instances, you also have to account for servers where an audit or specification already exists but needs to be updated:

  • Msg 33071 ("This command requires Audit to be disabled. Disable the Audit and rerun this command."): Triggered if you run ALTER SERVER AUDIT (for example, to change MAX_ROLLOVER_FILES from UNLIMITED to 10) while the Server Audit is currently enabled.
  • Msg 33229 ("Changes to an audit specification must be done while the audit specification is disabled."): Triggered if you run ALTER SERVER AUDIT SPECIFICATION to add or remove action groups while the specification is enabled.
  • Msg 33230 ("An audit specification for audit 'Baseline_Security_Audit' already exists."): Triggered if you try to create a second Server Audit Specification pointing to the same Server Audit.

To modify an existing audit or specification in T-SQL, you must first set WITH (STATE = OFF), apply your changes, and then set WITH (STATE = ON) again:


Step 2: Why Pure T-SQL Breaks Across 50 Servers (And How Ansible Fixes It)

Look back at everything we just did on a single server. To roll this out and keep it enforced across 50 instances, our automation has to handle three steps cleanly:

  1. Create a Windows directory (T:\SQLAudit) on hosts where it doesn't exist (avoiding Msg 33072).
  2. Discover the Windows service account running SQL Server on each host and grant it NTFS Modify permissions on that directory (avoiding Msg 33222).
  3. Inspect sys.server_file_audits and sys.server_audit_specifications inside SQL Server, compare them against our baseline, and only cycle STATE = OFF → ALTER → STATE = ON on servers that actually need changes (avoiding Msg 33071 and Msg 33229 while leaving compliant audits untouched).

To keep the YAML playbook clean and avoid indenting T-SQL here-strings inside YAML, we store the PowerShell logic in a standalone script (playbooks/files/Set-SqlBaselineAudit.ps1) and load it into memory on the control node using lookup('ansible.builtin.file', ...):

Tip

Handling Named Instances and Per-Server Service Accounts

Instead of hardcoding a service account, ansible.windows.win_service_info queries the Windows Service Control Manager on each host (even during --check mode) and passes sql_service.services[0].username directly into win_acl, whether that server runs under NT SERVICE\MSSQLSERVER, a domain service account, or a gMSA. In production, you can move the vars: block into group_vars/database_servers.yml and override sql_instance_name or sql_audit_dir for specific servers in host_vars/.

And here is playbooks/files/Set-SqlBaselineAudit.ps1, which follows the four state-aware win_powershell rules from Writing Idempotent PowerShell for SQL Server in Ansible (SupportsShouldProcess, $PSCmdlet.ShouldProcess(), $Ansible.Changed, $Ansible.Diff, and $Ansible.Result—click Expand to inspect or Copy to grab the full script):

First, we can run the playbook in --check (Dry Run) mode from AC1 across SQL1 (which we already configured in Step 1) and SQL2 (which hasn't been configured yet). Notice how SQL1 comes back changed=0 while SQL2 is flagged for remediation:

Then, when we run the playbook to remediate SQL2 and immediately run it a second time to verify compliance across both servers, every task comes back green (changed=0):

Prefer to watch the walkthrough? Watch it on YouTube.

Share this article:
The Database Platform Automation Roadmap

The Database Platform Automation Roadmap

Move from checklists and scattered scripts to desired-state Ansible playbooks—so you can stop wondering whether production still matches what you built. Includes the 60-second self-audit and 23-page field guide (PDF).