Monitor DuoKey SQL EKM encryption status and configure alerts
Effective monitoring ensures your encrypted databases remain healthy and compliant. This guide covers real-time status monitoring, automated health checks, SQL Server Agent alerts, and audit logging.
Create a view for easy monitoring of all encrypted databases:
CREATE OR ALTER VIEW dbo.vw_TDE_Status
AS
SELECT
DB_NAME(dek.database_id) AS DatabaseName,
d.state_desc AS DatabaseState,
CASE dek.encryption_state
WHEN 0 THEN 'No DEK Present'
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'
WHEN 6 THEN 'Protection Change In Progress'
END AS EncryptionStatus,
dek.percent_complete AS ProgressPercent,
dek.key_algorithm AS Algorithm,
dek.key_length AS KeyLength,
ak.name AS ProtectingKeyName,
cp.name AS EKMProvider,
dek.create_date AS DEKCreated,
dek.regenerate_date AS LastKeyRotation,
DATEDIFF(DAY, ISNULL(dek.regenerate_date, dek.create_date), GETDATE())
AS DaysSinceRotation
FROM sys.dm_database_encryption_keys dek
INNER JOIN sys.databases d ON dek.database_id = d.database_id
LEFT JOIN sys.asymmetric_keys ak ON dek.encryptor_thumbprint = ak.thumbprint
LEFT JOIN sys.cryptographic_providers cp ON ak.provider_id = cp.provider_id;
GOSELECT * FROM dbo.vw_TDE_StatusUnderstanding encryption states is critical for monitoring:
| State | Value | Description | Action Required |
|---|---|---|---|
| 0 | No DEK | No database encryption key present | Create DEK with CREATE DATABASE ENCRYPTION KEY |
| 1 | Unencrypted | DEK exists but encryption not enabled | Enable with ALTER DATABASE SET ENCRYPTION ON |
| 2 | Encrypting | Encryption in progress | Wait for completion, monitor percent_complete |
| 3 | Encrypted | Database fully encrypted | None - normal operation |
| 4 | Key Change | Key rotation in progress | Wait for completion |
| 5 | Decrypting | Decryption in progress | Wait for completion |
| 6 | Protection Change | Protector change in progress | Wait for completion |
-- All encrypted databases with status
SELECT
DB_NAME(database_id) AS [Database],
encryption_state,
percent_complete
FROM sys.dm_database_encryption_keys;
GO
-- Check EKM provider is healthy
SELECT name, is_enabled, version
FROM sys.cryptographic_providers
WHERE name = 'DuoKeyProvider';
GO
-- Databases needing attention
SELECT
DB_NAME(database_id) AS [Database],
'Key rotation overdue' AS Issue
FROM sys.dm_database_encryption_keys
WHERE DATEDIFF(DAY, ISNULL(regenerate_date, create_date), GETDATE()) > 365;
GOThis comprehensive stored procedure checks for common issues and can optionally send email alerts:
CREATE OR ALTER PROCEDURE dbo.usp_TDE_HealthCheck
@AlertThresholdDays INT = 365,
@SendEmail BIT = 0,
@EmailRecipients NVARCHAR(500) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Issues TABLE (
Severity VARCHAR(20),
Category VARCHAR(50),
DatabaseName NVARCHAR(128),
Issue NVARCHAR(500),
Recommendation NVARCHAR(500)
);
-- Check for databases that should be encrypted but aren't
INSERT INTO @Issues
SELECT
'WARNING',
'Encryption Gap',
name,
'Database is not encrypted',
'Consider enabling TDE for this database'
FROM sys.databases
WHERE database_id > 4 -- Exclude system databases
AND is_encrypted = 0
AND name NOT IN ('tempdb', 'ReportServer', 'ReportServerTempDB');
-- Check for key rotation overdue
INSERT INTO @Issues
SELECT
'WARNING',
'Key Rotation',
DB_NAME(database_id),
'Key rotation overdue (' +
CAST(DATEDIFF(DAY, ISNULL(regenerate_date, create_date), GETDATE()) AS VARCHAR) +
' days since last rotation)',
'Rotate DEK per security policy'
FROM sys.dm_database_encryption_keys
WHERE DATEDIFF(DAY, ISNULL(regenerate_date, create_date), GETDATE()) > @AlertThresholdDays;
-- Check for stuck encryption operations
INSERT INTO @Issues
SELECT
'CRITICAL',
'Encryption Stuck',
DB_NAME(database_id),
'Encryption operation stuck at ' + CAST(percent_complete AS VARCHAR) + '%',
'Investigate I/O performance and DuoKey connectivity'
FROM sys.dm_database_encryption_keys
WHERE encryption_state IN (2, 4, 5, 6)
AND percent_complete < 100
AND DATEDIFF(HOUR, modify_date, GETDATE()) > 4;
-- Check EKM provider status
IF NOT EXISTS (
SELECT 1 FROM sys.cryptographic_providers
WHERE name = 'DuoKeyProvider' AND is_enabled = 1
)
BEGIN
INSERT INTO @Issues VALUES (
'CRITICAL',
'EKM Provider',
'N/A',
'DuoKey EKM Provider is not enabled or missing',
'Verify provider installation and enable if disabled'
);
END
-- Return results
SELECT * FROM @Issues
ORDER BY
CASE Severity WHEN 'CRITICAL' THEN 1 WHEN 'WARNING' THEN 2 ELSE 3 END,
Category, DatabaseName;
-- Optionally send email (requires Database Mail configured)
IF @SendEmail = 1 AND EXISTS (SELECT 1 FROM @Issues)
BEGIN
DECLARE @body NVARCHAR(MAX);
SELECT @body = STRING_AGG(
Severity + ': ' + Category + ' - ' + DatabaseName + ': ' + Issue,
CHAR(13) + CHAR(10)
) FROM @Issues;
EXEC msdb.dbo.sp_send_dbmail
@recipients = @EmailRecipients,
@subject = 'SQL EKM Health Check Alert',
@body = @body;
END
END
GO-- Basic health check (returns issues only)
EXEC dbo.usp_TDE_HealthCheck;
GO
-- With custom threshold (90 days for key rotation)
EXEC dbo.usp_TDE_HealthCheck @AlertThresholdDays = 90;
GO
-- With email alerts (requires Database Mail)
EXEC dbo.usp_TDE_HealthCheck
@AlertThresholdDays = 365,
@SendEmail = 1,
@EmailRecipients = '[email protected],[email protected]';
GOCreate an automated SQL Server Agent job to run the health check on a schedule:
-- Create health check job
USE msdb;
GO
EXEC dbo.sp_add_job
@job_name = N'TDE_HealthCheck_Hourly',
@enabled = 1,
@description = N'Hourly TDE encryption health check';
GO
EXEC dbo.sp_add_jobstep
@job_name = N'TDE_HealthCheck_Hourly',
@step_name = N'RunHealthCheck',
@subsystem = N'TSQL',
@command = N'EXEC dbo.usp_TDE_HealthCheck
@AlertThresholdDays = 365,
@SendEmail = 1,
@EmailRecipients = ''[email protected]''',
@database_name = N'master';
GO
EXEC dbo.sp_add_schedule
@schedule_name = N'Hourly',
@freq_type = 4,
@freq_interval = 1,
@freq_subday_type = 8,
@freq_subday_interval = 1;
GO
EXEC dbo.sp_attach_schedule
@job_name = N'TDE_HealthCheck_Hourly',
@schedule_name = N'Hourly';
GO
EXEC dbo.sp_add_jobserver
@job_name = N'TDE_HealthCheck_Hourly';
GO
PRINT 'Health check job created successfully.';
GO| Schedule | freq_type | freq_subday_type | freq_subday_interval |
|---|---|---|---|
| Every hour | 4 (Daily) | 8 (Hour) | 1 |
| Every 15 minutes | 4 (Daily) | 4 (Minute) | 15 |
| Every 6 hours | 4 (Daily) | 8 (Hour) | 6 |
| Once daily at 6 AM | 4 (Daily) | 1 (Once) | N/A |
Monitor I/O performance to ensure TDE isn't causing excessive overhead:
-- TDE Performance Baseline Query
SELECT
DB_NAME(fs.database_id) AS DatabaseName,
mf.name AS FileName,
mf.type_desc AS FileType,
fs.num_of_reads AS Reads,
fs.num_of_writes AS Writes,
fs.io_stall_read_ms AS ReadStallMs,
fs.io_stall_write_ms AS WriteStallMs,
CAST(fs.size_on_disk_bytes / 1024.0 / 1024.0 AS DECIMAL(10,2)) AS SizeMB,
d.is_encrypted AS Encrypted,
CASE
WHEN fs.num_of_reads > 0
THEN CAST(fs.io_stall_read_ms / fs.num_of_reads AS DECIMAL(10,2))
ELSE 0
END AS AvgReadLatencyMs,
CASE
WHEN fs.num_of_writes > 0
THEN CAST(fs.io_stall_write_ms / fs.num_of_writes AS DECIMAL(10,2))
ELSE 0
END AS AvgWriteLatencyMs
FROM sys.dm_io_virtual_file_stats(NULL, NULL) fs
INNER JOIN sys.master_files mf
ON fs.database_id = mf.database_id AND fs.file_id = mf.file_id
INNER JOIN sys.databases d
ON fs.database_id = d.database_id
WHERE d.is_encrypted = 1
ORDER BY fs.io_stall_read_ms + fs.io_stall_write_ms DESC;
GO| Metric | Healthy Range | Action if Exceeded |
|---|---|---|
| AvgReadLatencyMs | < 20ms | Check storage I/O, consider faster storage |
| AvgWriteLatencyMs | < 20ms | Check storage I/O, consider log optimization |
| CPU during encryption | < 80% | Schedule encryption during off-hours |
| TDE scan progress | Increasing | Check for locks, I/O bottlenecks |
Enable SQL Server Audit to track key operations for compliance:
-- Create server audit
CREATE SERVER AUDIT TDE_Audit
TO FILE (FILEPATH = 'C:\SQLAudit\', MAXSIZE = 100 MB)
WITH (QUEUE_DELAY = 1000, ON_FAILURE = CONTINUE);
GO
ALTER SERVER AUDIT TDE_Audit WITH (STATE = ON);
GO
-- Create audit specification for crypto operations
CREATE SERVER AUDIT SPECIFICATION TDE_AuditSpec
FOR SERVER AUDIT TDE_Audit
ADD (DATABASE_CHANGE_GROUP),
ADD (SCHEMA_OBJECT_ACCESS_GROUP),
ADD (BACKUP_RESTORE_GROUP)
WITH (STATE = ON);
GO
PRINT 'Audit configuration created successfully.';
GO| Event Group | What It Captures |
|---|---|
| DATABASE_CHANGE_GROUP | CREATE/ALTER/DROP DATABASE operations |
| SCHEMA_OBJECT_ACCESS_GROUP | Access to encryption keys and certificates |
| BACKUP_RESTORE_GROUP | Backup and restore operations |