MSSQL Hardening
SQL Without Sleep Deprivation
In network security, structure beats improvisation: clear paths, fewer privileges, and explicit trust boundaries.
With MSSQL Hardening, it's about strict input boundaries, parameterized queries, and reviews that stop query risk early.
This way you limit not only the chance of incidents, but especially the scope and duration when something goes wrong.
Immediate measures (15 minutes)
Why this matters
The core of MSSQL Hardening is risk reduction in practice. Technical context supports the choice of measures, but implementation and assurance are central.
Installation & configuration
A SQL Server installation starts with choices that determine the rest of the security story. Most organizations click through the wizard as if it were a EULA — nobody reads it, everyone clicks "Next".
Minimal installation
Install only the features that are actually needed. The default "select all" approach yields an attack surface that is larger than necessary:
Features you probably DON'T need:
- SQL Server Replication (unless you actually use it)
- Full-Text and Semantic Extractions for Search
- Analysis Services
- Reporting Services
- Integration Services
- Machine Learning Services (R/Python)
- PolyBase Query Service for External Data
- Data Quality Services
Every feature that is installed but not used is a potential attack surface that needs patching but is monitored by no one.
Named instances and ports
Use named instances instead of the default instance, and change the default TCP port:
-- Check the current port configuration
SELECT local_tcp_port
FROM sys.dm_exec_connections
WHERE session_id = @@SPID;# Change TCP port via SQL Server Configuration Manager
# Or via registry (caution: this requires a restart)
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQLServer\SuperSocketNetLib\Tcp\IPAll" `
-Name "TcpPort" -Value "41433"
# Disable SQL Browser service if you don't discover named instances
Set-Service -Name "SQLBrowser" -StartupType Disabled
Stop-Service -Name "SQLBrowser"Default port 1433 is the first thing every scanner checks. Changing it is not a security measure in itself — security through obscurity is not security — but it does filter out the background noise of automated scanners.
Patch management
# Check the current version and patch level
SELECT @@VERSION;
SELECT SERVERPROPERTY('ProductVersion') AS Version,
SERVERPROPERTY('ProductLevel') AS PatchLevel,
SERVERPROPERTY('Edition') AS Edition;Always keep SQL Server up to date with the latest Cumulative Updates. Microsoft releases monthly CUs. Many organizations dutifully patch their Windows servers but forget SQL Server — as if the database engine were magically immune to vulnerabilities.
Authentication
Windows Authentication vs Mixed Mode
SQL Server offers two authentication modes:
| Mode | Description | Recommended? |
|---|---|---|
| Windows Authentication Mode | Windows/AD accounts only | Yes |
| Mixed Mode | Windows + SQL Server logins | Only if there is no alternative |
Windows Authentication is always preferred: - Kerberos authentication with ticket-based tokens - No passwords over the network - Central password policies via Active Directory - Account lockout and audit via existing AD infrastructure
Mixed Mode is sometimes necessary for legacy applications that expect SQL logins. Document why Mixed Mode is needed and limit the number of SQL logins to the absolute minimum.
The sa account
The sa account is the holy grail for every attacker.
Present by default, always sysadmin, and in many organizations
configured with the password "sa", "P@ssw0rd", or simply blank.
-- Step 1: Rename the sa account
ALTER LOGIN sa WITH NAME = [sql_disabled_admin];
-- Step 2: Set a strong password (minimum 30 characters)
ALTER LOGIN [sql_disabled_admin] WITH PASSWORD = 'HIER-EEN-LANG-RANDOM-WACHTWOORD-VAN-30+-TEKENS!';
-- Step 3: Disable the account
ALTER LOGIN [sql_disabled_admin] DISABLE;
-- Verify that sa is disabled
SELECT name, is_disabled, is_policy_checked, is_expiration_checked
FROM sys.sql_logins
WHERE principal_id = 1;Password policy enforcement
For SQL logins that are needed, enforce password policy:
-- Create a login with password policy enforcement
CREATE LOGIN [app_service]
WITH PASSWORD = 'SterkWachtwoord!ComplexGenoeg123',
CHECK_POLICY = ON,
CHECK_EXPIRATION = ON;
-- Check existing logins for policy status
SELECT name,
is_policy_checked,
is_expiration_checked,
create_date,
modify_date
FROM sys.sql_logins
WHERE is_policy_checked = 0 OR is_expiration_checked = 0;CHECK_POLICY = ON enforces the Windows password policy on
SQL logins. CHECK_EXPIRATION = ON ensures that passwords
expire. Both are enabled by default for new logins, but are regularly
disabled "because the application can no longer connect otherwise."
Authorization & least privilege
Service accounts
The SQL Server service itself runs under an account. Use the
following for this: - A Group Managed Service Account (gMSA) —
automatic password rotation, no human intervention - Or a
dedicated domain service account with minimal rights -
Never: LocalSystem, NT AUTHORITY\SYSTEM, or a
domain admin account
Database-level vs server-level roles
| Level | Role | Usage |
|---|---|---|
| Server | sysadmin | Never assign unless absolutely necessary |
| Server | securityadmin | Management of logins and permissions |
| Server | serveradmin | Server-wide configuration |
| Server | dbcreator | Creating databases |
| Database | db_owner | Full control over a database |
| Database | db_datareader | SELECT only on all tables |
| Database | db_datawriter | INSERT, UPDATE, DELETE on all tables |
| Database | db_executor | EXECUTE on all stored procedures |
Least privilege in practice
-- Step 1: Create a server login
CREATE LOGIN [app_reader] WITH PASSWORD = 'SterkWachtwoord!456';
-- Step 2: Create a database user
USE [ProductieDB];
CREATE USER [app_reader] FOR LOGIN [app_reader];
-- Step 3: Assign ONLY the minimal permissions
-- NOT: ALTER ROLE db_datareader ADD MEMBER [app_reader];
-- INSTEAD: granular permissions at schema level
GRANT SELECT ON SCHEMA::dbo TO [app_reader];
-- Or even more specific: only certain tables
GRANT SELECT ON dbo.Products TO [app_reader];
GRANT SELECT ON dbo.Categories TO [app_reader];
-- Explicitly deny what is not allowed
DENY SELECT ON dbo.AuditLog TO [app_reader];
DENY SELECT ON dbo.UserCredentials TO [app_reader];
-- Verify effective permissions
EXECUTE AS USER = 'app_reader';
SELECT * FROM fn_my_permissions(NULL, 'DATABASE');
REVERT;GRANT, DENY, REVOKE
The order of priority: DENY always wins over
GRANT. Use DENY deliberately and sparingly:
-- REVOKE removes a previously granted permission
REVOKE EXECUTE ON dbo.sp_GetSensitiveData FROM [app_user];
-- DENY explicitly blocks, even if another role has GRANT
DENY VIEW DEFINITION ON SCHEMA::dbo TO [app_user];Disabling dangerous features
This is where most SQL Server installations fail. Features that are enabled "for convenience" and that give an attacker direct code execution on the operating system.
xp_cmdshell
The most famous one. Executes arbitrary OS commands from within SQL Server. There is no legitimate reason to have this enabled in production.
-- Check the current status
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'xp_cmdshell';
-- Disable
EXEC sp_configure 'xp_cmdshell', 0;
RECONFIGURE;
-- Verify
EXEC sp_configure 'xp_cmdshell';
-- run_value should be 0OLE Automation
sp_OACreate, sp_OAMethod, and related
procedures provide an alternative route to OS command execution:
CLR Integration
Allows loading and executing .NET assemblies in SQL Server. Legitimate for complex stored procedures, but also an attack vector:
-- Disable if not needed
EXEC sp_configure 'clr enabled', 0;
RECONFIGURE;
-- If CLR is needed: restrict to SAFE assemblies
-- and use CLR strict security (SQL 2017+)
EXEC sp_configure 'clr strict security', 1;
RECONFIGURE;Ad Hoc Distributed Queries
Enables OPENROWSET and OPENDATASOURCE — convenient for attackers to exfiltrate data:
Database Mail
If Database Mail is needed, secure it:
-- Restrict who can use Database Mail
-- Remove unnecessary users from the DatabaseMailUserRole
USE msdb;
EXEC sp_droprolemember 'DatabaseMailUserRole', 'onnodige_user';
-- Check current members
SELECT dp.name
FROM sys.database_role_members drm
JOIN sys.database_principals dp ON drm.member_principal_id = dp.principal_id
JOIN sys.database_principals dr ON drm.role_principal_id = dr.principal_id
WHERE dr.name = 'DatabaseMailUserRole';Dangerous features overview
| Feature | sp_configure option | Default | Recommended | Risk |
|---|---|---|---|---|
| xp_cmdshell | xp_cmdshell | 0 (off) | 0 (off) | OS command execution |
| OLE Automation | Ole Automation Procedures | 0 (off) | 0 (off) | OS command execution |
| CLR | clr enabled | 0 (off) | 0 (off) | Arbitrary code execution |
| Ad Hoc Queries | Ad Hoc Distributed Queries | 0 (off) | 0 (off) | Data exfiltration |
| Remote Admin | remote admin connections | 0 (off) | 0 (off) | Remote DAC access |
| External Scripts | external scripts enabled | 0 (off) | 0 (off) | Python/R execution |
Securing Linked Servers
Linked servers are one of the most underestimated risks in SQL Server environments. They create trust relationships between servers that are rarely audited.
-- Show all configured linked servers
EXEC sp_linkedservers;
-- Detailed information about linked server logins
SELECT s.name AS LinkedServer,
s.data_source,
s.provider,
ll.uses_self_credential,
ll.remote_name
FROM sys.servers s
LEFT JOIN sys.linked_logins ll ON s.server_id = ll.server_id
WHERE s.is_linked = 1;Best practices for linked servers
- Use dedicated accounts — never sa or sysadmin members as remote login
- Minimal permissions — the remote login only needs the permissions required by the query
- Remove unused linked servers — if nobody knows why a linked server exists, it probably isn't needed
- Restrict RPC — disable RPC on linked servers where it is not needed
-- Restrict a linked server: disable RPC
EXEC sp_serveroption @server = 'LINKED_SRV01', @optname = 'rpc out', @optvalue = 'false';
-- Change the login mapping to a dedicated account
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'LINKED_SRV01',
@useself = 'false',
@locallogin = NULL,
@rmtuser = 'linked_readonly',
@rmtpassword = 'SterkWachtwoord!789';The danger of linked servers is the chain reaction: Server A trusts Server B, Server B trusts Server C. Compromise A and you have a path to C. Linked server chains are a favorite technique for lateral movement in database environments.
Network isolation
Firewall rules
SQL Server should not be directly reachable from the internet. Period.
# Windows Firewall: allow only specific subnets
New-NetFirewallRule -DisplayName "SQL Server - App Servers" `
-Direction Inbound -Protocol TCP -LocalPort 41433 `
-RemoteAddress "10.1.2.0/24" -Action Allow
New-NetFirewallRule -DisplayName "SQL Server - Management" `
-Direction Inbound -Protocol TCP -LocalPort 41433 `
-RemoteAddress "10.1.0.0/24" -Action Allow
# Block all other SQL traffic
New-NetFirewallRule -DisplayName "SQL Server - Block All Other" `
-Direction Inbound -Protocol TCP -LocalPort 41433 `
-Action BlockEnforcing TLS/SSL
# SQL Server Configuration Manager > SQL Server Network Configuration
# > Protocols > Properties > Flags tab
# Set "Force Encryption" to "Yes"
# Or via registry
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQLServer\SuperSocketNetLib" `
-Name "ForceEncryption" -Value 1Install a valid TLS certificate (not the self-generated certificate that SQL Server creates by default). Use at minimum TLS 1.2:
# Disable older TLS versions at OS level
# Disable TLS 1.0
New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" -Force
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" `
-Name "Enabled" -Value 0Audit & logging
SQL Server Audit
SQL Server has a built-in audit framework that provides granular logging:
-- Step 1: Create a Server Audit
CREATE SERVER AUDIT [SecurityAudit]
TO FILE (
FILEPATH = 'D:\SQLAudit\',
MAXSIZE = 256 MB,
MAX_ROLLOVER_FILES = 20,
RESERVE_DISK_SPACE = OFF
)
WITH (
QUEUE_DELAY = 1000,
ON_FAILURE = CONTINUE
);
-- Step 2: Enable the audit
ALTER SERVER AUDIT [SecurityAudit] WITH (STATE = ON);
-- Step 3: Create a Server Audit Specification
CREATE SERVER AUDIT SPECIFICATION [LoginAuditSpec]
FOR SERVER AUDIT [SecurityAudit]
ADD (FAILED_LOGIN_GROUP),
ADD (SUCCESSFUL_LOGIN_GROUP),
ADD (LOGIN_CHANGE_PASSWORD_GROUP),
ADD (SERVER_ROLE_MEMBER_CHANGE_GROUP),
ADD (DATABASE_ROLE_MEMBER_CHANGE_GROUP),
ADD (AUDIT_CHANGE_GROUP);
ALTER SERVER AUDIT SPECIFICATION [LoginAuditSpec] WITH (STATE = ON);
-- Step 4: Database-level audit (per database)
USE [ProductieDB];
CREATE DATABASE AUDIT SPECIFICATION [DataAccessAudit]
FOR SERVER AUDIT [SecurityAudit]
ADD (SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo BY public),
ADD (EXECUTE ON SCHEMA::dbo BY public);
ALTER DATABASE AUDIT SPECIFICATION [DataAccessAudit] WITH (STATE = ON);Login auditing
At minimum, failed logins should be logged. Preferably successful logins as well:
-- Set login auditing (0=none, 1=failed, 2=success, 3=both)
EXEC xp_instance_regwrite
N'HKEY_LOCAL_MACHINE',
N'Software\Microsoft\MSSQLServer\MSSQLServer',
N'AuditLevel',
REG_DWORD, 3;Key events to monitor
| Event | What it means | Priority |
|---|---|---|
| Failed logins (bulk) | Brute-force attempt | High |
| sa login successful | Someone is using sa | Critical |
| xp_cmdshell re-enabled | Feature is being turned back on | Critical |
| New sysadmin member | Privilege escalation | Critical |
| Linked server created | Potential lateral movement | High |
| Backup to unknown location | Possible data exfiltration | High |
| DBCC commands | Possible reconnaissance | Medium |
Backup security
Backups contain the same data as the production database. If the backup is not secured, the data is not secured.
Backup encryption
-- Step 1: Create a Database Master Key
USE master;
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'MasterKeyWachtwoord!Complex789';
-- Step 2: Create a certificate for backup encryption
CREATE CERTIFICATE BackupEncryptCert
WITH SUBJECT = 'Database Backup Encryption Certificate',
EXPIRY_DATE = '2030-12-31';
-- Step 3: Create an encrypted backup
BACKUP DATABASE [ProductieDB]
TO DISK = 'D:\Backups\ProductieDB_encrypted.bak'
WITH COMPRESSION,
ENCRYPTION (
ALGORITHM = AES_256,
SERVER CERTIFICATE = BackupEncryptCert
);
-- IMPORTANT: Back up the certificate itself as well!
BACKUP CERTIFICATE BackupEncryptCert
TO FILE = 'D:\CertBackup\BackupEncryptCert.cer'
WITH PRIVATE KEY (
FILE = 'D:\CertBackup\BackupEncryptCert.pvk',
ENCRYPTION BY PASSWORD = 'CertBackupWachtwoord!456'
);Transparent Data Encryption (TDE)
TDE encrypts the database files on disk, including tempdb:
-- Step 1: Master Key (if it doesn't exist yet)
USE master;
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'TDEMasterKey!Complex123';
-- Step 2: Certificate
CREATE CERTIFICATE TDECert WITH SUBJECT = 'TDE Certificate';
-- Step 3: Database Encryption Key
USE [ProductieDB];
CREATE DATABASE ENCRYPTION KEY
WITH ALGORITHM = AES_256
ENCRYPTION BY SERVER CERTIFICATE TDECert;
-- Step 4: Enable TDE
ALTER DATABASE [ProductieDB] SET ENCRYPTION ON;
-- Check the status
SELECT db.name, db.is_encrypted,
dek.encryption_state, dek.key_algorithm, dek.key_length
FROM sys.databases db
LEFT JOIN sys.dm_database_encryption_keys dek
ON db.database_id = dek.database_id;NTFS permissions on backup files
# Restrict access to the backup directory
$acl = Get-Acl "D:\Backups"
$acl.SetAccessRuleProtection($true, $false)
# Only SQL Server service account and backup operators
$rule1 = New-Object System.Security.AccessControl.FileSystemAccessRule(
"NT SERVICE\MSSQLSERVER", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
$rule2 = New-Object System.Security.AccessControl.FileSystemAccessRule(
"DOMAIN\SQL-BackupOperators", "Read,Write", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.AddAccessRule($rule1)
$acl.AddAccessRule($rule2)
Set-Acl "D:\Backups" $aclCommon mistakes
| # | Mistake | Risk | Solution |
|---|---|---|---|
| 1 | sa account with weak password | Full server compromise | Disable, rename, strong password |
| 2 | Mixed Mode without necessity | Larger attack surface | Windows Authentication Only |
| 3 | xp_cmdshell enabled | OS command execution | Disable via sp_configure |
| 4 | SQL Server as LocalSystem | Service runs with maximum privileges | Dedicated (g)MSA with minimal rights |
| 5 | Port 1433 open to the internet | Directly reachable by attackers | Firewall, network segmentation |
| 6 | No TLS/SSL | Credentials in plaintext over network | Force Encryption + valid certificate |
| 7 | Linked servers with sa credentials | Lateral movement between servers | Dedicated accounts, minimal rights |
| 8 | Unencrypted backups on network share | Data leakage via backup files | Backup encryption (AES-256) |
| 9 | No audit logging | No visibility during incident | SQL Server Audit + SIEM integration |
| 10 | Application accounts with sysadmin | Any SQLi = full compromise | Least privilege, schema-level permissions |
| 11 | CLR without strict security | Arbitrary .NET code execution | Enable clr strict security |
| 12 | No patch management | Known CVEs exploitable | Plan monthly CU installations |
Checklist
| Priority | Measure | Status |
|---|---|---|
| Critical | sa account disabled or renamed with strong password | [ ] |
| Critical | xp_cmdshell disabled | [ ] |
| Critical | OLE Automation disabled | [ ] |
| Critical | Windows Authentication Mode (or Mixed Mode with documentation) | [ ] |
| Critical | SQL Server not reachable from internet | [ ] |
| Critical | Latest Cumulative Update installed | [ ] |
| High | TLS/SSL Force Encryption enabled | [ ] |
| High | Dedicated service account (gMSA) for SQL Server service | [ ] |
| High | Login auditing enabled (failed + successful) | [ ] |
| High | Backup encryption (AES-256) | [ ] |
| High | Linked servers audited and secured | [ ] |
| High | Application accounts with minimal rights (no sysadmin) | [ ] |
| High | CHECK_POLICY and CHECK_EXPIRATION on all SQL logins | [ ] |
| Medium | CLR disabled or strict security enabled | [ ] |
| Medium | Ad Hoc Distributed Queries disabled | [ ] |
| Medium | TDE enabled for sensitive databases | [ ] |
| Medium | SQL Server Audit with SIEM integration | [ ] |
| Medium | TCP port changed (not 1433) | [ ] |
| Medium | SQL Browser service disabled | [ ] |
| Medium | Database Mail restricted to authorized users | [ ] |
| Low | Named instances in use | [ ] |
| Low | NTFS permissions on backup files verified | [ ] |
| Low | Certificates for TDE/backup encryption securely stored | [ ] |
There is a specific type of DBA who manages SQL Server in most organizations. You recognize him by the following characteristics: he performed the installation by clicking "Next" fifteen times, selected Mixed Mode "just in case," and set the sa password to something he can remember — which in practice means: "sa", "Password1", or the company name with an exclamation mark at the end.
xp_cmdshell is enabled. Not because anyone needs it, but because there was once, in 2014, an application that needed it. That application was decommissioned three years ago. The xp_cmdshell configuration has remained, like a kind of digital fossil.
The linked servers are the most beautiful part. Server A trusts Server B with the sa account. Server B trusts Server C with the sa account. Server C trusts Server D with — you guessed it — the sa account. Nobody knows why this chain exists. Nobody dares to change anything. "It works, and if I change something production will break." The DBA is not incompetent. He is pragmatic. And pragmatic means in IT: "it works, so we don't touch it."
The result is an environment where a single SQL injection in a web application doesn't just lead to reading a table, but to full OS command execution via xp_cmdshell, followed by lateral movement via linked servers to three other database servers, followed by credential harvesting, followed by domain admin. All because someone once said "the application needs it" and nobody ever verified it.
But hey, the backups are running. Unencrypted, on a network share that everyone can access. But they're running.
Summary
MSSQL hardening revolves around a limited number of fundamental measures that together make the difference between a database server and an open door to your entire network. Disable dangerous features (xp_cmdshell, OLE Automation, CLR), secure authentication (Windows Auth, disable sa, CHECK_POLICY), apply least privilege at every level (server roles, database roles, schema permissions), isolate the server at network level (firewall, TLS, no direct internet access), audit everything (SQL Server Audit, login logging, SIEM integration), and secure your backups (encryption, NTFS permissions, TDE). None of these measures is revolutionary. They are all documented, all available in the standard SQL Server installation, and all disabled or not configured by default. The only thing needed is someone who takes the time to enable them.