DBA Scripts: Security

🔧Part of the DBA-Tools Project, copy/paste SQL Server scripts and health checks.

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?

👤 Logins & Authentication

What is actually happening at the login layer?

🔒 Encryption & Connections

Will the encryption layer survive a restore?

🛡️ Audit & Surface Area

What protection is missing or hidden?


The one people ask for most

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 sa account 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.

1
WHO HAS ACCESSSysadmin Members then Permissions and Role MembershipEstablishes who has privileged access at every level, not just the top.
2
IS THAT ACCESS CLEANOrphaned UsersEstablishes whether the access that exists is clean and intentional, not leftover from a migration.
3
WHAT IS HAPPENING NOWLogin Security AuditEstablishes what’s actually happening right now: failed attempts, active sessions, weak settings.
4
WILL IT SURVIVE A RESTORECertificates, Keys, and TDE StatusEstablishes whether the encryption layer will survive a restore or DR failover.
5
WHAT IS MISSING ENTIRELYAudit Specifications, DDL Triggers, and Proxy CredentialsEstablishes what’s missing entirely: formal auditing, hidden schema protection, privilege-escalation paths through Agent proxies.

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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *