Who Can Do What, What’s Happening, and What’s Missing
Security on an inherited SQL Server instance breaks down into four genuinely different questions, and a single “run a security scan” checklist tends to blur them together. Who has privileged access, right now? What’s happening at the login layer, brute-force attempts, weak settings, active sessions? Will the encryption layer still work after a restore onto different hardware? And what protective infrastructure, auditing, DDL protection, surface-area settings, either doesn’t exist yet or exists silently and gets forgotten?
This post is the map across all four, tying together every security post on this site into one place: what each answers, how they fit together, and the order to run them in when reviewing a server’s security posture from zero.
Start Here
The Four Areas
Every security check on this site answers one of four questions. Start with the area that matches what you’re being asked.
🔑 Privileged Access
Who can do what, right now?
- Sysadmin Members, the highest-stakes question, focused and fast
- Permissions and Role Membership, everything underneath sysadmin: explicit object and schema grants, server-level denies, database roles
- User Permissions Audit, one login’s effective access across the whole instance (the featured script below)
- Orphaned Users, users whose login SID no longer matches a server login, and how to fix them
👤 Logins & Authentication
What is actually happening at the login layer?
- Login Security Audit, failed login patterns, lockout state, session activity, weak password policy and
sastatus in one cluster - Login and Job Inventory, what logins exist and what they own
- Creating Logins on an AG, keeping logins consistent across replicas
- Login Failed for User (Error 18456), decoding the state codes when a login is being refused
🔒 Encryption & Connections
Will the encryption layer survive a restore?
- Certificates, Keys, and TDE Status, every certificate’s expiry, what it backs, and TDE state per database
- Backup Encryption Status, which backups are actually encrypted, and with what
- Backing Up With Encryption, doing it properly, including the certificate you must keep
- Database Master Keys, the key the certificates themselves sit on, and what a restore onto another instance needs from it
- Forcing Encrypted Connections, encryption in transit, not just at rest
- Check Connection Encryption, confirming what sessions are really using
🛡️ Audit & Surface Area
What protection is missing or hidden?
- Audit Specs, DDL Triggers, Proxy Credentials, audit gaps, hidden schema-change triggers, Agent privilege-escalation paths
- Database Mail and xp_cmdshell, surface-area settings that widen what a compromised login can do
- Linked Servers, where a linked server’s own credentials become someone else’s access
- Login Activity Session, capture logins as they happen, when the audit trail doesn’t exist yet
- Decommission Audit Session, prove whether anything still connects before switching a server off
Audit One Login’s Real Access, in a Single Script
“What can this account actually get to?” arrives from auditors, from a manager about a leaver, and from yourself at 2am wondering how a job reaches a database it shouldn’t. This script answers it in one pass: set @LoginName to a SQL login or a Windows/AD user or group, run it, and you get one row per scope, server level first, then every database that login can reach. Because it reads the login’s own security token under EXECUTE AS rather than reading grant tables, access inherited through nested AD groups shows up automatically, which is exactly the access a manual role-by-role check misses.
Show the full script (Get-UserPermissionsAudit.sql)
/*
Script Name : Get-UserPermissionsAudit
Category : security
Purpose : Audit one login's effective access across the whole instance in a single pass:
server roles/connection principals, plus per-database role membership —
resolved through the real security token, so nested AD group membership
shows up automatically. EDIT @LoginName below before running.
Author : Peter Whyte (https://sqldba.blog)
Requires : sysadmin, or IMPERSONATE permission on the target login plus VIEW ANY DATABASE
*/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;
-- EDIT THIS: the login to investigate. Works for SQL logins and Windows users/groups.
DECLARE @LoginName SYSNAME = N'DOMAIN\username'; -- e.g. N'CONTOSO\jsmith' or a SQL login name
DECLARE @IncludeDatabasesWithoutAccess BIT = 0; -- 1 = also list databases this login can't reach
IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = @LoginName)
BEGIN
RAISERROR('Login "%s" not found on this server. Check spelling (DOMAIN\name for Windows logins).', 16, 1, @LoginName);
RETURN;
END;
IF OBJECT_ID('tempdb..#myuser') IS NOT NULL DROP TABLE #myuser;
CREATE TABLE #myuser (
server_name NVARCHAR(128),
database_name SYSNAME,
principal_id INT,
sid VARBINARY(85),
name NVARCHAR(128),
type NVARCHAR(128),
usage_desc NVARCHAR(128)
);
DECLARE @impersonating BIT = 0;
DECLARE @sql NVARCHAR(MAX);
BEGIN TRY
EXECUTE AS LOGIN = @LoginName;
SET @impersonating = 1;
-- Server-level token, captured before REVERT so it reflects the target login, not the caller.
INSERT INTO #myuser (server_name, database_name, principal_id, sid, name, type, usage_desc)
SELECT @@SERVERNAME, N'[CONNECTION]', lt.principal_id, lt.sid, lt.name,
CASE WHEN lt.type = 'SERVER ROLE' THEN 'ROLE'
WHEN lt.type = 'WINDOWS GROUP' THEN 'WINDOWS GROUP'
ELSE 'SQL USER' END,
lt.usage
FROM sys.login_token AS lt
WHERE lt.sid IN (SELECT sid FROM sys.server_principals);
-- Only databases this login can actually reach (HAS_DBACCESS under impersonation) —
-- otherwise USE on an inaccessible database raises Msg 916 and aborts the whole script.
SET @sql = N'';
SELECT @sql = @sql + N'
USE ' + QUOTENAME(name) + N';
INSERT INTO #myuser (server_name, database_name, principal_id, sid, name, type, usage_desc)
SELECT DISTINCT @@SERVERNAME, DB_NAME(), principal_id, sid, name, type, usage
FROM sys.user_token
WHERE sid IN (SELECT sid FROM sys.database_principals)
AND name <> ''public'';'
FROM sys.databases
WHERE state_desc = 'ONLINE'
AND database_id > 4 -- skip system databases
AND HAS_DBACCESS(name) = 1;
IF LEN(@sql) > 0
EXEC (@sql);
IF @IncludeDatabasesWithoutAccess = 1
BEGIN
INSERT INTO #myuser (server_name, database_name, principal_id, sid, name, type, usage_desc)
SELECT
@@SERVERNAME,
d.name,
NULL,
NULL,
CASE WHEN d.state_desc = 'ONLINE' THEN 'MISSING' ELSE d.state_desc END,
'ROLE',
'MISSING'
FROM sys.databases AS d
WHERE d.database_id > 4
AND (d.state_desc <> 'ONLINE' OR HAS_DBACCESS(d.name) = 0);
END;
REVERT;
SET @impersonating = 0;
END TRY
BEGIN CATCH
IF @impersonating = 1
BEGIN
REVERT;
END;
THROW;
END CATCH;
-- One row per scope: [CONNECTION] (server-level) plus one row per database.
SELECT
x.server_name,
x.database_name,
ISNULL(STUFF((
SELECT ',' + t.name
FROM #myuser AS t
WHERE t.type = 'ROLE'
AND t.server_name = x.server_name
AND t.database_name = x.database_name
ORDER BY t.name
FOR XML PATH('')), 1, 1, ''), '') AS [ROLE],
ISNULL(STUFF((
SELECT ',' + t.name
FROM #myuser AS t
WHERE t.type = 'WINDOWS GROUP'
AND t.server_name = x.server_name
AND t.database_name = x.database_name
ORDER BY t.name
FOR XML PATH('')), 1, 1, ''), '') AS [WINDOWS GROUP],
ISNULL(STUFF((
SELECT ',' + t.name
FROM #myuser AS t
WHERE t.type = 'SQL USER'
AND t.server_name = x.server_name
AND t.database_name = x.database_name
ORDER BY t.name
FOR XML PATH('')), 1, 1, ''), '') AS [SQL USER]
FROM #myuser AS x
GROUP BY x.server_name, x.database_name
ORDER BY
CASE WHEN x.database_name = N'[CONNECTION]' THEN 0 ELSE 1 END,
x.database_name;
DROP TABLE #myuser;
Read-only; needs sysadmin, or IMPERSONATE on the login plus VIEW ANY DATABASE. Full walkthrough, example output and edge cases: DBA Scripts: Get User Permissions Audit · view on GitHub
Why This Matters
- Sysadmin membership is the headline question, but privilege lives at every level, server roles beyond sysadmin, database roles, explicit object grants, all commonly overlooked once the sysadmin check comes back clean
- Failed login patterns, weak password policy, and an enabled
saaccount are all things that make a compromise easier, not evidence that one has happened, catching them early is prevention, not incident response - Certificates back TDE, Service Broker, and column encryption, and they all share the same failure mode: nobody notices an expiring one until a restore fails on the wrong server
- SQL Server Audit, DDL triggers, and Agent proxy/credential mappings are the kind of infrastructure that either was never set up or was set up once and forgotten, neither shows up in a routine health check
How They Fit Together
Run these roughly in this order when reviewing a server’s security posture from scratch. If the question is about one specific account rather than the whole server, skip the sequence and run the single-login audit instead, it answers that in one pass.
Each step narrows from “who can do things” to “what’s actually happening” to “what’s silently absent.” A server that passes step 1 cleanly can still fail steps 3 through 5 badly, they’re genuinely different questions. When the question is about one specific account rather than the whole server, the single-login audit above is the faster route.
Best Practices Across the Series
- Run the full sequence on any inherited server, sysadmin alone is necessary but never sufficient
- Grant VIEW SERVER STATE rather than sysadmin when someone only needs to read diagnostics, it covers most monitoring scripts without handing over the instance
- Treat
SA_ENABLED, an unconfigured SQL Server Audit, and an expiring TDE certificate as the three highest-priority individual findings across this whole series, act on them same-week, not on a rolling backlog - Re-run the Login Security Audit cluster on a routine schedule, it’s the one most likely to change week to week
- Cross-reference TDE findings against Edition Feature Usage before any edition downgrade, since TDE is Enterprise-only up to SQL Server 2017 and available in Standard from SQL Server 2019
Frequently Asked Questions
Do I need sysadmin to run these?
For the single-login audit, you need sysadmin or IMPERSONATE on the target login plus VIEW ANY DATABASE, because it works by impersonating the login and reading its own token. Most of the other scripts in this pillar need far less, typically VIEW SERVER STATE and read access to the catalog views, though anything reading msdb or audit configuration will want more.
Does it pick up access granted through an Active Directory group?
Yes, and that’s the main reason to use it. Because it reads sys.login_token and sys.user_token under impersonation rather than reading grants directly, access that arrives through AD group membership, including nested groups, resolves automatically. A manual check of server and database role members will not show it, which is how “this account shouldn’t have been able to do that” happens.
Why does a login show access I can’t find in the role membership tables?
Almost always an AD group, or the public role, or an explicit grant on an individual object rather than role membership. The token-based approach here catches the first two. For explicit object and schema grants, Permissions and Role Membership is the script that enumerates them.
Is checking sysadmin membership enough on its own?
No. A clean sysadmin list is necessary but not sufficient. Privilege escalation paths commonly sit in other server roles, in Agent proxy and credential mappings, in linked server credential mappings, and in database-level roles like db_owner, none of which appear in a sysadmin check. That’s why the review order above treats sysadmin as step one of five rather than the whole review.
Are these safe to run on production?
All the scripts in this pillar are read-only. The single-login audit is the only one that changes session context, and it does so with EXECUTE AS followed by REVERT inside a TRY/CATCH, so the impersonation is released even if a statement fails. It also skips databases the login cannot open, which avoids a Msg 916 aborting the run partway through.
See Also
This pillar is part of DBA Scripts: The Complete Guide, the map across the whole series organized by the question you’re actually asking.
Summary
Security isn’t one checklist, it’s four separate questions that need four separate answers: who has access, what’s happening at the login layer, whether the encryption layer survives a restore, and what protective infrastructure is missing entirely. This pillar covers all four, from the fast sysadmin check to the deepest audit-and-encryption review, so a security pass on an unfamiliar server means working through a known sequence, not guessing at what to check next.
When the question is about the whole server, start with Sysadmin Members and Permissions and Role Membership, then work down to login activity, encryption, and the audit layer. When it’s about one account, the single-login audit answers it in one run.
Leave a Reply