PowerShell Remoting enables administrators to run PowerShell commands and interactive sessions on remote Windows and Linux systems over the network. It's built on the WS-Management (WS-Man) protocol and Windows Remote Management (WinRM), Microsoft's implementation of WS-Management. PowerShell Remoting serializes structured .NET objects into CLIXML for transport over HTTP/HTTPS, allowing remote administrative workflows while preserving pipeline object processing. It forms the backbone of most enterprise automation and configuration management workflows.
WinRM Basics
WinRM is the underlying service that listens for incoming remote management requests, authenticates the caller, and executes commands on their behalf. WinRM operates over standard network ports:
- Port 5985: default unencrypted HTTP port (traffic is still encrypted at the protocol layer via Kerberos or NTLM).
- Port 5986: default HTTPS port requiring an SSL/TLS server certificate.
Get-Service -Name "WinRM"Status Name DisplayName
------ ---- -----------
Running WinRM Windows Remote Management (WS-Management)Checking WinRM Configuration
Get-Item -Path "WSMan:\localhost\Client\TrustedHosts"WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Client
Type Name SourceOfValue Value
---- ---- ------------- -----
System.String TrustedHosts Enabling Remoting with Enable-PSRemoting
The Enable-PSRemoting cmdlet configures a machine to accept incoming remote commands. It performs the foundational setup for remote management:
- Starts the Windows Remote Management (
WinRM) service and sets its startup type toAutomatic. - Configures an active listener on
0.0.0.0for ports 5985/5986. - Adds Windows Firewall exceptions for incoming WS-Management traffic.
- Registers default PowerShell session configurations (
Microsoft.PowerShellandMicrosoft.PowerShell.Workflow).
Enable-PSRemoting -Forcewhere:
-Forcesuppresses confirmation prompts, useful for unattended setup.-SkipNetworkProfileCheckallows remoting to be enabled on client versions of Windows connected to local networks categorized as "Public".
WinRM has been updated to receive requests.
WinRM service type changed successfully.
WinRM service started.Trusting Remote Hosts (Workgroup Environments)
Outside of a domain, remoting between machines requires explicitly trusting the target hosts.
Set-Item -Path "WSMan:\localhost\Client\TrustedHosts" -Value "SRV01,SRV02" -Forcewhere -Value a comma-separated list of trusted computer names or IP addresses (or "*" to trust all, not recommended).
Interactive Sessions with Enter-PSSession
Interactive remoting is a one-to-one (1:1) connection model analogous to SSH. The Enter-PSSession cmdlet opens an interactive command prompt on the target host, altering the console prompt to reflect the remote system's hostname.
Enter-PSSession -ComputerName "SRV01"where:
-ComputerNameis the DNS hostname, NetBIOS name, or IP address of the target machine.-Credentialprompts for or passes authorized administrative credentials for the remote system (e.g.,(Get-Credential)).
[SRV01]: PS C:\Users\Administrator\Documents> hostname
SRV01
[SRV01]: PS C:\Users\Administrator\Documents> Exit-PSSession
PS C:\LocalWorkstation>where:
[SRV01]: PS>is the prompt modification confirming that all typed commands now execute onSRV01.Exit-PSSessionterminates the remote interactive session and returns the console to the local machine. Typingexithas the same effect.
One-to-Many Execution with Invoke-Command
The Invoke-Command cmdlet enables fan-out remoting (one-to-many execution). It sends script blocks to multiple target computers simultaneously, executes the commands in parallel across remote instances, and aggregates the results back into the local pipeline. By default, it runs up to 32 remote connections concurrently.
Invoke-Command -ComputerName "SRV01" -ScriptBlock { Get-Process | Where-Object { $_.CPU -gt 5 } }where:
-ComputerNameis the target remote machine(s), accepts an array for multiple targets.-ScriptBlockis the code to execute remotely.
ProcessName CPU PSComputerName
----------- --- --------------
chrome 12.3 SRV01Running Against Multiple Computers
Invoke-Command -ComputerName "SRV01", "SRV02" -ScriptBlock { Get-Service -Name "WinRM" }Status Name DisplayName PSComputerName
------ ---- ----------- --------------
Running WinRM Windows Remote Management (WS-Manag...) SRV01
Running WinRM Windows Remote Management (WS-Manag...) SRV02Passing Local Variables into a Remote Script Block
Variables from the local session are not automatically available inside a remote script block. Use -ArgumentList (with a param() block) to pass values positionally, or reference them with the $using: scope modifier.
Using -ArgumentList:
$serviceName = "Spooler"
Invoke-Command -ComputerName "SRV01" -ScriptBlock {
param($svc)
Get-Service -Name $svc
} -ArgumentList $serviceNamewhere -ArgumentList is the values passed into the remote script block, matched positionally to param().
Using the $using: scope modifier:
$servers = @("Server01", "Server02", "Server03")
$serviceName = "wuauserv"
Invoke-Command -ComputerName $servers -ScriptBlock {
$svc = Get-Service -Name $using:serviceName -ErrorAction SilentlyContinue
[PSCustomObject]@{
Service = $svc.Name
Status = $svc.Status
StartType = $svc.StartType
}
} | Select-Object -Property PSComputerName, Service, Status, StartTypewhere:
$using:serviceNameis the$using:scope modifier, which passes the value of a local variable from the caller's console into the remote script block.PSComputerNameis an automatic property added to all objects returning through PowerShell Remoting, identifying the origin node.
./invoke-command-demo.ps1PSComputerName Service Status StartType
-------------- ------- ------ ---------
Server01 wuauserv Running Automatic
Server02 wuauserv Stopped Manual
Server03 wuauserv Running ManualPersistent Sessions with New-PSSession
Both Invoke-Command -ComputerName and Enter-PSSession create and tear down a temporary connection by default. Any variables or state created during that execution are lost. New-PSSession creates a persistent session object instead, which can be reused across multiple commands. This is useful when running several related remote operations, as it avoids the overhead of reconnecting each time and allows variables, functions, and state to persist.
$session = New-PSSession -ComputerName "SRV01"where $session stores a PSSession object representing the open, reusable connection.
Invoke-Command -Session $session -ScriptBlock { Get-Process }
Invoke-Command -Session $session -ScriptBlock { Get-Service -Name "WinRM" }where -Session reuses the existing persistent connection instead of opening a new one for each command.
Complete Persistent Session Example
$session = New-PSSession -ComputerName "Server01"
Invoke-Command -Session $session -ScriptBlock {
$SessionVar = "Initialized at $(Get-Date -Format 'HH:mm:ss')"
}
$output = Invoke-Command -Session $session -ScriptBlock {
"Remote State: $SessionVar"
}
Remove-PSSession -Session $session
$outputwhere:
New-PSSessionestablishes an ongoing background connection to the target system and returns aPSSessionobject.-SessiondirectsInvoke-Commandto reuse an established persistent session instead of opening and closing a new connection.Remove-PSSessionexplicitly closes the remote runspace and tears down network resources on both the client and server.
./persistent-session.ps1Remote State: Initialized at 12:15:30Closing a Persistent Session
Remove-PSSession -Session $sessionPractical Usage
Running a Health Check Across Multiple Servers
A common remoting pattern uses a single persistent session per server to run several diagnostic commands efficiently.
$servers = "SRV01", "SRV02"
$sessions = New-PSSession -ComputerName $servers
$results = Invoke-Command -Session $sessions -ScriptBlock {
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
CPUCount = (Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors
FreeMemoryMB = [math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1KB)
WinRMStatus = (Get-Service -Name "WinRM").Status
}
}
$results | Format-Table -AutoSize
Remove-PSSession -Session $sessionswhere:
New-PSSession -ComputerName $serversopens one persistent session per server in the array.Invoke-Command -Session $sessionsruns the same script block across all open sessions simultaneously.
./remote-health-check.ps1ComputerName CPUCount FreeMemoryMB WinRMStatus PSComputerName
------------ -------- ------------ ----------- --------------
SRV01 4 3821 Running SRV01
SRV02 8 7502 Running SRV02Fleet-Wide Health Audit
Administrators regularly perform parallel diagnostic sweeps across server farms to detect resource starvation, failing storage disks, and system uptimes.
$fleet = @("Server01", "Server02", "Server03")
$fleetReport = Invoke-Command -ComputerName $fleet -ThrottleLimit 10 -ScriptBlock {
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$uptimeDays = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 1)
$cDrive = Get-Volume -DriveLetter 'C'
$freePercent = [math]::Round(($cDrive.SizeRemaining / $cDrive.Size) * 100, 1)
[PSCustomObject]@{
UptimeDays = $uptimeDays
CDriveFreePct = $freePercent
TotalRAM_GB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
}
} -ErrorAction SilentlyContinue
$fleetReport | Select-Object -Property PSComputerName, UptimeDays, CDriveFreePct, TotalRAM_GB |
Format-Table -AutoSizewhere:
-ThrottleLimit 10limits concurrent network operations to 10 nodes at a time to prevent saturating local client network buffers.Get-CimInstance -ClassName Win32_OperatingSystemqueries local OS metrics within the remote execution context.Get-Volume -DriveLetter 'C'gathers volume statistics on the remote system's root partition.Format-Table -AutoSizeorganizes the deserialized objects returned from the fleet into a unified table.
./audit-server-fleet.ps1PSComputerName UptimeDays CDriveFreePct TotalRAM_GB
-------------- ---------- ------------- -----------
Server01 14.2 38.4 16.00
Server02 45.1 12.8 32.00
Server03 2.0 65.2 16.00