jan-karel.com

MSSQL Hardening

MSSQL Hardening

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 0

OLE Automation

sp_OACreate, sp_OAMethod, and related procedures provide an alternative route to OS command execution:

-- Disable
EXEC sp_configure 'Ole Automation Procedures', 0;
RECONFIGURE;

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:

EXEC sp_configure 'Ad Hoc Distributed Queries', 0;
RECONFIGURE;

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

  1. Use dedicated accounts — never sa or sysadmin members as remote login
  2. Minimal permissions — the remote login only needs the permissions required by the query
  3. Remove unused linked servers — if nobody knows why a linked server exists, it probably isn't needed
  4. 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 Block

Enforcing 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 1

Install 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 0

Audit & 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" $acl

Common 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.

Op de hoogte blijven?

Ontvang maandelijks cybersecurity-inzichten in je inbox.

← Network & Active Directory ← Home