Skip to content

PowerShell Remoting

How PowerShell Remoting uses WS-Management (WinRM) to execute commands and interactive sessions on remote Windows and Linux systems securely over HTTP/HTTPS.

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.
Consolepowershell
Get-Service -Name "WinRM"
Outputplaintext
Status   Name   DisplayName
------   ----   -----------
Running  WinRM  Windows Remote Management (WS-Management)

Checking WinRM Configuration

Consolepowershell
Get-Item -Path "WSMan:\localhost\Client\TrustedHosts"
Outputplaintext
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 to Automatic.
  • Configures an active listener on 0.0.0.0 for ports 5985/5986.
  • Adds Windows Firewall exceptions for incoming WS-Management traffic.
  • Registers default PowerShell session configurations (Microsoft.PowerShell and Microsoft.PowerShell.Workflow).
Consolepowershell
Enable-PSRemoting -Force

where:

  • -Force suppresses confirmation prompts, useful for unattended setup.
  • -SkipNetworkProfileCheck allows remoting to be enabled on client versions of Windows connected to local networks categorized as "Public".
Outputplaintext
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.

Consolepowershell
Set-Item -Path "WSMan:\localhost\Client\TrustedHosts" -Value "SRV01,SRV02" -Force

where -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.

Consolepowershell
Enter-PSSession -ComputerName "SRV01"

where:

  • -ComputerName is the DNS hostname, NetBIOS name, or IP address of the target machine.
  • -Credential prompts for or passes authorized administrative credentials for the remote system (e.g., (Get-Credential)).
Outputplaintext
[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 on SRV01.
  • Exit-PSSession terminates the remote interactive session and returns the console to the local machine. Typing exit has 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.

Consolepowershell
Invoke-Command -ComputerName "SRV01" -ScriptBlock { Get-Process | Where-Object { $_.CPU -gt 5 } }

where:

  • -ComputerName is the target remote machine(s), accepts an array for multiple targets.
  • -ScriptBlock is the code to execute remotely.
Outputplaintext
ProcessName   CPU      PSComputerName
-----------   ---      --------------
chrome        12.3     SRV01

Running Against Multiple Computers

Consolepowershell
Invoke-Command -ComputerName "SRV01", "SRV02" -ScriptBlock { Get-Service -Name "WinRM" }
Outputplaintext
Status   Name   DisplayName                              PSComputerName
------   ----   -----------                              --------------
Running  WinRM  Windows Remote Management (WS-Manag...)  SRV01
Running  WinRM  Windows Remote Management (WS-Manag...)  SRV02

Passing 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:

Consolepowershell
$serviceName = "Spooler"
Invoke-Command -ComputerName "SRV01" -ScriptBlock {
    param($svc)
    Get-Service -Name $svc
} -ArgumentList $serviceName

where -ArgumentList is the values passed into the remote script block, matched positionally to param().

Using the $using: scope modifier:

invoke-command-demo.ps1powershell
$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, StartType

where:

  • $using:serviceName is the $using: scope modifier, which passes the value of a local variable from the caller's console into the remote script block.
  • PSComputerName is an automatic property added to all objects returning through PowerShell Remoting, identifying the origin node.
Consolepowershell
./invoke-command-demo.ps1
Outputplaintext
PSComputerName Service  Status  StartType
-------------- -------  ------  ---------
Server01       wuauserv Running Automatic
Server02       wuauserv Stopped    Manual
Server03       wuauserv Running    Manual

Persistent 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.

Consolepowershell
$session = New-PSSession -ComputerName "SRV01"

where $session stores a PSSession object representing the open, reusable connection.

Consolepowershell
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

persistent-session.ps1powershell
$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

$output

where:

  • New-PSSession establishes an ongoing background connection to the target system and returns a PSSession object.
  • -Session directs Invoke-Command to reuse an established persistent session instead of opening and closing a new connection.
  • Remove-PSSession explicitly closes the remote runspace and tears down network resources on both the client and server.
Consolepowershell
./persistent-session.ps1
Outputplaintext
Remote State: Initialized at 12:15:30

Closing a Persistent Session

Consolepowershell
Remove-PSSession -Session $session

Practical 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.

remote-health-check.ps1powershell
$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 $sessions

where:

  • New-PSSession -ComputerName $servers opens one persistent session per server in the array.
  • Invoke-Command -Session $sessions runs the same script block across all open sessions simultaneously.
Consolepowershell
./remote-health-check.ps1
Outputplaintext
ComputerName CPUCount FreeMemoryMB WinRMStatus PSComputerName
------------ -------- ------------ ----------- --------------
SRV01        4        3821         Running     SRV01
SRV02        8        7502         Running     SRV02

Fleet-Wide Health Audit

Administrators regularly perform parallel diagnostic sweeps across server farms to detect resource starvation, failing storage disks, and system uptimes.

audit-server-fleet.ps1powershell
$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 -AutoSize

where:

  • -ThrottleLimit 10 limits concurrent network operations to 10 nodes at a time to prevent saturating local client network buffers.
  • Get-CimInstance -ClassName Win32_OperatingSystem queries local OS metrics within the remote execution context.
  • Get-Volume -DriveLetter 'C' gathers volume statistics on the remote system's root partition.
  • Format-Table -AutoSize organizes the deserialized objects returned from the fleet into a unified table.
Consolepowershell
./audit-server-fleet.ps1
Outputplaintext
PSComputerName 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
Navigation