cheat sheet
PowerShell Remoting
Execute PowerShell against one or many remote hosts using WinRM or SSH transport, persistent sessions, credential management, and JEA.
PowerShell Remoting — Running Commands on Other Machines
What it is
PowerShell Remoting is the built-in transport layer in PowerShell that lets you execute commands against one or many remote machines as if you were sitting at their console, returning structured .NET objects rather than text. Microsoft ships it with every modern Windows host using WinRM (Windows Remote Management) as the default transport, and PowerShell 7+ adds SSH as an alternative — useful for Linux targets or any environment where WinRM is blocked. Reach for it whenever you need to run the same script across a fleet, gather inventory from many servers in parallel, or open a long-running interactive session without screen-sharing. Both transports remain fully supported in PowerShell 7.6 LTS (March 2026) — there are no current deprecation plans for WinRM, though the 2026 community recommendation is SSH-over-PowerShell-7 for new automation in cross-platform environments.
Install
PowerShell Remoting is a built-in feature on Windows. WinRM-based remoting just needs to be enabled; SSH-based remoting (PowerShell 7+) requires OpenSSH and a small sshd_config edit on the target.
# Enable WinRM listener on the target (run elevated)
Enable-PSRemoting -Force
# (Optional) install PowerShell 7 if not present — needed for SSH transport
winget install --id Microsoft.PowerShell -e
# (SSH transport, on Linux target) install pwsh and update sshd
sudo apt install -y openssh-server powershell
Output: (none — exits 0 on success)
Syntax
The two most common cmdlets are Invoke-Command (one-shot, fire-and-forget against one or many hosts) and Enter-PSSession (interactive REPL against a single host).
Invoke-Command -ComputerName <Host[]> -ScriptBlock { <code> } [-Credential <PSCredential>]
Enter-PSSession -ComputerName <Host> [-Credential <PSCredential>]
New-PSSession -ComputerName <Host[]> [-Credential <PSCredential>]
Output: (none — exits 0 on success)
Essential parameters
| Parameter | Meaning |
|---|---|
-ComputerName | One or more WinRM target hostnames/IPs |
-HostName | One or more SSH target hostnames (PowerShell 7+) |
-UserName | Username for SSH transport (no -Credential on SSH) |
-Credential | PSCredential object for WinRM auth |
-Authentication | Default, Negotiate, Kerberos, CredSSP, Basic |
-UseSSL | Encrypt WinRM over port 5986 (HTTPS) |
-Port | Override default port (5985 WinRM, 5986 WinRM-HTTPS, 22 SSH) |
-ScriptBlock | Code to execute remotely |
-FilePath | Local .ps1 to copy and run on the remote |
-ArgumentList | Positional arguments passed to the script block / file |
-Session | Reuse an existing PSSession instead of opening a new one |
-AsJob | Run asynchronously; returns a job handle |
-ThrottleLimit | Max concurrent connections (default 32) |
-ConfigurationName | Endpoint name on the remote (default Microsoft.PowerShell) |
Enable WinRM on a target
WinRM is the original PowerShell remoting transport, listening on TCP 5985 (HTTP) and 5986 (HTTPS). Enable-PSRemoting starts the service, opens the firewall, registers the default Microsoft.PowerShell session configuration, and trusts the loopback. Server SKUs have remoting on by default; desktop SKUs do not.
# Run from an elevated PowerShell on the target machine
Enable-PSRemoting -Force
# Verify the listener
winrm enumerate winrm/config/Listener
# On the calling machine, trust the target (workgroup or cross-domain)
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "myhost,fileserver01" -Force
# View what we trust right now
Get-Item WSMan:\localhost\Client\TrustedHosts
# Reset to empty (clears trust list)
Clear-Item WSMan:\localhost\Client\TrustedHosts -Force
Output (winrm enumerate winrm/config/Listener):
Listener
Address = *
Transport = HTTP
Port = 5985
Hostname
Enabled = true
URLPrefix = wsman
CertificateThumbprint
ListeningOn = 127.0.0.1, 192.168.1.42, ::1
Output (Get-Item WSMan:\localhost\Client\TrustedHosts):
WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Client
Type Name SourceOfValue Value
---- ---- ------------- -----
System.String TrustedHosts myhost,fileserver01
Invoke-Command — one-shot remoting
Invoke-Command opens a connection, runs the script block, returns its output, and closes the connection — the workhorse cmdlet for ad-hoc fleet operations. The block is executed on the remote machine, so any cmdlets and modules referenced must exist there, not locally.
# Single host
Invoke-Command -ComputerName myhost -ScriptBlock { Get-Service spooler }
# Fleet (parallel by default, up to -ThrottleLimit)
Invoke-Command -ComputerName myhost,fileserver01,web01 -ScriptBlock {
[pscustomobject]@{
Host = $env:COMPUTERNAME
Uptime = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
PSVer = $PSVersionTable.PSVersion.ToString()
}
}
# Throttle (max 5 concurrent)
$hosts = 1..50 | ForEach-Object { "web{0:00}" -f $_ }
Invoke-Command -ComputerName $hosts -ScriptBlock { hostname } -ThrottleLimit 5
# Run a local script on the remote (no need to copy it manually)
Invoke-Command -ComputerName myhost -FilePath C:\scripts\inventory.ps1
Output (Invoke-Command -ComputerName myhost -ScriptBlock { Get-Service spooler }):
Status Name DisplayName PSComputerName
------ ---- ----------- --------------
Running spooler Print Spooler myhost
Output (fleet uptime):
Host Uptime PSVer PSComputerName
---- ------ ----- --------------
MYHOST 2.04:32:18.4731234 5.1.22621.4391 myhost
FILESERVER01 41.18:09:22.0011235 7.4.6 fileserver01
WEB01 0.03:51:07.6512344 7.4.6 web01
Passing local data — $using:
Remote script blocks run in a fresh scope on the target and cannot see your local variables. The $using: prefix snapshots a local variable at invocation time and serializes it to the remote — the idiomatic way to pass data into the block without resorting to -ArgumentList.
$serviceName = "spooler"
$threshold = 100MB
Invoke-Command -ComputerName myhost -ScriptBlock {
$svc = Get-Service $using:serviceName
$mem = (Get-Process | Measure-Object WorkingSet -Sum).Sum
[pscustomobject]@{
Service = $svc.Name
Status = $svc.Status
OverThreshold = $mem -gt $using:threshold
}
}
# Equivalent with -ArgumentList (positional)
Invoke-Command -ComputerName myhost `
-ScriptBlock { param($name,$bytes) Get-Service $name; $bytes } `
-ArgumentList $serviceName, $threshold
Output:
Service Status OverThreshold PSComputerName
------- ------ ------------- --------------
spooler Running True myhost
Persistent sessions — New-PSSession
A PSSession is a long-lived connection you can reuse across many Invoke-Command or Enter-PSSession calls, avoiding the connect-disconnect overhead and preserving in-session state (variables, loaded modules, current location). Always Remove-PSSession when finished to release server-side resources.
# Open a session
$s = New-PSSession -ComputerName myhost -Credential (Get-Credential)
# Reuse it
Invoke-Command -Session $s -ScriptBlock { $procs = Get-Process; $procs.Count }
Invoke-Command -Session $s -ScriptBlock { $procs | Sort-Object CPU -Descending | Select-Object -First 5 }
# Open a session against multiple hosts
$fleet = New-PSSession -ComputerName web01,web02,web03
# Run the same command across all of them
Invoke-Command -Session $fleet -ScriptBlock { (Get-CimInstance Win32_OperatingSystem).LastBootUpTime }
# Inspect open sessions
Get-PSSession
# Clean up
Remove-PSSession $s
Get-PSSession | Remove-PSSession
Output (Get-PSSession):
Id Name ComputerName ComputerType State ConfigurationName Availability
-- ---- ------------ ------------ ----- ----------------- ------------
1 Runspace1 myhost RemoteMachine Opened Microsoft.PowerShell Available
2 Runspace2 web01 RemoteMachine Opened Microsoft.PowerShell Available
3 Runspace3 web02 RemoteMachine Opened Microsoft.PowerShell Available
4 Runspace4 web03 RemoteMachine Opened Microsoft.PowerShell Available
Interactive remoting — Enter-PSSession
Enter-PSSession drops you into an interactive shell on the remote host, mirroring the ssh experience but with PowerShell semantics — your prompt is rewritten to show the target, and every command runs there until you Exit-PSSession. Useful for exploration; for automation always prefer Invoke-Command so the script is reproducible.
# Open an interactive shell
Enter-PSSession -ComputerName myhost -Credential (Get-Credential)
# Prompt changes to: [myhost]: PS C:\Users\alicedev\Documents>
hostname
Get-Service spooler
Exit-PSSession
# Enter an existing session
$s = New-PSSession -ComputerName myhost
Enter-PSSession -Session $s
Output (interactive session prompt):
[myhost]: PS C:\Users\alicedev\Documents> hostname
myhost
[myhost]: PS C:\Users\alicedev\Documents> Get-Service spooler
Status Name DisplayName
------ ---- -----------
Running spooler Print Spooler
[myhost]: PS C:\Users\alicedev\Documents> Exit-PSSession
PS C:\Users\alicedev\Documents>
SSH transport — PowerShell 7+
PowerShell 7 introduces SSH as an alternative remoting transport — useful for cross-platform Windows-to-Linux scenarios, or when WinRM is unavailable. Targets need OpenSSH plus a Subsystem powershell entry in sshd_config. Authentication uses standard SSH key pairs (no Get-Credential prompt) and you use -HostName / -UserName instead of -ComputerName.
# On the SSH target (Linux), edit /etc/ssh/sshd_config:
# Subsystem powershell /usr/bin/pwsh -sshs -NoLogo
# Then restart sshd:
# sudo systemctl restart sshd
# Connect from Windows / macOS / Linux client
Enter-PSSession -HostName myhost.example.com -UserName alicedev
# One-shot via SSH
Invoke-Command -HostName myhost.example.com -UserName alicedev -ScriptBlock {
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5
}
# Persistent SSH session
$s = New-PSSession -HostName myhost.example.com -UserName alicedev -KeyFilePath ~/.ssh/id_ed25519
Invoke-Command -Session $s -ScriptBlock { (Get-Date), $PSVersionTable.OS }
Remove-PSSession $s
Output:
Friday, May 22, 2026 9:14:02 AM
Linux 6.5.0-26-generic #26-Ubuntu SMP PREEMPT_DYNAMIC
Credentials
The cleanest way to authenticate non-interactively is to capture a PSCredential once and reuse it. Get-Credential prompts for a password, returning an object whose Password field is a SecureString — encrypted in memory with DPAPI on Windows. Persist credentials with Export-Clixml; only the user that exported them on the same machine can decrypt the file.
# Prompt and stash for the duration of the session
$cred = Get-Credential alicedev
# Or build one programmatically (avoid plaintext in real scripts)
$secure = ConvertTo-SecureString "S3cret!" -AsPlainText -Force
$cred = [pscredential]::new("alicedev", $secure)
# Persist to disk (DPAPI — only this user on this machine can decrypt)
$cred | Export-Clixml -Path "$env:USERPROFILE\.creds\myhost.xml"
# Reload later
$cred = Import-Clixml -Path "$env:USERPROFILE\.creds\myhost.xml"
# Use the SecretManagement module for a vault-backed approach
Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore -Scope CurrentUser
Register-SecretVault -Name MyVault -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
Set-Secret -Name myhost-admin -Secret $cred
$cred = Get-Secret -Name myhost-admin
Output (Get-Credential):
PowerShell credential request
Enter your credentials.
User: alicedev
Password for user alicedev: ********
UserName Password
-------- --------
alicedev System.Security.SecureString
Double-hop and CredSSP
The "double-hop" problem: by default a remote session cannot delegate your credentials to a second remote machine, so things like Invoke-Command -ComputerName A -ScriptBlock { Get-ChildItem \\B\share } fail with access-denied even when you would have access locally. CredSSP authentication explicitly delegates credentials and resolves the issue, at the cost of allowing the first hop to act fully on your behalf — enable it only on machines you trust.
# On the client (your machine)
Enable-WSManCredSSP -Role Client -DelegateComputer "myhost" -Force
# On the first hop (target machine)
Enable-WSManCredSSP -Role Server -Force
# Now the double hop works
Invoke-Command -ComputerName myhost -Authentication CredSSP -Credential $cred -ScriptBlock {
Get-ChildItem \\fileserver01\share
}
# Audit who's delegated
Get-WSManCredSSP
# Disable when no longer needed
Disable-WSManCredSSP -Role Client
Disable-WSManCredSSP -Role Server
Output (Get-WSManCredSSP):
The machine is configured to allow delegating fresh credentials to the following target(s): wsman/myhost
This computer is not configured to receive credentials from a remote client computer.
Background jobs and async fleets
Invoke-Command -AsJob returns immediately with a job handle while the remote work runs in the background. Combined with Wait-Job, Receive-Job, and Get-Job, this scales to hundreds of targets without blocking the prompt and lets you collect output as each host reports back. PowerShell 7's ForEach-Object -Parallel is a lighter alternative when you don't need the full job lifecycle.
$hosts = "web01","web02","web03","web04","web05"
# Kick off async
$job = Invoke-Command -ComputerName $hosts -ScriptBlock {
Start-Sleep -Seconds (Get-Random -Min 1 -Max 5)
[pscustomobject]@{
Host = $env:COMPUTERNAME
Procs = (Get-Process).Count
Mem = [Math]::Round(((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory)/1MB, 2)
}
} -AsJob
# Poll until done
Wait-Job $job | Out-Null
# Collect results, preserving PSComputerName origin
$results = Receive-Job $job
$results | Sort-Object Procs -Descending | Format-Table -AutoSize
# PowerShell 7+ parallel without the Job machinery
$hosts | ForEach-Object -Parallel {
Invoke-Command -ComputerName $_ -ScriptBlock { hostname }
} -ThrottleLimit 10
Output:
Host Procs Mem PSComputerName
---- ----- --- --------------
WEB03 412 3.21 web03
WEB01 387 2.98 web01
WEB05 376 4.07 web05
WEB02 341 2.55 web02
WEB04 299 3.74 web04
Session configuration and JEA
Every WinRM connection lands in a "session configuration" (an endpoint) that defines which cmdlets, modules, and providers are available. The default endpoint (Microsoft.PowerShell) exposes everything. Just Enough Administration (JEA) lets you publish a constrained endpoint that runs as a privileged virtual account but only permits an allowlisted set of commands — the canonical way to grant help-desk staff "restart this service" rights without giving them admin.
# List endpoints exposed by the local machine
Get-PSSessionConfiguration
# Define a role capability (which cmdlets a role may run)
New-Item -ItemType Directory C:\JEA\Roles -Force | Out-Null
$role = @{
Path = "C:\JEA\Roles\HelpDesk.psrc"
VisibleCmdlets = "Restart-Service","Get-Service"
VisibleFunctions = "Get-Uptime"
}
New-PSRoleCapabilityFile @role
# Define a session config that maps users to that role
$session = @{
Path = "C:\JEA\HelpDesk.pssc"
SessionType = "RestrictedRemoteServer"
RunAsVirtualAccount = $true
RoleDefinitions = @{ "MYHOST\HelpDesk" = @{ RoleCapabilities = "HelpDesk" } }
}
New-PSSessionConfigurationFile @session
# Register the endpoint
Register-PSSessionConfiguration -Name JEA_HelpDesk -Path C:\JEA\HelpDesk.pssc -Force
# Connect to the constrained endpoint
Enter-PSSession -ComputerName myhost -ConfigurationName JEA_HelpDesk -Credential $cred
Output (Get-PSSessionConfiguration):
Name : microsoft.powershell
PSVersion : 5.1
StartupScript :
RunAsUser :
Permission : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
Name : JEA_HelpDesk
PSVersion : 5.1
StartupScript :
RunAsUser :
RunAsVirtualAccount : True
Permission : MYHOST\HelpDesk AccessAllowed
Copying files across a session
Copy-Item accepts -ToSession and -FromSession parameters to transfer files using an open PSSession — no need to set up an SMB share or SCP. Files are streamed over the same WinRM channel and decrypted/encrypted with the session's transport security.
$s = New-PSSession -ComputerName myhost
# Push a file from local to remote
Copy-Item C:\src\config.json -Destination C:\app\config.json -ToSession $s
# Pull a file from remote to local
Copy-Item C:\Logs\app.log -Destination C:\local\backup\ -FromSession $s
# Recursive directory push
Copy-Item C:\src\scripts -Destination C:\Tools\scripts -Recurse -ToSession $s
# Clean up
Remove-PSSession $s
Output: (none — exits 0 on success)
Disconnect and reconnect long jobs
A PSSession can be disconnected from the client while keeping its state alive on the server (idle timeout default: 7 days). Useful for kicking off a long-running task, closing your laptop, and reconnecting from another machine to grab the results. Not supported on SSH transport.
# Start a long task in a session
$s = New-PSSession -ComputerName myhost -Name LongImport
Invoke-Command -Session $s -ScriptBlock {
Start-Sleep 600
"imported"
} -AsJob
# Disconnect — task keeps running
Disconnect-PSSession $s
# Later, from any machine (same user identity)
$s = Get-PSSession -ComputerName myhost -Name LongImport | Connect-PSSession
Receive-PSSession $s
# Inspect disconnected sessions on the remote
Get-PSSession -ComputerName myhost -State Disconnected
Output (Get-PSSession -ComputerName myhost -State Disconnected):
Id Name ComputerName ComputerType State ConfigurationName Availability
-- ---- ------------ ------------ ----- ----------------- ------------
3 LongImport myhost RemoteMachine Disconnected Microsoft.PowerShell None
Common pitfalls
- Plaintext passwords in scripts — never
ConvertTo-SecureString "..." -AsPlainTextin committed code. UseGet-Credential,Import-Clixml, or theSecretManagementmodule. - Variable not seen on remote — wrap with
$using:varNameinside the script block, or pass via-ArgumentListwith a matchingparam(). - Trusted hosts wildcard everything —
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*'works but disables host validation. Scope it to specific names. - HTTP transport on the public Internet — WinRM HTTP encrypts auth but not always the body for non-Kerberos auth. Always use
-UseSSLover untrusted networks. - Double-hop access denied — first hop cannot reach a second remote resource. Solve with CredSSP, Kerberos delegation, or a
New-PSSessionfrom inside the script block. Enter-PSSessionin a script — interactive only; scripts must useInvoke-Command.- Forgetting to clean up
PSSessionobjects — they consume slots on the target. AlwaysRemove-PSSessionorGet-PSSession | Remove-PSSessionat the end of a script. - SSH transport on PS 5.1 — only PowerShell 7+ supports
-HostName/-UserName. Older targets need WinRM. - Module not loaded remotely — the remote endpoint may not have the modules you have locally. Use
Import-Moduleinside the script block, or useInvoke-Command -FilePathwith a self-bootstrapping script. - Profile not loaded —
$PROFILEis not sourced in remote sessions by default. Initialize aliases orSet-Locationcalls explicitly inside the script block.
Real-world recipes
Run a health check across 10 servers in parallel
Gather uptime, free memory, and the number of running services from every server in a list, then sort by uptime to find the longest-running box.
$hosts = "web01","web02","web03","web04","web05",
"db01","db02","cache01","queue01","monitor01"
$cred = Get-Credential alicedev
$report = Invoke-Command -ComputerName $hosts -Credential $cred -ScriptBlock {
$os = Get-CimInstance Win32_OperatingSystem
[pscustomobject]@{
Host = $env:COMPUTERNAME
UptimeDays = [Math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 1)
FreeMemGB = [Math]::Round($os.FreePhysicalMemory / 1MB, 2)
RunningSvcs = (Get-Service | Where-Object Status -eq Running).Count
}
} -ThrottleLimit 10 -ErrorAction Continue
$report |
Sort-Object UptimeDays -Descending |
Format-Table -AutoSize
Output:
Host UptimeDays FreeMemGB RunningSvcs PSComputerName
---- ---------- --------- ----------- --------------
MONITOR01 82.4 6.71 147 monitor01
DB02 54.1 4.20 132 db02
DB01 54.1 4.18 133 db01
WEB03 18.2 3.91 128 web03
WEB02 18.2 3.88 127 web02
WEB01 18.2 3.85 128 web01
QUEUE01 12.6 5.10 121 queue01
CACHE01 12.6 2.95 119 cache01
WEB05 3.0 4.07 128 web05
WEB04 3.0 3.74 128 web04
Patch and restart a service across a fleet
Push a new config file, restart the service, and verify it came back up — failing fast for any host that doesn't recover.
$hosts = "web01","web02","web03"
$cred = Import-Clixml "$env:USERPROFILE\.creds\webfleet.xml"
$sessions = New-PSSession -ComputerName $hosts -Credential $cred
# 1. Push config
$sessions | ForEach-Object {
Copy-Item .\nginx.conf -Destination 'C:\nginx\conf\nginx.conf' -ToSession $_
}
# 2. Restart service and check status
$results = Invoke-Command -Session $sessions -ScriptBlock {
Restart-Service nginx -Force
Start-Sleep 2
[pscustomobject]@{
Host = $env:COMPUTERNAME
Status = (Get-Service nginx).Status
}
}
# 3. Tear down
Remove-PSSession $sessions
# 4. Report
$results | Format-Table -AutoSize
if ($results.Status -contains 'Stopped') { throw "One or more hosts failed to restart" }
Output:
Host Status PSComputerName
---- ------ --------------
WEB01 Running web01
WEB02 Running web02
WEB03 Running web03
Collect Windows event-log errors from every domain controller
Query each DC for System log errors in the last hour and produce a single sorted table — useful as a quick triage when something is going wrong.
$dcs = (Get-ADDomainController -Filter *).HostName
Invoke-Command -ComputerName $dcs -ScriptBlock {
$cutoff = (Get-Date).AddHours(-1)
Get-WinEvent -FilterHashtable @{ LogName='System'; Level=1,2; StartTime=$cutoff } |
Select-Object TimeCreated, Id, LevelDisplayName, ProviderName, Message
} -ThrottleLimit 20 -ErrorAction SilentlyContinue |
Sort-Object TimeCreated -Descending |
Select-Object PSComputerName, TimeCreated, Id, LevelDisplayName, ProviderName, Message |
Out-GridView -Title "DC errors in the last hour"
Output (sample — opens an interactive grid):
PSComputerName TimeCreated Id LevelDisplayName ProviderName Message
-------------- ----------- -- ---------------- ------------ -------
DC02 5/24/2026 9:12:03 1014 Warning DNS Client …
DC01 5/24/2026 9:05:47 4625 Error Microsoft-Win… An account failed to log on.
Bootstrap a fresh server via SSH-PSRemoting
Use PowerShell 7's SSH transport to apply a baseline configuration to a brand-new Linux host without ever opening a separate ssh session.
$h = "myhost.example.com"
$session = New-PSSession -HostName $h -UserName alicedev -KeyFilePath ~/.ssh/id_ed25519
Invoke-Command -Session $session -ScriptBlock {
if (-not (Test-Path '/opt/app')) { sudo mkdir -p /opt/app }
Set-Location /opt/app
# Drop a config file
@"
listen 0.0.0.0:8080
log-level info
"@ | Out-File config.cfg -Encoding utf8
}
# Push the binary
Copy-Item .\app -Destination /opt/app/app -ToSession $session
# Mark executable and start it under systemd
Invoke-Command -Session $session -ScriptBlock {
sudo chmod +x /opt/app/app
sudo systemctl enable --now app.service
systemctl status app.service --no-pager
}
Remove-PSSession $session
Output:
* app.service - app worker
Loaded: loaded (/etc/systemd/system/app.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-05-26 09:18:11 UTC; 2s ago
Main PID: 14823 (app)
Tasks: 4 (limit: 4567)
Memory: 5.6M
CPU: 12ms