Cross-Platform SSH Remoting in PowerShell: Reusable Sessions, Key-Based Auth, and Reliable Cleanup
PowerShell 7+ makes SSH a first-class transport for remoting, which means you can open the same kind of predictable PowerShell sessions to Windows, Linux, and macOS using a single mental model. With key-based authentication, a single reusable session, and reliable cleanup, you get faster automation, fewer prompts, and consistent behavior across platforms.
In this guide, you'll set up SSH remoting for PowerShell, create and reuse sessions with New-PSSession -HostName, wrap execution with try/finally for cleanup, and apply practical security and performance tips you can lift straight into your scripts and CI/CD pipelines.
Why SSH Remoting with PowerShell?
- Cross-platform consistency: One transport (SSH) for Windows, Linux, and macOS.
- Safer auth: Strong, passphrase-protected keys instead of passwords.
- Lower overhead: Reuse an SSH-backed PSSession to avoid repeated handshakes.
- Firewall-friendly: Standard SSH port and tooling already allowed in many environments.
Prerequisites and Setup
Requirements
- PowerShell 7+ installed on both client and target hosts.
- OpenSSH client on your workstation; OpenSSH server on targets you connect to.
Install and configure OpenSSH
Windows (Target):
# Run as Administrator (PowerShell)
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
# Configure sshd to use PowerShell as a subsystem (C:\Program Files\PowerShell\7\pwsh.exe)
# Edit C:\ProgramData\ssh\sshd_config and add:
# Subsystem powershell "C:\\Program Files\\PowerShell\\7\\pwsh.exe" -sshs -NoLogo -NoProfile
Restart-Service sshd
Linux/macOS (Target):
# Ensure OpenSSH server is installed and running
# Configure PowerShell subsystem in /etc/ssh/sshd_config:
# Subsystem powershell /usr/bin/pwsh -sshs -NoLogo -NoProfile
# Save, then restart sshd
sudo systemctl restart sshd
Harden SSH for key-based auth
On each target's sshd_config, prefer keys and disable password logins when possible:
PubkeyAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no
In ~/.ssh/authorized_keys on the target, consider restrictions per key:
from="10.0.0.0/24",no-port-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... user@host
On your client, a minimal ~/.ssh/config entry improves ergonomics:
Host dev01
HostName dev01.example.com
User devops
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
Create, Reuse, and Clean Up a Session
Open one SSH-backed PSSession and reuse it for multiple commands to avoid repeated handshakes. Always wrap in try/finally so cleanup runs even on errors.
$s = New-PSSession -HostName 'dev01.example.com' -UserName 'devops' -KeyFilePath '~/.ssh/id_ed25519' -ErrorAction Stop
try {
$sys = Invoke-Command -Session $s -ScriptBlock {
if ($IsWindows) {
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
[pscustomobject]@{ Host=$env:COMPUTERNAME; OS=$os.Caption; Kernel=$os.Version }
} else {
[pscustomobject]@{ Host=$env:HOSTNAME; OS=$(uname -sr); Kernel=$(uname -r) }
}
}
$sys
} finally {
Remove-PSSession -Session $s -ErrorAction SilentlyContinue
}
This pattern gives you a single pleasant, predictable interface for Windows and non-Windows hosts alike, with faster execution and fewer interactive prompts.
Practical tips
- Use
-ErrorAction Stop: Fail fast if the session can't open; yourfinallystill runs. - Store keys safely: Prefer
ed25519keys with a passphrase; use anssh-agentfor non-interactive runs. - Pin host keys: Let SSH manage host trust in
~/.ssh/known_hostsand avoid-o StrictHostKeyChecking=noin production.
Performance: Reuse Sessions to Cut Handshakes
Establishing an SSH connection is relatively expensive. Keep a session open and run many commands through it:
# Naive: new session per command
$timeNewEach = Measure-Command {
1..10 | ForEach-Object {
$s = New-PSSession -HostName 'dev01.example.com' -UserName 'devops' -KeyFilePath '~/.ssh/id_ed25519'
Invoke-Command -Session $s -ScriptBlock { hostname }
Remove-PSSession $s
}
}
# Optimized: reuse a single session
$s = New-PSSession -HostName 'dev01.example.com' -UserName 'devops' -KeyFilePath '~/.ssh/id_ed25519'
$timeReuse = Measure-Command {
1..10 | ForEach-Object {
Invoke-Command -Session $s -ScriptBlock { hostname }
}
}
Remove-PSSession $s
"NewEach: $($timeNewEach.TotalSeconds)s"
"ReuseOne: $($timeReuse.TotalSeconds)s"
You'll typically see a substantial reduction in total time when reusing a session, especially over higher latency links.
Fan-Out to Multiple Hosts
You can scale the same pattern to a small fleet by creating a pool of sessions and invoking a script block across them. This keeps per-host handshakes to one and parallelizes the work.
$hosts = 'dev01.example.com','dev02.example.com','dev03.example.com'
$sessions = @()
try {
foreach ($h in $hosts) {
$sessions += New-PSSession -HostName $h -UserName 'devops' -KeyFilePath '~/.ssh/id_ed25519' -ErrorAction Stop
}
$results = Invoke-Command -Session $sessions -ScriptBlock {
if ($IsWindows) {
$os = (Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue)
[pscustomobject]@{ Host=$env:COMPUTERNAME; OS=$os.Caption; Uptime=(Get-CimInstance Win32_OperatingSystem).LastBootUpTime }
} else {
[pscustomobject]@{ Host=$env:HOSTNAME; OS=$(uname -sr); Uptime=$(uptime -p) }
}
}
$results | Format-Table -AutoSize
} finally {
$sessions | Remove-PSSession -ErrorAction SilentlyContinue
}
Use -ThrottleLimit on Invoke-Command if you're hitting many hosts and want to cap concurrent work to avoid saturating networks or endpoints.
Security and Reliability Best Practices
- Keys over passwords: Generate
ed25519keys and protect them with a passphrase. In CI, load them viassh-agentor a secure secret store. - Least privilege: Use a dedicated
devopsuser with only required rights. Avoid full admin/root unless needed. - Restrict keys: Consider
authorized_keysoptions likefrom=,no-port-forwarding, andcommand=to confine usage. - Known hosts hygiene: Pre-seed
known_hostsin automation to avoid trust prompts; rotate host keys carefully. - Profiles and predictability: The
-NoProfilein the subsystem reduces surprises from user profiles. Keep script behavior deterministic. - Error handling: Always use
try/finallywithRemove-PSSession. Consider logging with-Verboseand wrappingInvoke-Commandcalls in retry logic for transient network issues.
Troubleshooting
- Permission denied (publickey): Ensure your public key is in the target's
~/.ssh/authorized_keys, the private key path is correct, and file permissions are restrictive (chmod 600 ~/.ssh/id_ed25519,chmod 700 ~/.ssh). - Subsystem not found: Verify the
Subsystem powershell ...line insshd_configand thatpwshexists at that path. Restartsshd. - Host key changed: If SSH warns about a changed host key, investigate. Don't blindly remove entries; validate and then update
known_hosts. - Windows firewall: Allow inbound TCP/22 to
sshdon the target if you can't connect. - Path handling on Windows:
-KeyFilePathis local to the client. Use proper quoting for Windows paths, e.g.,-KeyFilePath "C:\\Users\\you\\.ssh\\id_ed25519".
What You Get
- Cross-platform: The same remoting pattern for Windows, Linux, and macOS.
- Predictable sessions: Deterministic behavior with subsystem-based
pwshand-NoProfile. - Safer auth: Keys and host pinning reduce credential risk.
- Lower overhead: Session reuse eliminates repeated SSH handshakes.
Copy-Paste Starter
Use this minimal, production-friendly pattern in scripts and pipelines:
$hostName = 'dev01.example.com'
$userName = 'devops'
$keyPath = '~/.ssh/id_ed25519'
$s = New-PSSession -HostName $hostName -UserName $userName -KeyFilePath $keyPath -ErrorAction Stop
try {
Invoke-Command -Session $s -ScriptBlock {
if ($IsWindows) {
Get-ChildItem Env: | Where-Object Name -in 'COMPUTERNAME','USERNAME'
} else {
[pscustomobject]@{ Host=$env:HOSTNAME; User=$(whoami); Shell=$SHELL }
}
}
} finally {
Remove-PSSession -Session $s -ErrorAction SilentlyContinue
}
Build consistent cross-platform remoting that feels the same everywhere and scales from one host to many. For deeper patterns, advanced security, and larger fleet orchestration, see the PowerShell Advanced Cookbook.
Read the PowerShell Advanced CookBook → https://www.amazon.com/PowerShell-Advanced-Cookbook-scripting-advanced-ebook/dp/B0D5CPP2CQ/