PowerShell “Remoting” enables you to create a shell or execute commands on a remote machine. Basically, it’s the PowerShell equivalent of the ssh
command. There are certain aspects of it that feel rather well-designed and almost novel that make it well worth learning. Especially if your environment is heavily Windows-based.
Setup for Windows Host/Client
PowerShell, being “native” to Windows, is relatively easy on Windows 10. The requirements basically boil down to running Enable-PSRemoting in an admin powershell:
Enable-PSRemoting -Force
Setup for Linux/Mac Clients
PowerShell Core is available for multiple platforms including Linux and Mac. But PowerShell Remoting from a Linux/Mac client to a Windows host needs to use SSH instead of WinRM. This requires:
- Install SSHD (OpenSSH daemon) on Windows Host
- Test Powershell Remoting
- Optional Configuration
- Copy Client Key to Host
Most of this comes from official docs on OpenSSH installation and SSH remoting.
Install SSHD on Windows Host
Everything here needs to be done as admin.
Get-WindowsCapability -Online | ? Name -like 'OpenSSH*'
# Install listed `OpenSSH.Server~~~~X.X.X.X`. E.g.:
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Depending on your environment you may get:
Add-WindowsCapability failed. Error code = 0x800f0954
. If so, install it manually:
- Download latest Win32-OpenSSH release
- Run
install-sshd.ps1
Edit $env:ProgramData\ssh\sshd_config
:
# Uncomment the following
PasswordAuthentication yes
PubkeyAuthentication yes
# Add the following
Subsystem powershell c:/progra~1/powershell/7/pwsh.exe -sshs -NoLogo -NoProfile
Note the use of 8.3 path to avoid spaces on account of this issue. To verify the 8.3 name:
Get-CimInstance Win32_Directory -Filter 'Name="C:\\Program Files"' |
Select-Object EightDotThreeFileName
Finally, restart sshd:
Restart-Service sshd
Test Powershell Remoting
From Mac/Linux Powershell:
Enter-PSSession WindowsHostnameOrIP
If it fails with:
Enter-PSSession: This parameter set requires WSMan, and no supported WSMan client library was found. WSMan is either not installed or unavailable for this system.
Explicitly use SSH or try the workaround of installing an older version of OpenSSL:
Enter-PSSession WindowsHostnameOrIP -SSHTransport
It also works from sh/bash/zsh/et al. but this probably results in cmd.exe
shell on the remote Windows host:
ssh WindowsUsername@WindowsHostnameOrIP
# Or, if client/host usernames are the same
ssh WindowsHostnameOrIP
Optional Configuration
When you ssh into a Windows host the default shell is cmd.exe
*shudder*. To change the default shell to powershell configure SSHD on the Windows host:
# Again, 8.3 path to executable
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "c:/progra~1/powershell/7/pwsh.exe" -PropertyType String -Force
# Have sshd service start automatically
Set-Service -Name sshd -StartupType Automatic
To avoid having to specify WindowsUsername@...
with ssh, you can set a per-host default in ~/.ssh/config
on Linux/Mac:
Host WindowsHostnameOrIP
HostName WindowsHostnameOrIP
User WindowsUsername
Copy Client Public Key to Host
Techinically this is optional, but key-based authentication is too convenient to overlook. We assume you’ve already generated a key-pair on the client with ssh-keygen
. You can then follow the PowerShell docs to deploy the key.
Copy client public key to host (NB: use single-quotes to avoid local shell expansion of variables, and replace id_rsa.pub
with name of your public key):
# If Windows host doesn't already have `authorized_keys` file
scp $HOME/.ssh/id_rsa.pub WindowsHostnameOrIP:"C:\Users\WindowsUsername\.ssh\authorized_keys"
# OR, if default Windows SSH shell is Powershell, you can append to the existing file or create it:
ssh WindowsHostnameOrIP '$Input | Add-Content $env:Home/.ssh/authorized_keys' < $HOME/.ssh/id_rsa.pub
# For completeness; for Linux/Mac hosts:
ssh-copy-id HostnameOrIP
# OR
ssh HostnameOrIP 'cat >> $HOME/.ssh/authorized_keys' < $HOME/.ssh/id_rsa.pub
In the above, $Input
is an automatic variable containing stdin (from < $HOME/.ssh/id_rsa.pub
).
Permissions must be set accordingly (SO):
- References to OpenSSHUtils should be ignored because it’s deprecated
- Right-click
authorized_keys
, Properties > Security > Advanced - Click Disable Inheritance, and “Convert inherited permissions into explicit permissions on this object” if prompted
- Remove all permissions EXCEPT for
SYSTEM
and yourself, both of which should have Full control - In
$env:ProgramData\ssh\sshd_config
removeMatch Group administrators
lines, andRestart-Service sshd
- Right-click
Test you can connect from Linux/Mac client to Windows host without inputting password:
# In Linux/Mac PowerShell
Enter-PSSession WindowsHostnameOrIP -SSHTransport
# OR
ssh WindowsHostnameOrIP
Commands
There’s several good overview documents on remoting commands:
Get-Credential enables you to get user credentials e.g. when not using SSH for authentication:
$cred = Get-Credential -UserName domain\username
This is most useful when remoting between a Windows client and host. This can be passed to most commands below by adding -Credential $cred
.
If using SSH and not using key-based authentication or needing a different username, most of the commands below also accept a -UserName username
argument.
Enter-PSSession creates an interactive connection giving you a shell on a host. This is comparable to ssh-ing into a machine:
Enter-PSSession WindowsHostnameOrIP
# OR, if required by Linux/Mac client, explicitly specify to use SSH
Enter-PSSession WindowsHostnameOrIP -SSHTransport
# Leave the interactive session
Exit-PSSession
# OR
<ctrl+d>
Example:
PS /Users/mac_user> Enter-PSSession win_hostname -SSHTransport
[win_hostname]: PS C:\Users\win_user\Documents>
Invoke-Command can be used to run commands or scripts on one or more hosts:
# Execute command `Hostname` on host
Invoke-Command -ComputerName hostname -ScriptBlock { Hostname }
Invoke-Command -SSHTransport -HostName hostname -ScriptBlock { Hostname }
# Execute multiple commands on host
Invoke-Command -SSHTransport -HostName hostname -ScriptBlock { Hostname; Get-Date }
# Run remote script on host
Invoke-Command -ComputerName hostname -ScriptBlock { c:\path\remote.ps1 }
Invoke-Command -SSHTransport -HostName hostname -ScriptBlock { c:\path\remote.ps1 }
# Run local script on host
Invoke-Command -FilePath ./local.ps1 -ComputerName hostname
Invoke-Command -FilePath ./local.ps1 -SSHTransport -HostName hostname
# Execute command `Hostname` on multiple hosts
Invoke-Command -ComputerName hostname0, hostname1 -ScriptBlock { Hostname }
Invoke-Command -SSHTransport -HostName hostname0, hostname1 -ScriptBlock { Hostname }
# You can likewise run local/remote scripts on multiple hosts
Additional information about ScriptBlock.
New-PSSession creates a “persistent” connection that can re-used to run multiple remote commands:
# Create session to host
$session = New-PSSession -ComputerName hostname
$session = New-PSSession -SSHTransport -HostName hostname
# List sessions
Get-PSSession
# Re-use session to run multiple commands/scripts
Invoke-Command -Session $session -ScriptBlock { Hostname }
Invoke-Command -Session $session -ScriptBlock { Hostname; $d = Get-Date }
# Variables are created in each session and can subsequently be reused
Invoke-Command -Session $session -ScriptBlock { $d; c:\path\remote.ps1 }
Invoke-Command -Session $session -FilePath ./local.ps1
# Create session to multiple hosts
$session2 = New-PSSession -SSHTransport -HostName hostname1, hostname2
# Use multiple single-host sessions
Invoke-Command -Session $session,$session -ScriptBlock { Hostname }
# Use multiple multi-host sessions
Invoke-Command -Session ($session2+$session) -ScriptBlock { Hostname }
That last one is tricky. New-PSSession
with a single host returns a PSSession
, but with multiple hosts returns an Object[]
(array of PSSession
s). You’ll have to concatenate them otherwise it fails with Cannot convert 'System.Object[]' to the type 'System.Management.Automation.Runspaces.PSSession' required by parameter 'Session'
.
Variables
Using remote vs. local variables is surprisingly painless. To use a local variable prefix the name with $using:
, otherwise it’s remote:
Invoke-Command -Session $session -ScriptBlock { $home; $using:home }
# Output
C:\Users\win_user
/Users/mac_user