Forty Servers, One Afternoon, One Script
We got called in after a client’s previous MSP left them with no documentation and no standardized network adapter configuration across their server fleet. Forty-three Windows Server 2022 nodes spread across two datacenters, and we needed to audit every single one — adapter settings, IP configurations, RSC status, IPsec policies. The obvious move was to write something that could reach out and query all of them without logging into each one manually. That is when PowerShell CimSession became the tool I kept coming back to.
This post covers how CimSession works, how to build sessions against remote machines, and how to run network cmdlets through those sessions at scale. Explaining how to write PowerShell from scratch is outside the scope of this post — if you need that foundation first, Microsoft Learn’s PowerShell documentation is the right starting point.
Why CimSession Instead of Just -ComputerName
Before CIM, most remote management in PowerShell ran through WMI — specifically Get-WmiObject with a -ComputerName parameter. It worked, but it was clunky. Every cmdlet opened its own connection, handled its own auth, and closed when it was done. Run twenty queries against one server and you have opened twenty separate sessions. That overhead is noticeable at scale.
CIM — Common Information Model — was Microsoft’s answer to that problem. The CIM cmdlets broker all communications through a persistent session object. You create the session once, pass it to every cmdlet you want to run, and the connection stays open across all of them. One session, many queries. The performance difference on large environments is real.
There is also a protocol advantage. The old WMI stack used DCOM for everything, which is notoriously difficult to firewall properly. CIM sessions use WS-Man (Web Services for Management) for remote connections, which runs over standard HTTP and HTTPS ports. That matters a lot in environments where the security team controls the firewall and does not want to open the full DCOM port range.
One thing worth knowing upfront: newer CIM cmdlets like Get-CimInstance do not carry a -ComputerName parameter the way Get-WmiObject did. They are designed to work through sessions. That is a deliberate design choice — it pushes you toward the session model rather than letting you accidentally fall back to one-off connections.
Notes Before You Start
A few requirements need to be in place before any of this works:
- PowerShell 4.0 or later (Windows Management Framework 4.0 minimum) on both the management machine and the target servers
- WinRM enabled on all targets —
Enable-PSRemotinghandles this if it is not already on - Port 5985 open for HTTP communications, or port 5986 for HTTPS — these need to be open on target server firewalls
- Administrator-level credentials, either via your current session or passed explicitly
Running everything under administrator accounts eliminates most of the permission headaches. You can scope things down tighter with constrained endpoints, but that is a separate conversation.
Creating a CimSession
The cmdlet is New-CimSession. Basic usage against a single remote server:
$session = New-CimSession -ComputerName 'SERVER01'
That is it for the simple case. PowerShell uses your current credentials and connects over WS-Man. If you need to pass alternate credentials:
$cred = Get-Credential
$session = New-CimSession -ComputerName 'SERVER01' -Credential $cred
For multiple servers, pass an array to -ComputerName:
$servers = @('SERVER01', 'SERVER02', 'SERVER03', 'SERVER04')
$sessions = New-CimSession -ComputerName $servers -Credential $cred
That single command creates one session per server and stores them all in $sessions. When you pass $sessions to a cmdlet’s -CimSession parameter later, it runs against all of them. This is where the efficiency comes from — you are not rebuilding connections for every query.
On the protocol side: local connections use DCOM by default, remote connections use WS-Man. You generally do not need to think about this unless you want to force WS-Man locally for consistency, which you can do with New-CimSessionOption:
# Force WS-Man even for local connections
$opt = New-CimSessionOption -Protocol WSMan
$session = New-CimSession -ComputerName 'localhost' -SessionOption $opt
For HTTPS instead of HTTP (port 5986 instead of 5985), set -UseSsl in the session options. Worth doing in production environments where you want management traffic encrypted in transit.
Running Network Cmdlets Through the Session
Once you have sessions, the pattern is the same for almost every network cmdlet — add -CimSession $sessions. Here is what that looks like in practice.
Querying Network Adapters
This was the first thing we ran on that client engagement — get a picture of every adapter across every server:
Get-NetAdapter -CimSession $sessions | Select-Object PSComputerName, Name, InterfaceDescription, Status, LinkSpeed
The PSComputerName property tells you which server each result came from. When you are looking at output from forty machines, that column is the only thing keeping you sane. Pipe it to Export-Csv and you have your inventory baseline in a spreadsheet the client can actually read.
IP Configuration
Get-NetIPConfiguration -CimSession $sessions | Select-Object PSComputerName, InterfaceAlias, IPv4Address, IPv6Address, IPv4DefaultGateway, DNSServer
This caught a real problem on that engagement. Three servers in the secondary datacenter had a stale DNS server entry pointing to a decommissioned domain controller. Nobody knew because nobody had looked at the config programmatically across all machines at once. That is the value of doing this at fleet scale rather than box by box.
Receive Segment Coalescing
If you have read our post on Get-NetAdapterRsc and configuring RSC in PowerShell, you know RSC is one of those settings that is easy to miss during initial server builds. Checking it across a session array is straightforward:
Get-NetAdapterRsc -CimSession $sessions | Select-Object PSComputerName, Name, IPv4Enabled, IPv6Enabled
On that same engagement, RSC was disabled on about a third of the servers — all the ones built from an older gold image that predated current build standards. We fixed them in the same script pass using Set-NetAdapterRsc with the same -CimSession pattern.
IPsec Rules
Network cmdlets that work against IPsec rules follow the same session pattern. This came up on a separate engagement where we were brought in to assess a financial services client’s internal traffic encryption posture:
Get-NetIPsecRule -CimSession $sessions | Select-Object PSComputerName, DisplayName, Enabled, Direction, Action
The -CimSession parameter accepts a CimSession[] array, so passing your full $sessions variable fans out across all sessions, collects results, and returns them as a unified object set with the computer name stamped on each row.
Getting CIM Instances Directly
For anything not covered by a dedicated network cmdlet, you can query CIM classes directly through your session:
Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -CimSession $sessions |
Where-Object { $_.IPEnabled -eq $true } |
Select-Object PSComputerName, Description, IPAddress, DefaultIPGateway, DNSServerSearchOrder
This pulls from Win32_NetworkAdapterConfiguration and gives you a different angle on adapter config than Get-NetIPConfiguration. The WMI class tends to have more legacy fields; the newer cmdlets have cleaner output. Use whichever fits your reporting need.
Managing Multiple Sessions
When you are working across a lot of servers, session management becomes part of the job. A few things worth knowing.
Check active sessions: Get-CimSession shows you what is open — useful when you want to confirm what is still connected before running something that modifies config.
Get-CimSession | Select-Object Name, ComputerName, Protocol, State
Target a subset: You do not have to pass all sessions to every cmdlet. Pull a specific session when you only need to touch one machine:
$targetSession = Get-CimSession -ComputerName 'SERVER07'
Get-NetAdapter -CimSession $targetSession
Build sessions from a file: For larger managed environments, we typically maintain a server list as a text file and build sessions from it at the top of scripts:
$servers = Get-Content 'C:\Admin\server-list.txt'
$cred = Get-Credential
$sessions = New-CimSession -ComputerName $servers -Credential $cred
That pattern scales cleanly from ten servers to several hundred. At very large scale, session creation itself becomes the chokepoint — we batch in groups of fifty for environments above that threshold to avoid overwhelming WinRM on the management host.
Running Long Queries as Background Jobs
Some network cmdlets take a while to return results, especially over slower WAN links or when querying many endpoints. Most CIM-aware network cmdlets support an -AsJob switch that pushes the work to a background job:
$job = Get-NetIPConfiguration -CimSession $sessions -AsJob
# Do other work here
Receive-Job -Job $job -Wait | Select-Object PSComputerName, InterfaceAlias, IPv4Address
The job runs while your PowerShell session stays interactive. For audit scripts that touch a lot of machines and are not time-sensitive, this is a cleaner pattern than waiting synchronously. That said, not every cmdlet that accepts -CimSession supports -AsJob — test before building a workflow around it. The Microsoft Learn reference pages for each cmdlet will tell you whether the parameter exists.
Always Clean Up Sessions
Open CimSessions hold resources on both ends — your management machine and the target servers. In any script that creates sessions at the top, close them at the end:
Remove-CimSession -CimSession $sessions
Or clean up everything at once:
Get-CimSession | Remove-CimSession
On a server hardening engagement we ran earlier this year — and if you have not looked at our Windows Server hardening baseline, it covers the broader security picture — one finding was a management workstation with seventeen stale CIM sessions still open from a script someone had run and never cleaned up. Not directly exploitable, but unnecessary resource drain and an open WinRM surface. Build Remove-CimSession into your cleanup block as a habit, not an afterthought.
When You Hit Firewall Problems
This is the most common failure mode when rolling CimSession out in a new environment. The error message is not always obvious about the root cause. Before going down a PowerShell debugging rabbit hole, verify the basics:
- Port 5985 or 5986 is open from your management host to the target
- WinRM is running on the target:
Get-Service WinRM -ComputerName SERVER01 - The target is in your TrustedHosts list if it is not domain-joined:
Set-Item WSMan:\localhost\Client\TrustedHosts -Value 'SERVER01'
In domain environments, WinRM configuration typically propagates via Group Policy without manual intervention. In workgroup or mixed-trust scenarios, you will need to handle TrustedHosts explicitly. Cross-forest and non-domain authentication gets more involved — CIM’s WS-Man foundation was designed partly to communicate with non-Windows systems from the same command base, but that auth configuration is a separate post.
A Practical Audit Script
Here is a stripped-down but functional version of what we built for that initial engagement. Creates sessions, pulls three data sets, exports to CSV, and cleans up:
# Load server list
$servers = Get-Content 'C:\Admin\servers.txt'
$cred = Get-Credential
# Create sessions
Write-Host 'Creating CIM sessions...'
$sessions = New-CimSession -ComputerName $servers -Credential $cred
# Pull data
Write-Host 'Querying adapters...'
$adapterData = Get-NetAdapter -CimSession $sessions |
Select-Object PSComputerName, Name, InterfaceDescription, Status, LinkSpeed, MACAddress
Write-Host 'Querying IP configuration...'
$ipData = Get-NetIPConfiguration -CimSession $sessions |
Select-Object PSComputerName, InterfaceAlias, IPv4Address, IPv4DefaultGateway, DNSServer
Write-Host 'Querying RSC status...'
$rscData = Get-NetAdapterRsc -CimSession $sessions |
Select-Object PSComputerName, Name, IPv4Enabled, IPv6Enabled
# Export results
$timestamp = Get-Date -Format 'yyyyMMdd-HHmm'
$adapterData | Export-Csv "C:\Audits\adapters-$timestamp.csv" -NoTypeInformation
$ipData | Export-Csv "C:\Audits\ipconfig-$timestamp.csv" -NoTypeInformation
$rscData | Export-Csv "C:\Audits\rsc-$timestamp.csv" -NoTypeInformation
# Clean up
Write-Host 'Removing sessions...'
Remove-CimSession -CimSession $sessions
Write-Host 'Done.'
That script ran across forty-three servers in about four minutes on that engagement. The output went into a shared folder, the client’s team imported it into their CMDB, and they had a documented network baseline for the first time in their history. That is the concrete outcome of getting comfortable with CimSession — it turns a multi-day manual effort into a script you run once.
One Opinion on Config Changes at Scale
I will say it plainly: if you are managing more than five Windows servers and you are not using CimSession for fleet-wide queries, you are making unnecessary work for yourself. The pattern is consistent across almost all the networking cmdlets, the performance is better than old WMI approaches, and the output pipes cleanly into whatever reporting format your clients need.
Where I would pump the brakes is config changes rather than read-only queries. Running Set-NetAdapter or Set-NetAdapterRsc against a session array of forty servers simultaneously is powerful and dangerous in equal measure. For change operations, target specific sessions explicitly, test on one machine first, and run with proper change control. Reads are forgiving. Writes are not.
If you want to talk through how this fits into your environment’s management strategy or build out a broader automation workflow, get in touch through our contact page. We scope these engagements regularly and can usually identify quick wins in the first conversation.


