Home / SQL / SQL Server Security Best Practices: Protecting Your Data
SQL

SQL Server Security Best Practices: Protecting Your Data

Secure SQL Server databases with authentication strategies, encryption (TDE, Always Encrypted), SQL injection prevention, auditing, row-level security, and c...

What you will learn

Practical execution with concise explanations, real implementation patterns, and production-ready recommendations.

SQL Server Security Best Practices: Protecting Your Data

CREATE LOGIN [CONTOSO\JohnDoe] FROM WINDOWS;

-- Grant access to database USE [AdventureWorks]; CREATE USER [CONTOSO\JohnDoe] FOR LOGIN [CONTOSO\JohnDoe];

-- Add to database role ALTER ROLE db_datareader ADD MEMBER [CONTOSO\JohnDoe];


**SQL Server Authentication:**

```sql
-- Create SQL login with strong password policy
CREATE LOGIN AppUser 
WITH PASSWORD = 'StrongP@ssw0rd123!',
     CHECK_POLICY = ON,
     CHECK_EXPIRATION = ON;

-- Create database user
USE [AdventureWorks];
CREATE USER AppUser FOR LOGIN AppUser;

-- Grant specific permissions
GRANT SELECT, INSERT, UPDATE ON dbo.Orders TO AppUser;

Principle of Least Privilege

-- Create custom database role
CREATE ROLE OrdersReader;

-- Grant minimal permissions
GRANT SELECT ON dbo.Orders TO OrdersReader;
GRANT SELECT ON dbo.OrderDetails TO OrdersReader;

-- Add user to role
ALTER ROLE OrdersReader ADD MEMBER AppUser;

-- Deny sensitive columns
DENY SELECT ON dbo.Customers(CreditCardNumber) TO OrdersReader;

Server-Level Permissions

-- View server-level permissions
SELECT 
```text
pr.name AS PrincipalName,
pr.type_desc AS PrincipalType,
pe.permission_name,
pe.state_desc```
FROM sys.server_principals pr
INNER JOIN sys.server_permissions pe ON pr.principal_id = pe.grantee_principal_id
WHERE pr.name = 'AppUser';

-- Grant server permissions
GRANT VIEW SERVER STATE TO AppUser;
GRANT VIEW ANY DEFINITION TO AppUser;

-- Revoke dangerous permissions
REVOKE CONTROL SERVER FROM AppUser;

Encryption

Transparent Data Encryption (TDE)

Encrypts entire database at rest:

-- Create master key in master database
USE master;
GO
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'MasterKeyP@ssw0rd123!';
GO

-- Create certificate
CREATE CERTIFICATE TDECert 
WITH SUBJECT = 'TDE Certificate for AdventureWorks';
GO

-- Backup certificate (CRITICAL - store securely!)
BACKUP CERTIFICATE TDECert
TO FILE = 'C:\Backup\TDECert.cer'
WITH PRIVATE KEY (
```text
FILE = 'C:\Backup\TDECert.pvk',
ENCRYPTION BY PASSWORD = 'CertP@ssw0rd123!'```
);
GO

-- Create database encryption key
USE AdventureWorks;
GO
CREATE DATABASE ENCRYPTION KEY
WITH ALGORITHM = AES_256
ENCRYPTION BY SERVER CERTIFICATE TDECert;
GO

-- Enable TDE
ALTER DATABASE AdventureWorks
SET ENCRYPTION ON;
GO

-- Verify encryption status
SELECT 
```text
DB_NAME(database_id) AS DatabaseName,
encryption_state,
CASE encryption_state
    WHEN 0 THEN 'No encryption'
    WHEN 1 THEN 'Unencrypted'
    WHEN 2 THEN 'Encryption in progress'
    WHEN 3 THEN 'Encrypted'
    WHEN 4 THEN 'Key change in progress'
    WHEN 5 THEN 'Decryption in progress'
