Part of the DBA-Tools Project.
Four Questions, One Topic: What’s Linked to This Server
Linked servers are the kind of configuration that gets set up once, for one integration, and then forgotten until a migration or an incident forces someone to ask what’s actually connected to this instance and how. There are four separate questions worth asking, and one script for each:
- What linked servers exist? — a plain inventory
- What do they connect alongside logins and jobs, for a migration review? — a broader pre-migration snapshot
- How risky is the security context of each one? — catch-all credential mappings are the highest-risk configuration
- Are they actually reachable right now? — a live connectivity test, not just a config listing
This post covers all four, one script each, because they answer related but genuinely different questions and a DBA reaching for “linked servers” could mean any of them.
Why Linked Server Auditing Matters
- A linked server with a catch-all credential mapping (any local login maps to one stored remote identity) is a real security exposure, anyone who can query the linked server inherits that remote identity
- Before a migration, linked servers are dependencies that need to exist on the target server too, missing one breaks whatever process depends on it, often silently until that process runs
- A linked server can be configured but unreachable, DNS changes, firewall rules, and decommissioned remote servers all leave stale linked server definitions behind
sys.linked_serverswas removed in SQL Server 2025, scripts written against it will break; all four scripts here usesys.servers WHERE is_linked = 1instead
When to Run These Scripts
- Get-LinkedServerInventory — a quick “what’s linked here” check, first thing when reviewing an unfamiliar server
- Get-LinkedServerAndJobInventory — before a migration, alongside a login and job audit, to see the full pre-migration dependency picture in one run
- Get-LinkedServerSecurity — a periodic security review, or immediately after inheriting a server, to check for catch-all credential mappings
- Get-LinkedServerConnectivity — when troubleshooting a process that depends on a linked server, or as a pre-migration sanity check that every linked server the target needs is actually reachable
The Scripts
1. Get-LinkedServerInventory — Plain Inventory
/*
Script Name : Get-LinkedServerInventory
Category : migration
Purpose : Inventory linked servers for migration and connectivity dependency mapping.
Author : Peter Whyte (https://sqldba.blog/dba-scripts-get-linked-servers/)
Requires : VIEW ANY DATABASE
*/
-- Blog: https://sqldba.blog/dba-scripts-get-linked-servers/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;
SELECT
s.name AS linked_server_name,
s.product,
s.provider,
s.data_source,
s.location,
s.catalog,
CASE WHEN s.is_linked = 1 THEN 'Linked' ELSE 'Local' END AS status
FROM sys.servers AS s
WHERE s.is_linked = 1
ORDER BY s.name;
2. Get-LinkedServerAndJobInventory — Migration Pre-Check (Logins + Linked Servers + Jobs)
/*
Script Name : Get-LinkedServerAndJobInventory
Category : configuration-and-environment
Purpose : Inventory logins, linked servers, and SQL Agent jobs for pre-migration reviews.
Author : Peter Whyte (https://sqldba.blog/dba-scripts-get-linked-servers/)
Requires : VIEW ANY DATABASE, db_datareader on msdb
Notes : Returns three result sets (logins, linked servers, jobs). Run in SSMS or
use the individual focused scripts for CSV export.
*/
-- Blog: https://sqldba.blog/dba-scripts-get-linked-servers/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;
SELECT
'LOGIN' AS object_type,
sp.name AS name,
sp.type_desc AS detail,
CAST(sp.is_disabled AS VARCHAR(5)) AS status
FROM sys.server_principals AS sp
WHERE sp.type IN ('S', 'U', 'G')
AND sp.name NOT LIKE '##%'
AND sp.name NOT LIKE 'NT AUTHORITY%'
AND sp.name NOT LIKE 'NT SERVICE%'
ORDER BY sp.name;
SELECT
'LINKED SERVER' AS object_type,
s.name AS name,
s.product + ' / ' + s.provider AS detail,
s.data_source AS status
FROM sys.servers AS s
WHERE s.is_linked = 1
ORDER BY s.name;
SELECT
'JOB' AS object_type,
j.name AS name,
ISNULL(sp.name, '(unknown)') AS detail,
CASE j.enabled WHEN 1 THEN 'Enabled' ELSE 'Disabled' END AS status
FROM msdb.dbo.sysjobs AS j
LEFT JOIN sys.server_principals AS sp ON j.owner_sid = sp.sid
ORDER BY j.name;
This one returns three result sets in a single run, logins, linked servers, and jobs, because those three together are what a pre-migration review actually needs: who can log in, what’s linked, and what’s scheduled to run. Run it in SSMS, or use the three focused scripts individually for clean CSV export.
3. Get-LinkedServerSecurity — Security Context and Risk Level
/*
Script Name : Get-LinkedServerSecurity
Category : security
Purpose : Lists linked servers with their security context — how local logins are
mapped to remote logins. Catch-all mappings with stored credentials are
the highest-risk configuration.
Author : Peter Whyte (https://sqldba.blog/dba-scripts-get-linked-servers/)
Requires : VIEW ANY DEFINITION or sysadmin
HealthCheck : Yes
*/
-- Blog: https://sqldba.blog/dba-scripts-get-linked-servers/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;
/*
DESIGN: sys.linked_logins contains one row per mapping. A NULL local_login_name
means the mapping applies to any login without a specific mapping (catch-all).
Risk assessment:
HIGH — catch-all or specific mapping with stored remote credentials
MEDIUM — self-credentials (impersonation) mapping
LOW — no mapping / will fail for unmatched logins
*/
SELECT
s.name AS linked_server,
s.product,
s.provider,
s.data_source,
ISNULL(s.catalog, '') AS remote_catalog,
ISNULL(SUSER_SNAME(NULLIF(ll.local_principal_id, 0)), '(any login)') AS local_login,
CASE
WHEN ll.uses_self_credential = 1
THEN 'Impersonate — context of the calling login'
WHEN ll.remote_name IS NOT NULL AND ll.local_principal_id = 0
THEN 'Catch-all mapping → ' + ll.remote_name
WHEN ll.remote_name IS NOT NULL
THEN 'Explicit mapping → ' + ll.remote_name
ELSE '(no mapping — will fail for unmatched logins)'
END AS security_context,
ll.uses_self_credential,
ll.remote_name AS remote_login,
CASE
WHEN ll.remote_name IS NOT NULL AND ll.local_principal_id = 0
THEN 'HIGH — catch-all with stored credentials'
WHEN ll.remote_name IS NOT NULL AND ll.local_principal_id != 0
THEN 'HIGH — explicit mapping with stored credentials'
WHEN ll.uses_self_credential = 1
THEN 'MEDIUM — impersonation (caller context)'
WHEN ll.remote_name IS NULL AND ll.uses_self_credential = 0
THEN 'LOW — no mapping (access denied for unmatched logins)'
ELSE 'UNKNOWN'
END AS risk_level,
s.is_remote_login_enabled,
s.is_rpc_out_enabled,
s.modify_date AS last_modified
FROM sys.servers s
LEFT JOIN sys.linked_logins ll ON ll.server_id = s.server_id
WHERE s.is_linked = 1
ORDER BY
CASE
WHEN ll.remote_name IS NOT NULL AND ll.local_principal_id = 0 THEN 1
WHEN ll.remote_name IS NOT NULL THEN 2
WHEN ll.uses_self_credential = 1 THEN 3
ELSE 4
END,
s.name,
ll.local_principal_id;
This is the one that matters most for security review. A catch-all mapping (local_principal_id = 0) with stored credentials means any login on this server that doesn’t have its own explicit mapping falls through to one shared remote identity, that’s the highest-risk configuration this script flags.
4. Get-LinkedServerConnectivity — Live Connectivity Test
/*
Script Name : Get-LinkedServerConnectivity
Category : monitoring
Purpose : Inventories all linked servers and tests each one for connectivity using sp_testlinkedserver.
Author : Peter Whyte (https://sqldba.blog/dba-scripts-get-linked-servers/)
Requires : sysadmin or ALTER ANY LINKED SERVER
*/
-- Blog: https://sqldba.blog/dba-scripts-get-linked-servers/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;
/* Note: sys.linked_servers was removed in SQL Server 2025 — use sys.servers WHERE is_linked = 1 */
IF OBJECT_ID('tempdb..#LSResults') IS NOT NULL DROP TABLE #LSResults;
CREATE TABLE #LSResults (
linked_server_name NVARCHAR(128) NOT NULL,
product NVARCHAR(128),
provider NVARCHAR(128),
data_source NVARCHAR(4000),
is_rpc_out_enabled BIT,
is_remote_login_enabled BIT,
modify_date DATETIME,
connectivity VARCHAR(15) NOT NULL DEFAULT 'UNTESTED',
error_detail NVARCHAR(2000)
);
INSERT INTO #LSResults (linked_server_name, product, provider, data_source,
is_rpc_out_enabled, is_remote_login_enabled, modify_date)
SELECT
name,
product,
provider,
data_source,
is_rpc_out_enabled,
is_remote_login_enabled,
modify_date
FROM sys.servers
WHERE is_linked = 1;
/* ── Test connectivity for each linked server ────────────────────────────── */
DECLARE @ls NVARCHAR(128);
DECLARE @err NVARCHAR(2000);
DECLARE ls_cur CURSOR FAST_FORWARD FOR
SELECT linked_server_name FROM #LSResults ORDER BY linked_server_name;
OPEN ls_cur; FETCH NEXT FROM ls_cur INTO @ls;
WHILE @@FETCH_STATUS = 0
BEGIN
BEGIN TRY
EXEC sp_testlinkedserver @ls;
UPDATE #LSResults SET connectivity = 'REACHABLE' WHERE linked_server_name = @ls;
END TRY
BEGIN CATCH
SET @err = LEFT(ERROR_MESSAGE(), 2000);
UPDATE #LSResults
SET connectivity = 'UNREACHABLE', error_detail = @err
WHERE linked_server_name = @ls;
END CATCH;
FETCH NEXT FROM ls_cur INTO @ls;
END;
CLOSE ls_cur; DEALLOCATE ls_cur;
SELECT
linked_server_name,
product,
provider,
data_source,
connectivity,
is_rpc_out_enabled,
is_remote_login_enabled,
modify_date,
error_detail
FROM #LSResults
ORDER BY CASE connectivity WHEN 'UNREACHABLE' THEN 1 WHEN 'REACHABLE' THEN 2 ELSE 3 END,
linked_server_name;
DROP TABLE #LSResults;
This one actually calls sp_testlinkedserver for each linked server found, wrapped in TRY/CATCH so one unreachable server doesn’t stop the rest from being tested. error_detail captures the real connection failure message, useful for handing straight to a network or firewall investigation.
How To Run From The Repo
Clone DBA Tools, initialize and run whichever script answers your question:
# Clone dba-tools repo:
git clone https://github.com/peterwhyte-lgtm/dba-tools
# Initialize environment:
cd dba-tools
.\Initialize-Environment.ps1
# Plain linked server inventory:
.\run.ps1 Get-LinkedServerInventory
# Migration pre-check: logins + linked servers + jobs:
.\run.ps1 Get-LinkedServerAndJobInventory
# Security context and risk level for each linked server:
.\run.ps1 Get-LinkedServerSecurity
# Live connectivity test:
.\run.ps1 Get-LinkedServerConnectivity
# Any of these against a remote server:
.\run.ps1 Get-LinkedServerSecurity -ServerInstance SQLSERVER01
These scripts live in the repo at:
sql/inventory/Get-LinkedServerInventory.sqlsql/inventory/Get-LinkedServerAndJobInventory.sqlsql/security/Get-LinkedServerSecurity.sqlsql/monitoring/Get-LinkedServerConnectivity.sql
Example Output
Real output from this lab instance, not staged. This lab box has no linked servers configured, so Get-LinkedServerInventory, Get-LinkedServerSecurity, and Get-LinkedServerConnectivity all correctly returned zero rows, an honest “nothing found” result, not a bug. That’s a valid and useful finding in its own right: it confirms there’s no linked-server dependency to carry into a migration plan for this server.
Get-LinkedServerAndJobInventory still returns real data for its login and job result sets (condensed here, 11 logins and 5 jobs total on this instance):
LOGIN result set (sample):
| object_type | name | detail | status |
|---|---|---|---|
| LOGIN | DBA_ServiceAccount | SQL_LOGIN | 0 |
| LOGIN | DemoDisabledAdmin | SQL_LOGIN | 1 |
| LOGIN | HPAI01\Peter | WINDOWS_LOGIN | 0 |
| LOGIN | sa | SQL_LOGIN | 0 |
LINKED SERVER result set: (empty, none configured)
JOB result set:
| object_type | name | detail | status |
|---|---|---|---|
| JOB | DBA Daily Full Backups | sa | Enabled |
| JOB | DBA Index Stats Maintenance | sa | Enabled |
| JOB | DBA Watchtower CollectDatabaseGrowthEvents | sa | Enabled |
| JOB | DBA Watchtower Database Free Space Monitor | sa | Enabled |
| JOB | syspolicy_purge_history | sa | Enabled |
status = 1 on DemoDisabledAdmin in the login result set is exactly the kind of thing this script is built to surface fast, a disabled login sitting in the list, worth a second look during any migration or access review.
Understanding the Results
- Zero linked servers is a valid finding — don’t mistake an empty result set for a script failure, run
Get-LinkedServerInventoryalone first to sanity-check before assuming something’s broken is_linked = 1filter —sys.serversalso contains the local server itself and any non-linked entries, all four scripts filter to only genuinely linked servers- risk_level HIGH — a catch-all or explicit mapping with stored remote credentials, this is the finding to act on first if present
- connectivity = UNREACHABLE — check
error_detailfor the real connection error, common causes are DNS resolution, firewall rules, or a decommissioned remote server left behind in configuration
Best Practices
- Run
Get-LinkedServerInventoryfirst as a quick sanity check, then reach for the security or connectivity scripts only if linked servers actually exist - Treat any catch-all credential mapping (
risk_level = HIGH) as a finding to resolve, not just document, scope the mapping to specific logins or remove the stored credential entirely - Before a migration, run
Get-LinkedServerAndJobInventoryalongside a full login and job audit, missing a linked server dependency on the target is a common, avoidable migration gap - Re-run
Get-LinkedServerConnectivityafter any network change (firewall rule, DNS update, remote server maintenance) that could affect a linked server’s reachability
Related Scripts
You may also find these scripts useful:
- Server Inventory (hub)
- Version and Edition
- OS and Hardware Info
- Migration Risk Assessment
- Audit Specifications, DDL Triggers, and Proxy Credentials
- Database Snapshot Inventory
- Login and Job Inventory
- Active Connections by Database
Frequently Asked Questions
Why four scripts instead of one combined script?
Each answers a genuinely different question, plain inventory, migration pre-check, security risk, and live connectivity are different enough in scope and permission requirements (Get-LinkedServerConnectivity needs sysadmin or ALTER ANY LINKED SERVER, the others don’t) that combining them into one script would mean over-requesting permissions or silently skipping sections.
What replaced sys.linked_servers in SQL Server 2025?
sys.servers WHERE is_linked = 1. All four scripts here already use it, if you have older scripts referencing sys.linked_servers directly, they’ll break on SQL Server 2025 and need the same fix.
Does a catch-all mapping always mean a problem?
Not automatically, some environments deliberately use one shared service account for simplicity. But it does mean anyone with access to run a query through that linked server inherits that identity’s full permissions on the remote side, worth confirming it’s an intentional choice, not a forgotten default.
Summary
Linked servers are easy to forget about until a migration or a security review forces the question. These four scripts cover the question from every angle that matters: what’s linked, what else is tied to it for migration purposes, how risky the credential mapping is, and whether it’s actually reachable right now.
Run Get-LinkedServerInventory as a first pass on any server, and reach for the security and connectivity scripts specifically when planning a migration or investigating an access review finding.
Leave a Reply