SQL Server running as local system and not a domain or local user
What this check looks for
The account the SQL Server service runs under, read from the service configuration. The finding is raised when that account is LocalSystem. The check is skipped on Amazon RDS, where the service account is managed by the platform.
Why it matters
LocalSystem is the most privileged account on a Windows machine. It is not an administrator account; it is above one.
Specifically, LocalSystem:
- Has full control of the entire operating system, including every file, every registry key and every service, regardless of the ACLs that would stop an administrator.
- Acts as the computer account on the network. Outbound connections authenticate as
DOMAIN\MACHINENAME$, so whatever the computer object has been granted in Active Directory, SQL Server has. - Cannot be restricted meaningfully. There is no way to reduce what it can do while leaving the service working.
Why that matters for a database engine specifically: SQL Server has several features that execute code or touch the file system as the service account. xp_cmdshell, unsafe CLR assemblies, BULK INSERT, BACKUP to a path, extended stored procedures, and the Agent subsystems all run in that context. So the practical statement is:
Anybody who reaches
sysadminon this instance has complete control of the Windows server, and network identity as the machine itself.
That converts a SQL injection flaw or a compromised sa password from a database incident into a server incident, and on a domain joined machine, potentially into a lateral movement path.
The reverse problem is real too, and it is the one people notice first. Because LocalSystem presents as the computer account on the network, it usually cannot reach a file share or a linked server the way a domain user would, so backups to a UNC path fail with access denied and linked servers fail to delegate. People then grant the computer account share permissions, which quietly widens the problem.
What to use instead has changed over the years, and the modern answer is simpler than the old one:
| Option | When |
|---|---|
Virtual account (NT SERVICE\MSSQLSERVER) |
The default and best choice for a standalone instance with no network resource access. No password, no management, correctly scoped by the installer. |
Managed Service Account (DOMAIN\sqlsvc$) |
When network resources are needed. The domain manages the password. |
| Domain user account | When network resources are needed and gMSA is unavailable. Needs password management. |
| Local user account | A workgroup machine with no network resource needs. |
| LocalSystem | Never, deliberately. |
| LocalService / NetworkService | Not supported for SQL Server. |
The virtual account is the important one to know about, because it removes the usual objection. It needs no password, it is created and granted correctly by the SQL Server installer, and it is scoped to that service. On a standalone instance that does not reach out to file shares, it is both more secure and less work than what you have.
How to confirm it yourself
The service account, from SQL Server itself:
SELECT [servicename],
[service_account],
[startup_type_desc],
[status_desc],
[last_startup_time],
[is_clustered],
[instant_file_initialization_enabled]
FROM sys.dm_server_services;
That covers the engine, Agent and full text services in one result. LocalSystem in service_account is the finding.
From the registry, which works on older builds without that DMV:
DECLARE @serviceAccount NVARCHAR(256);
EXEC master.dbo.xp_instance_regread
N'HKEY_LOCAL_MACHINE',
N'SYSTEM\CurrentControlSet\Services\MSSQLSERVER',
N'ObjectName', @serviceAccount OUTPUT;
SELECT @serviceAccount AS [service_account];
Check Agent as well, since it is frequently overlooked and has the same exposure through its job subsystems.
What is actually exercising the privilege, which sets the urgency:
SELECT [name], [value_in_use]
FROM sys.configurations WITH (NOLOCK)
WHERE [name] IN ('xp_cmdshell', 'clr enabled', 'Ole Automation Procedures',
'Ad Hoc Distributed Queries', 'clr strict security');
xp_cmdshell enabled alongside LocalSystem is the combination that turns a sysadmin into a machine administrator in one statement.
Unsafe assemblies, which have the same reach:
EXEC sp_MSforeachdb N'
USE [?];
IF DB_ID() > 4
SELECT DB_NAME() AS [database_name], [name], [permission_set_desc]
FROM sys.assemblies WITH (NOLOCK)
WHERE [is_user_defined] = 1;';
And who holds sysadmin, since that is the set of people this privilege is actually delegated to:
SELECT sp.[name] AS [member], sp.[type_desc], sp.[is_disabled]
FROM sys.server_role_members AS rm WITH (NOLOCK)
INNER JOIN sys.server_principals AS r WITH (NOLOCK) ON r.[principal_id] = rm.[role_principal_id]
INNER JOIN sys.server_principals AS sp WITH (NOLOCK) ON sp.[principal_id] = rm.[member_principal_id]
WHERE r.[name] = 'sysadmin';
How to fix it
Change the service account using SQL Server Configuration Manager. Do not use the Services applet in Windows, and do not change it in the registry.
Configuration Manager does several things the other routes do not: it grants the new account the file system permissions on the data, log and backup folders, the registry permissions, the required local security policy rights, and it re-protects the service master key. Changing the account any other way leaves SQL Server unable to start, and diagnosing that during an outage is avoidable.
- Choose the account. For a standalone instance with no network resource access, use the virtual account
NT SERVICE\MSSQLSERVER(orNT SERVICE\MSSQL$INSTANCENAMEfor a named instance). For network access, use a gMSA or a domain user. - Inventory what the service currently reaches, before changing anything. This is the step that prevents the outage:
- Backup destinations, especially UNC paths.
- Linked servers and their authentication.
- Any folder used by
BULK INSERTorOPENROWSET. - Agent job steps that touch the file system, including PowerShell and CmdExec steps.
- Database Mail, replication shares, and any SSIS package file location.
-- where backups have been going
SELECT DISTINCT [physical_device_name]
FROM msdb.dbo.backupmediafamily
ORDER BY [physical_device_name];
-- linked servers to check
SELECT [name], [product], [provider], [data_source] FROM sys.servers WHERE [is_linked] = 1;
- Grant the new account what it needs on those paths, before the change.
- Make the change in Configuration Manager and restart the service. This is the downtime, and it is a service restart, typically a couple of minutes.
- Confirm the local rights are present. Configuration Manager grants them, and they are worth knowing: Log on as a service, Lock pages in memory if you use it, and Perform volume maintenance tasks for instant file initialization. That last one is easy to lose in the move:
SELECT [servicename], [instant_file_initialization_enabled]
FROM sys.dm_server_services;
- Verify afterwards: databases online, a test backup to the real destination, a linked server query, and an Agent job that touches the file system.
Change Agent’s account at the same time, in the same window.
If SQL Server Reporting Services is involved, back up the encryption key first. Changing the account that Reporting Services runs as invalidates its access to the encrypted content in the report server database. Back the key up before the change and restore it afterwards, or the stored data sources and subscriptions become unreadable.
Then reduce what the account can do, because the account change is only half of it:
-- turn off what is not used
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 0;
RECONFIGURE;
EXEC sp_configure 'Ole Automation Procedures', 0;
RECONFIGURE;
And review sysadmin membership, since the service account privilege is only reachable through it.
How long it takes
About an hour, most of it the inventory of what the service reaches. The change itself plus the restart is a short maintenance window.
Related reports
| Report | Why you would go there |
|---|---|
| Security Posture | The whole security surface, including the service account. |
| Server Overview | Service accounts and startup state. |
| Logins | Who can reach sysadmin and therefore this privilege. |
| Linked Servers | Connections that depend on the account identity. |
| Job Commands | Agent steps that touch the file system. |
| Configuration Values | xp_cmdshell and CLR settings. |
Related checks
| Check | |
|---|---|
| xp_cmdshell is enabled | The feature that makes this privilege directly usable. |
| Database connections as sa | The same instinct applied to the login. |
| User defined assemblies found | CLR code running in the service context. |
| SQL Agent running as local system | The same finding for the Agent service. |
| Too many sysadmins | Who this privilege is effectively delegated to. |
Frequently asked questions
What should we use instead? For a standalone instance that does not reach network resources, the virtual account NT SERVICE\MSSQLSERVER. It has no password to manage and the installer scopes it correctly. For network access, a group Managed Service Account.
Our backups go to a UNC path, so do we need a domain account? Usually yes, or a gMSA. A virtual account presents as the computer account on the network, which most file shares are not configured to accept.
Can I change it in the Services applet? No. Use SQL Server Configuration Manager. The Services applet does not grant the file system, registry and policy rights, and the service will fail to start.
Is the risk real if nobody has sysadmin except us? The risk is what a compromise of any sysadmin path becomes, including a SQL injection flaw in an application connecting with high privilege. Reducing the service account limits the damage of something you have not found yet.