END AS EncryptionStatus,
percent_complete,
encryptor_type```
FROM sys.dm_database_encryption_keys;

Always Encrypted

Encrypts specific columns, data remains encrypted in memory:

Architecture Overview: Enable Always Encrypted in SSMS

KEY_STORE_PROVIDER_NAME = 'MSSQL_CERTIFICATE_STORE', KEY_PATH = 'CurrentUser/My/A1B2C3D4E5F6789012345678901234567890ABCD'``` );

-- Create column encryption key (CEK) CREATE COLUMN ENCRYPTION KEY CEK_Auto1 WITH VALUES (

COLUMN_MASTER_KEY = CMK_Auto1,
ALGORITHM = 'RSA_OAEP',
ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F006100310062003200630033006400340065003500660036003700380039003000310032003300340035003600370038003900300031003200330034003500360037003800390030006100620063006400```
);

-- Create table with encrypted columns
CREATE TABLE dbo.Customers (
```sql
CustomerID INT PRIMARY KEY IDENTITY,
FirstName NVARCHAR(50),
LastName NVARCHAR(50),
Email NVARCHAR(100),
SSN NVARCHAR(11) COLLATE Latin1_General_BIN2 
    ENCRYPTED WITH (
        COLUMN_ENCRYPTION_KEY = CEK_Auto1,
        ENCRYPTION_TYPE = DETERMINISTIC,
        ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256'
    ),
CreditCardNumber NVARCHAR(19) COLLATE Latin1_General_BIN2
    ENCRYPTED WITH (
        COLUMN_ENCRYPTION_KEY = CEK_Auto1,
        ENCRYPTION_TYPE = RANDOMIZED,
        ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256'
    )```
);

-- Insert data (requires Always Encrypted-enabled connection)
INSERT INTO dbo.Customers (FirstName, LastName, Email, SSN, CreditCardNumber)
VALUES ('John', 'Doe', 'john.doe@contoso.com', '123-45-6789', '4111-1111-1111-1111');

Deterministic vs. Randomized:

  • Deterministic: Same plaintext = same ciphertext (supports equality, grouping)
  • Randomized: Same plaintext = different ciphertext (more secure, no operations)

Connection String Encryption

// .NET connection string with Always Encrypted
string connectionString = "Server=myserver;Database=AdventureWorks;Integrated Security=true;Column Encryption Setting=Enabled;";

using (SqlConnection connection = new SqlConnection(connectionString))
{
```sql
connection.Open();

// Queries automatically encrypt/decrypt
SqlCommand command = new SqlCommand(
    "SELECT FirstName, LastName, SSN FROM dbo.Customers WHERE SSN = @SSN",
    connection
);
command.Parameters.AddWithValue("@SSN", "123-45-6789");

SqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{
    Console.WriteLine($"{reader["FirstName"]} {reader["LastName"]}: {reader["SSN"]}");
}```
}

SQL Injection Prevention

Parameterized Queries

-- ❌ VULNERABLE - Dynamic SQL with concatenation
DECLARE @Username NVARCHAR(50) = 'admin'' OR ''1''=''1';
DECLARE @SQL NVARCHAR(MAX) = 'SELECT * FROM Users WHERE Username = ''' + @Username + '''';
EXEC sp_executesql @SQL;  -- Returns all users!

-- ✅ SECURE - Parameterized query
DECLARE @Username NVARCHAR(50) = 'admin'' OR ''1''=''1';
DECLARE @SQL NVARCHAR(MAX) = 'SELECT * FROM Users WHERE Username = @User';
EXEC sp_executesql @SQL, N'@User NVARCHAR(50)', @User = @Username;  -- Treats as literal

Input Validation

CREATE PROCEDURE GetCustomerOrders
```text
@CustomerID INT```
AS
BEGIN
```sql
-- Validate input
IF @CustomerID IS NULL OR @CustomerID <= 0
BEGIN
    RAISERROR('Invalid CustomerID', 16, 1);
    RETURN;
END

-- Safe parameterized query
SELECT 
    OrderID,
    OrderDate,
    TotalAmount
FROM dbo.Orders
WHERE CustomerID = @CustomerID;```
END;

Whitelist Validation

CREATE PROCEDURE SearchProducts
```text
@SortColumn NVARCHAR(50) = 'ProductName',
@SortDirection NVARCHAR(4) = 'ASC'```
AS
BEGIN
```sql
-- Whitelist allowed columns
IF @SortColumn NOT IN ('ProductName', 'Price', 'CategoryID')
BEGIN
    RAISERROR('Invalid sort column', 16, 1);
    RETURN;
END

-- Whitelist sort direction
IF @SortDirection NOT IN ('ASC', 'DESC')
BEGIN
    SET @SortDirection = 'ASC';
END

-- Build safe dynamic SQL
DECLARE @SQL NVARCHAR(MAX) = 
    'SELECT ProductID, ProductName, Price, CategoryID 
     FROM dbo.Products 
     ORDER BY ' + QUOTENAME(@SortColumn) + ' ' + @SortDirection;

EXEC sp_executesql @SQL;```
END;

Auditing

SQL Server Audit

-- Create server audit
USE master;
GO
CREATE SERVER AUDIT SecurityAudit
TO FILE (
```text
FILEPATH = 'C:\Audit\',
MAXSIZE = 100 MB,
MAX_ROLLOVER_FILES = 10,
RESERVE_DISK_SPACE = OFF```
)
WITH (
```text
QUEUE_DELAY = 1000,
ON_FAILURE = CONTINUE```
);
GO

-- Enable audit
ALTER SERVER AUDIT SecurityAudit
WITH (STATE = ON);
GO

-- Create server audit specification (server-level events)
CREATE SERVER AUDIT SPECIFICATION LoginAudit
FOR SERVER AUDIT SecurityAudit
ADD (FAILED_LOGIN_GROUP),
ADD (SUCCESSFUL_LOGIN_GROUP),
ADD (AUDIT_CHANGE_GROUP)
WITH (STATE = ON);
GO

-- Create database audit specification (database-level events)
USE AdventureWorks;
GO
CREATE DATABASE AUDIT SPECIFICATION SensitiveDataAccess
FOR SERVER AUDIT SecurityAudit
ADD (SELECT, UPDATE ON dbo.Customers BY public),
ADD (DELETE ON dbo.Orders BY public)
WITH (STATE = ON);
GO

-- Query audit log
SELECT 
```text
event_time,
action_id,
succeeded,
server_principal_name,
database_name,
schema_name,
object_name,
statement```
FROM sys.fn_get_audit_file('C:\Audit\*.sqlaudit', DEFAULT, DEFAULT)
WHERE event_time >= DATEADD(DAY, -7, GETDATE())
ORDER BY event_time DESC;

Change Data Capture (CDC)

-- Enable CDC on database
USE AdventureWorks;
GO
EXEC sys.sp_cdc_enable_db;
GO

-- Enable CDC on table
EXEC sys.sp_cdc_enable_table
```text
@source_schema = N'dbo',
@source_name = N'Customers',
@role_name = NULL,
@supports_net_changes = 1;```
GO

-- Query changes
DECLARE @begin_lsn BINARY(10), @end_lsn BINARY(10);

SET @begin_lsn = sys.fn_cdc_map_time_to_lsn('smallest greater than or equal', DATEADD(HOUR, -1, GETDATE()));
SET @end_lsn = sys.fn_cdc_get_max_lsn();

SELECT 
```text
__$operation AS Operation,  -- 1=Delete, 2=Insert, 3=Before Update, 4=After Update
__$start_lsn,
CustomerID,
FirstName,
LastName,
Email```
FROM cdc.fn_cdc_get_all_changes_dbo_Customers(@begin_lsn, @end_lsn, 'all')
ORDER BY __$start_lsn;

Row-Level Security (RLS)

Row-Level Security (RLS)

Figure: SQL Server security – server roles, logins, and database permissions.

-- Create security policy for multi-tenant application
CREATE SCHEMA Security;
GO





-- Create inline table-valued function
CREATE FUNCTION Security.fn_CustomerSecurityPredicate(@CustomerID INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS result
WHERE @CustomerID = CAST(SESSION_CONTEXT(N'CustomerID') AS INT)
```text
OR IS_MEMBER('db_owner') = 1;```
GO

-- Create security policy
CREATE SECURITY POLICY Security.CustomerSecurityPolicy
ADD FILTER PREDICATE Security.fn_CustomerSecurityPredicate(CustomerID) ON dbo.Orders,
ADD BLOCK PREDICATE Security.fn_CustomerSecurityPredicate(CustomerID) ON dbo.Orders AFTER INSERT
WITH (STATE = ON);
GO

-- Set session context (typically in application)
EXEC sp_set_session_context @key = N'CustomerID', @value = 1001;

-- User only sees their own orders
SELECT * FROM dbo.Orders;  -- Filtered by CustomerID = 1001

-- Bypass RLS (for administrators)
SELECT * FROM dbo.Orders WITH (SECURITY_BYPASS);

Dynamic Data Masking

-- Create table with masked columns
CREATE TABLE dbo.Employees (
```sql
EmployeeID INT PRIMARY KEY IDENTITY,
FirstName NVARCHAR(50),
LastName NVARCHAR(50),
Email NVARCHAR(100) MASKED WITH (FUNCTION = 'email()'),
SSN NVARCHAR(11) MASKED WITH (FUNCTION = 'partial(0,"XXX-XX-",4)'),
Salary DECIMAL(10,2) MASKED WITH (FUNCTION = 'default()')```
);

-- Insert data
INSERT INTO dbo.Employees (FirstName, LastName, Email, SSN, Salary)
VALUES ('John', 'Doe', 'john.doe@contoso.com', '123-45-6789', 75000.00);

-- Regular user sees masked data
-- Email: jXXX@XXXX.com
-- SSN: XXX-XX-6789
-- Salary: 0.00

-- Grant UNMASK permission
GRANT UNMASK TO AppUser;

-- Now sees actual data
SELECT * FROM dbo.Employees;

Security Best Practices

Disable sa Account

-- Disable sa account
ALTER LOGIN sa DISABLE;

-- Create dedicated admin account
CREATE LOGIN [CONTOSO\SQLAdmin] FROM WINDOWS;
ALTER SERVER ROLE sysadmin ADD MEMBER [CONTOSO\SQLAdmin];

Password Policies

-- Enforce strong password policy
ALTER LOGIN AppUser 
WITH PASSWORD = 'NewStrongP@ssw0rd123!',
     CHECK_POLICY = ON,
     CHECK_EXPIRATION = ON;

-- Review accounts with weak policies
SELECT 
```text
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;

Surface Area Reduction

-- Disable unnecessary features
EXEC sp_configure 'xp_cmdshell', 0;
RECONFIGURE;

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

-- Disable unused protocols (via SQL Server Configuration Manager)
-- Typically only enable TCP/IP, disable Named Pipes, Shared Memory (except local)

Regular Security Audits

-- Review server permissions
SELECT 
```text
pr.name AS LoginName,
pe.permission_name,
pe.state_desc```
FROM sys.server_principals pr
INNER JOIN sys.server_permissions pe ON pr.principal_id = pe.grantee_principal_id
WHERE pr.type IN ('S', 'U')  -- SQL and Windows logins
ORDER BY pr.name;

-- Review database permissions
USE AdventureWorks;
GO
SELECT 
```text
USER_NAME(grantee_principal_id) AS UserName,
OBJECT_NAME(major_id) AS ObjectName,
permission_name,
state_desc```
FROM sys.database_permissions
WHERE major_id > 0
ORDER BY UserName;

-- Identify orphaned users
SELECT 
```text
dp.name AS DatabaseUser,
dp.type_desc,
dp.create_date```
FROM sys.database_principals dp
LEFT JOIN sys.server_principals sp ON dp.sid = sp.sid
WHERE dp.type IN ('S', 'U')
```text
AND sp.sid IS NULL
AND dp.name NOT IN ('dbo', 'guest', 'INFORMATION_SCHEMA', 'sys');

### Connection Encryption

```sql
-- Force encrypted connections (Server Configuration)
-- SQL Server Configuration Manager → SQL Server Network Configuration
-- → Protocols for [Instance] → Properties → Force Encryption = Yes

-- Verify encrypted connections
SELECT 
```text
session_id,
login_name,
host_name,
program_name,
encrypt_option```
FROM sys.dm_exec_connections
WHERE encrypt_option = 'TRUE';

Compliance and Regulations

GDPR Compliance

-- Right to be forgotten
CREATE PROCEDURE dbo.DeleteCustomerData
```text
@CustomerID INT```
AS
BEGIN
```sql
SET NOCOUNT ON;

BEGIN TRY
    BEGIN TRANSACTION;
    
    -- Delete personal data
    DELETE FROM dbo.Orders WHERE CustomerID = @CustomerID;
    DELETE FROM dbo.Addresses WHERE CustomerID = @CustomerID;
    DELETE FROM dbo.Customers WHERE CustomerID = @CustomerID;
    
    -- Audit deletion
    INSERT INTO dbo.AuditLog (Action, CustomerID, Timestamp)
    VALUES ('GDPR_DELETE', @CustomerID, GETUTCDATE());
    
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION;
    THROW;
END CATCH;```
END;

Data Retention Policies

-- Archive old data
CREATE PROCEDURE dbo.ArchiveOldOrders
AS
BEGIN
```sql
-- Move orders older than 7 years to archive
INSERT INTO dbo.OrdersArchive
SELECT * FROM dbo.Orders
WHERE OrderDate < DATEADD(YEAR, -7, GETDATE());

DELETE FROM dbo.Orders
WHERE OrderDate < DATEADD(YEAR, -7, GETDATE());```
END;

Architecture Decision and Tradeoffs

When designing data management solutions with SQL Server, consider these key architectural trade-offs:

Approach Best For Tradeoff
Managed / platform service Rapid delivery, reduced ops burden Less customisation, potential vendor lock-in
Custom / self-hosted Full control, advanced tuning Higher operational overhead and cost

Recommendation: Start with the managed approach for most workloads and move to custom only when specific requirements demand it.

Validation and Versioning

  • Last validated: April 2026
  • Validate examples against your tenant, region, and SKU constraints before production rollout.
  • Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.

Security and Governance Considerations

  • Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
  • Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
  • Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.

Cost and Performance Notes

  • Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
  • Baseline performance with synthetic and real-user checks before and after major changes.
  • Scale resources with measured thresholds and revisit sizing after usage pattern changes.

Official Microsoft References

  • https://learn.microsoft.com/sql/
  • https://learn.microsoft.com/azure/azure-sql/
  • https://learn.microsoft.com/fabric/database/

Public Examples from Official Sources

  • These examples are sourced from official public Microsoft documentation and sample repositories.
  • Documentation examples: https://learn.microsoft.com/sql/
  • Sample repositories: https://github.com/microsoft/sql-server-samples
  • Prefer adapting these examples to your tenant, subscriptions, and governance requirements before production use.

Key Takeaways

  • Use Windows Authentication over SQL Authentication
  • Implement principle of least privilege
  • Enable TDE for data at rest encryption
  • Use Always Encrypted for sensitive columns
  • Prevent SQL injection with parameterized queries
  • Enable SQL Server Audit for compliance
  • Implement row-level security for multi-tenant scenarios
  • Regularly review and audit permissions
  • Enforce strong password policies
  • Keep SQL Server patched and updated

Next Steps

  • Audit current security configuration
  • Implement TDE for production databases
  • Enable SQL Server Audit
  • Review and remediate excessive permissions
  • Create security baseline and monitoring

Additional Resources


Secure by design. Protected by practice.

Discussion