How To

PowerShell Remoting over SSH from Linux to Windows Server

Your Linux box and your Windows servers already share one protocol: SSH. PowerShell remoting over SSH uses it, so you can run Enter-PSSession or Invoke-Command from a Ubuntu or Rocky machine against Windows Server and get real PowerShell objects back. There’s no WinRM listener, no HTTPS certificate and no TrustedHosts list to maintain. The piece that ties it together is one Subsystem line in the Windows sshd_config, and where you put that line decides who can connect.

Original content from computingforgeeks.com - post 180606

This guide wires a Linux client to Windows Server 2025 end to end, starting with PowerShell 7 on both sides and the pwsh SSH subsystem. From there we move from password login to key-based auth for administrators and normal users, then interactive sessions and Invoke-Command fan-out across Windows and Linux hosts in a single call. We also reproduced the errors that stop most first attempts and kept the exact messages. We confirmed the setup working with PowerShell 7.6 on Windows Server 2025 and an Ubuntu 26.04 client in October 2026.

Follow the request from pwsh to sshd

When you run Invoke-Command -HostName, PowerShell on the Linux side starts the system ssh client and asks the remote sshd for a subsystem named powershell. On Windows, sshd looks that name up in C:\ProgramData\ssh\sshd_config and launches pwsh.exe -sshs, which speaks the PowerShell remoting protocol over the SSH channel’s stdin and stdout. Authentication, encryption and host key checks all belong to OpenSSH, so everything you know about SSH keys still applies, and nothing from WinRM does.

That split explains most of the setup. The wiring runs in this order:

  1. Install PowerShell 7 on the Linux client and on the Windows server.
  2. Start sshd on Windows and confirm port 22 is open.
  3. Add the powershell subsystem line to the Windows sshd_config.
  4. Test the round trip with a password.
  5. Install the client’s public key in the right file for the account type.

Windows PowerShell 5.1 cannot take part on either end, because the SSH parameter set only exists in PowerShell 6 and later. Microsoft’s SSH remoting documentation lists PowerShell 6 or higher plus SSH on every machine as the only requirements.

Prerequisites

Our lab was one Windows Server 2025 Datacenter VM (win2025-srv) and one Ubuntu 26.04 VM (ops01) on the same subnet. You need roughly the same:

  • Windows Server with an administrator account. We tested on a fresh Windows Server 2025 install; 2022 and 2019 need the OpenSSH capability added first.
  • A Linux client (Ubuntu, Debian, Rocky, RHEL) with OpenSSH client and sudo.
  • TCP 22 reachable from the client to the server.
  • A name for the server the client can resolve, through DNS or /etc/hosts.

We used a hosts entry because the lab had no DNS zone for the Windows box. Replace the address with your server’s IP:

echo "10.0.1.50 win2025-srv" | sudo tee -a /etc/hosts

Every Windows command in this guide runs in an elevated PowerShell window on the server. Every Linux command runs as a normal user on the client, with sudo only where shown.

Install PowerShell 7 on the Linux client

On the Linux side you only need the pwsh binary. Our PowerShell on Ubuntu and Debian guide covers the Microsoft repository method, and the RHEL family install does the same for Rocky and AlmaLinux. On Ubuntu 26.04 that repository method failed for us, though. Microsoft’s install page lists 26.04 as supported, and the packages-microsoft-prod.deb for 26.04 installs fine, but the 26.04 repository it adds had no powershell package when we tested:

sudo apt-get install -y powershell

apt reads the Microsoft repository and still comes back empty:

Hit:3 https://packages.microsoft.com/ubuntu/26.04/prod resolute InRelease
Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package powershell

The same repository did list powershell-preview at the time, so do not let tab completion talk you into a preview build on a management host. If you hit the same thing, install the .deb that the PowerShell team attaches to every GitHub release. These three lines detect the latest stable version, download the matching package and let apt pull its dependencies:

PS_VER=$(curl -s https://api.github.com/repos/PowerShell/PowerShell/releases/latest | grep -m1 '"tag_name"' | cut -d '"' -f 4 | sed 's/^v//')
wget "https://github.com/PowerShell/PowerShell/releases/download/v${PS_VER}/powershell_${PS_VER}-1.deb_amd64.deb" -O /tmp/pwsh.deb
sudo apt-get install -y /tmp/pwsh.deb

Check the version before moving to the server, since the client and server don’t have to match exactly but staying on the same 7.x line avoids surprises with serialized objects.

pwsh -v

Our Ubuntu client reported the current stable release from the GitHub package we just installed.

PowerShell 7.6.9

A package installed from a .deb file will not update with apt upgrade. Once Microsoft publishes the package in the 26.04 repository, switching to the repo method gives you updates with the rest of the system.

Install PowerShell 7 on Windows Server

Windows Server 2025 ships Windows PowerShell 5.1 only, and 5.1 cannot serve as the SSH remoting endpoint. Install PowerShell 7 from the official MSI. The snippet below asks the GitHub API for the latest release, downloads the x64 MSI and installs it silently with PowerShell added to PATH and updates through Microsoft Update turned on:

$ProgressPreference = 'SilentlyContinue'
$v = (Invoke-RestMethod https://api.github.com/repos/PowerShell/PowerShell/releases/latest).tag_name.TrimStart('v')
Invoke-WebRequest "https://github.com/PowerShell/PowerShell/releases/download/v$v/PowerShell-$v-win-x64.msi" -OutFile "$env:TEMP\pwsh.msi"
Start-Process msiexec.exe -ArgumentList "/i `"$env:TEMP\pwsh.msi`" /qn ADD_PATH=1 USE_MU=1 ENABLE_MU=1" -Wait

Confirm the binary sits where the subsystem line will point. The path matters more than the version here:

& 'C:\Program Files\PowerShell\7\pwsh.exe' -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'

The server returned the same release as the Linux client, so both ends of the link now speak the same remoting protocol version.

7.6.9

Leave Windows PowerShell 5.1 in place, because scripts and modules written for it keep calling powershell.exe. PowerShell 7 installs side by side under C:\Program Files\PowerShell\7 and doesn’t replace it.

Start sshd and open port 22 on Windows

On Windows Server 2025, OpenSSH Server is installed but not enabled. On 2022 and 2019 you first add it with Add-WindowsCapability, which the OpenSSH Server on Windows guide walks through. Start the service and make it survive reboots:

Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
Get-Service sshd | Format-Table Status, Name, StartType -AutoSize

You want Running and Automatic side by side, because a service left on Manual comes back stopped after the next patch reboot.

 Status Name StartType
 ------ ---- ---------
Running sshd Automatic

The OpenSSH capability creates its own inbound firewall rule. Check that it’s enabled before you blame anything else for a timeout:

Get-NetFirewallRule -Name OpenSSH-Server-In-TCP | Format-Table Name, Enabled, Direction, Action -AutoSize

An enabled inbound allow rule on our server looked like this, with all three columns set the way the client needs them.

Name                  Enabled Direction Action
----                  ------- --------- ------
OpenSSH-Server-In-TCP    True   Inbound  Allow

If the rule is missing or you want to limit port 22 to your management subnet, the Windows Server firewall port guide shows how to scope a rule by remote address. Note that ICMP was blocked on our server while TCP 22 answered, so test with nc -zv win2025-srv 22 rather than ping.

Add the pwsh SSH subsystem on Windows

The subsystem line tells sshd which program answers a powershell subsystem request. Microsoft documents a bug in OpenSSH for Windows with spaces in the subsystem path, so the documented form uses the 8.3 short name of C:\Program Files. Short names are an NTFS feature that can be switched off per volume, so confirm yours exists first:

Get-CimInstance Win32_Directory -Filter 'Name="C:\\Program Files"' | Select-Object EightDotThreeFileName

A system drive with short names enabled returns progra~1, which is the folder name the subsystem line will use.

EightDotThreeFileName
---------------------
c:\progra~1

If the field comes back empty, short names are off on that volume. Microsoft’s other option, which its docs call the preferred one, is a symbolic link such as C:\ProgramData\ssh\pwsh.exe pointing at the real binary, used in place of the short path. We also tried the quoted long path, "C:\Program Files\PowerShell\7\pwsh.exe", on the OpenSSH 9.5p2 build that ships with Server 2025, and it worked. We kept the short form below because it needs no extra file and matches Microsoft’s example line.

Open the server config in Notepad from the elevated window. Only SYSTEM and Administrators have write access to it (Authenticated Users get read and execute), so an editor started without elevation can open the file but not save it.

notepad C:\ProgramData\ssh\sshd_config

Find the existing Subsystem sftp line and add the PowerShell line directly under it, so the block reads:

# override default of no subsystems
Subsystem	sftp	sftp-server.exe
Subsystem	powershell	c:/progra~1/powershell/7/pwsh.exe -sshs -NoLogo

Do not append the line at the bottom of the file. The default Windows sshd_config ends with a Match Group administrators block, and every line after a Match belongs to that block. sshd starts happily with the subsystem inside it, which makes the mistake hard to spot. When we tested that placement, Administrator connected while a non-admin account got subsystem request failed on channel 0. Restart sshd to load the change:

Restart-Service sshd
Select-String -Path C:\ProgramData\ssh\sshd_config -Pattern '^(Subsystem|AllowGroups|Match)' | ForEach-Object { '{0}: {1}' -f $_.LineNumber, $_.Line }

The line numbers prove the order. The powershell subsystem has to sit above the Match line:

79: Subsystem	sftp	sftp-server.exe
80: Subsystem	powershell	c:/progra~1/powershell/7/pwsh.exe -sshs -NoLogo
82: AllowGroups administrators "openssh users"
90: Match Group administrators

Line 82 matters for non-admin logins. The default config on our 2025 server allows only members of administrators and OpenSSH Users, so a standard account must join that group before it can connect at all. On 2022 or 2019, where you add OpenSSH yourself, run the same Select-String check before assuming that line is there. We added our local test user with Add-LocalGroupMember -Group 'OpenSSH Users' -Member eugene, and a domain user or group goes into the same local group (our lab had no domain, so we only tested the local account).

Test the round trip with a password

Prove the plumbing before you add keys, because a password login separates a broken subsystem from a broken key file. From the Linux client, run one remote command against the server:

pwsh -NoLogo -NoProfile -Command 'Invoke-Command -HostName win2025-srv -UserName Administrator -ScriptBlock { "{0} as {1}, PS {2}" -f $env:COMPUTERNAME, (whoami), $PSVersionTable.PSVersion }'

On the first connection the ssh client asks you to accept the server’s host key, then prompts for the Windows password. A working subsystem returns the remote computer name, the account and the PowerShell version that answered:

The authenticity of host 'win2025-srv (10.0.1.50)' can't be established.
ED25519 key fingerprint is: SHA256:5TtG20lumY00uYkLhc6Wa4BfY9tvqzOVwjejM4+wX74
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Administrator@win2025-srv's password:
WIN2025-SRV as win2025-srv\administrator, PS 7.6.9

Run the same command with -UserName eugene (or your standard account) and you should get WIN2025-SRV as win2025-srv\eugene, PS 7.6.9. If the admin works and the standard user fails with subsystem request failed, the subsystem line is inside the Match block. If the standard user gets Permission denied with the right password, it’s missing from OpenSSH Users.

Passwords don’t work for scripts, and Windows decides where it reads your public key by group membership. A member of the local Administrators group is checked against C:\ProgramData\ssh\administrators_authorized_keys only. A standard user is checked against C:\Users\<name>\.ssh\authorized_keys. Putting an admin’s key in their own profile is the most common mistake, and we reproduced it on purpose: the key sat in C:\Users\Administrator\.ssh\authorized_keys and sshd ignored it.

Create a key pair on the Linux client if you don’t have one:

ssh-keygen -t ed25519 -C eugene@ops01 -f ~/.ssh/id_ed25519

You can install the key through the password session you just tested, which saves copying files by hand. This appends the public key to the administrators file and resets its ACL to the two entries OpenSSH accepts, SYSTEM and Administrators, as Microsoft’s key management page prescribes:

pwsh -NoLogo -NoProfile -Command '$pub = Get-Content ~/.ssh/id_ed25519.pub; Invoke-Command -HostName win2025-srv -UserName Administrator -ArgumentList $pub -ScriptBlock { param($key) $file = "C:\ProgramData\ssh\administrators_authorized_keys"; Add-Content -Path $file -Value $key; icacls.exe $file /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F" }'

After the password prompt, icacls confirms that it rewrote the permissions on the one file it touched.

processed file: C:\ProgramData\ssh\administrators_authorized_keys
Successfully processed 1 files; Failed processing 0 files

A standard account reads its own $HOME\.ssh\authorized_keys instead, and the .ssh folder usually does not exist yet on a fresh profile. This creates it and adds the key, using a password session opened as that user (eugene in our lab):

pwsh -NoLogo -NoProfile -Command '$pub = Get-Content ~/.ssh/id_ed25519.pub; Invoke-Command -HostName win2025-srv -UserName eugene -ArgumentList $pub -ScriptBlock { param($key) New-Item -ItemType Directory -Force -Path $HOME\.ssh | Out-Null; Add-Content -Force -Path $HOME\.ssh\authorized_keys -Value $key }'

Now test with password fallback switched off. The -Options hashtable passes any ssh option through, and BatchMode=yes makes ssh fail instead of prompting, which is exactly what you want to know before a script depends on it:

pwsh -NoLogo -NoProfile -Command 'Invoke-Command -HostName win2025-srv -UserName Administrator -KeyFilePath ~/.ssh/id_ed25519 -Options @{BatchMode="yes"} -ScriptBlock { hostname }'

Getting the hostname back with no password prompt means the key did the work, and the same call is now safe inside a cron job or CI step.

WIN2025-SRV

To stop typing the user and key on every call, give the server a block in ~/.ssh/config on the client. PowerShell hands the connection to the system ssh client, so the file applies to -HostName exactly as it does to plain ssh. Open the file in your editor.

vim ~/.ssh/config

Add the host block we used in the lab, changing the user and key path if yours differ.

IgnoreUnknown WarnWeakCrypto

Host win2025-srv
    User Administrator
    IdentityFile ~/.ssh/id_ed25519
    WarnWeakCrypto no-pq-kex

After a chmod 600 ~/.ssh/config, Invoke-Command -HostName win2025-srv -ScriptBlock { whoami } returned win2025-srv\administrator with no user or key arguments. The WarnWeakCrypto line silences a post-quantum warning covered in the errors section, but the option only exists from OpenSSH 10.1. Older clients such as Ubuntu 24.04 (OpenSSH 9.6) or Rocky 10 (9.9) reject an unknown keyword anywhere in the file and refuse every connection, which is why IgnoreUnknown WarnWeakCrypto has to sit at the very top. Check your client with ssh -V; below 10.1 you can drop both lines, since those clients never print the warning.

Open an interactive session with Enter-PSSession

Enter-PSSession gives you a remote prompt, the closest thing to an RDP session you’ll get from a terminal. It takes the same -HostName, -UserName and -KeyFilePath parameters, and the prompt changes to show which account and host you’re on:

Enter-PSSession -HostName win2025-srv -UserName Administrator -KeyFilePath ~/.ssh/id_ed25519

Run it from a pwsh prompt on the Linux client. Every command after that runs on the Windows server, and the objects come back formatted as if you were sitting at its console:

Enter-PSSession over SSH from an Ubuntu 26.04 client to Windows Server 2025 showing the sshd service running

Type exit to drop back to the local prompt, and know two limits before you live in these sessions. Your PowerShell profile isn’t loaded over SSH remoting, so dot-source it by hand if you depend on aliases. And because the server’s sshd owns authentication, there are no JEA or custom endpoint configurations on this transport, while Disconnect-PSSession is Windows-only and is not available from a Linux client at all.

Fan out with Invoke-Command

-HostName takes an array, and each entry can carry its own user in user@host form. That lets one call reach Windows and Linux machines together, as long as every target runs PowerShell 7 with the subsystem configured. A Linux target needs PowerShell 7 installed the same way as the client, your public key in its ~/.ssh/authorized_keys (ssh-copy-id eugene@ops01 does that), and the subsystem, which is one line in a drop-in file. We used ops01 itself as the Linux target:

echo "Subsystem powershell /usr/bin/pwsh -sshs -NoLogo" | sudo tee /etc/ssh/sshd_config.d/50-powershell.conf
sudo sshd -t && sudo systemctl reload ssh

Ubuntu calls the service ssh, while Rocky and RHEL call it sshd, so reload that name there instead. The target also needs openssh-server, which Ubuntu Desktop doesn’t install, and if systemd answers that the unit is not active (recent Ubuntu starts sshd through a socket), run sudo systemctl restart ssh instead of the reload. With both targets ready, save the fan-out script on the client:

vim ~/fanout.ps1

This is the exact script we ran. It asks every host for its OS, PowerShell version, login and uptime, then prints one table:

$targets = 'Administrator@win2025-srv', 'eugene@ops01'
Invoke-Command -HostName $targets -KeyFilePath ~/.ssh/id_ed25519 -ScriptBlock {
    [pscustomobject]@{
        OS        = [System.Runtime.InteropServices.RuntimeInformation]::OSDescription
        PSVersion = $PSVersionTable.PSVersion.ToString()
        User      = [Environment]::UserName
        Uptime    = (Get-Uptime).ToString('d\.hh\:mm')
    }
} | Format-Table PSComputerName, OS, PSVersion, User, Uptime -AutoSize

Run it with pwsh -NoLogo -NoProfile -File ~/fanout.ps1. PowerShell adds PSComputerName to every object it brings back, which is how you tell the rows apart:

Invoke-Command fan-out over SSH returning results from Windows Server 2025 and Ubuntu 26.04 in one table

The whole two-host run took 3.2 seconds of wall clock time. Most of that is connection setup, not the script block, and it shows up as soon as you loop. We timed ten calls against the Windows server both ways, once with a fresh connection per call and once through a session opened with New-PSSession:

MethodCallsTotal secondsPer call
Invoke-Command -HostName each time107.56about 0.76 s
One reused PSSession100.17about 17 ms

That is a 44x difference on a quiet LAN, and it grows with latency because every new connection repeats the SSH key exchange and starts a fresh pwsh.exe on the server. For anything that touches the same hosts more than a couple of times, open the sessions once and pass them around. A session also carries files, which saves you a separate scp step. We first created a small test file with echo "hello from ops01" > /tmp/report.txt, then copied it:

$s = New-PSSession -HostName win2025-srv
Copy-Item /tmp/report.txt -Destination C:\Windows\Temp\ -ToSession $s
Invoke-Command -Session $s -ScriptBlock { Get-Item C:\Windows\Temp\report.txt | Select-Object Name, Length, LastWriteTime }
Remove-PSSession $s

The file landed on the server with its 17 bytes intact:

Name           : report.txt
Length         : 17
LastWriteTime  : 10/30/2026 19:56:03
PSComputerName : win2025-srv

Our New-PSSession listed SSH under Transport, which is a quick way to confirm a session isn’t quietly using WinRM. If you manage Windows fleets with Ansible instead, the Ansible for Windows Server tutorial covers that route. Plain PowerShell remoting over SSH is the lighter choice when you only need ad hoc commands from a Linux jump host.

Every message below came out of this lab, and PowerShell wraps most SSH failures in the same OpenError text, so the useful line is usually the one ssh printed just above it.

subsystem request failed on channel 0

You authenticated, but sshd has no powershell subsystem for this login. We hit it twice, once before adding the line at all and once with the line appended below Match Group administrators, where only admins got it. The full output looks like this:

subsystem request failed on channel 0
OpenError: [win2025-srv] The SSH client session has ended with error message: The SSH transport process has abruptly terminated causing this remote session to break.

Move the line above the first Match line and restart sshd. On a Linux target, the same error means the drop-in file wasn’t loaded, so check that /etc/ssh/sshd_config still has its Include /etc/ssh/sshd_config.d/*.conf line.

Permission denied (publickey,password,keyboard-interactive)

With BatchMode=yes, a rejected key ends here instead of at a password prompt. We got it for two different reasons. The first was an administrator’s key in the per-user authorized_keys file, which sshd never reads for admins. The second was a loose ACL on administrators_authorized_keys. Granting Users read access was tolerated, but granting Users modify broke it, and the server’s OpenSSH/Operational event log named the cause:

sshd: Bad permissions. Try removing permissions for user: BUILTIN\\Users (S-1-5-32-545) on file
C:/ProgramData/ssh/administrators_authorized_keys.
sshd: Authentication refused.

Re-run the icacls line from the key section to reset the file to SYSTEM and Administrators only. Read that log with Get-WinEvent -LogName 'OpenSSH/Operational' -MaxEvents 10 on the server whenever a key fails, because the client never sees the reason.

A parameter cannot be found that matches parameter name ‘HostName’

You ran the command in Windows PowerShell 5.1, where Invoke-Command and Enter-PSSession only know WinRM. We got this exact message from the built-in powershell.exe on the server. Start pwsh instead, and on the server side make sure the subsystem points at PowerShell 7’s pwsh.exe, never at powershell.exe.

WARNING: connection is not using a post-quantum key exchange algorithm

The OpenSSH client on Ubuntu 26.04 printed this three-line warning on every connection to the Windows server:

** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html

It’s a warning, not a failure, and the session works. The Windows side runs OpenSSH_9.5p2 for Windows, and ssh -v win2025-srv showed the pair settling on kex: algorithm: curve25519-sha256, a classical exchange, which is what OpenSSH 10.1 and later warn about. In scripts the noise lands on stderr and can trip error handling, so we set WarnWeakCrypto no-pq-kex for that host in ~/.ssh/config, guarded by IgnoreUnknown as shown earlier so the same file still works on clients older than 10.1. Scope it to the Windows hosts rather than Host *, so you still see the warning for any Linux server that ought to support the stronger exchange.

Keep reading

Create a Bootable Windows 11 USB and Install Windows 11 Windows Create a Bootable Windows 11 USB and Install Windows 11 Set Up a Samba Share on Windows 11 and Access It from Linux Networking Set Up a Samba Share on Windows 11 and Access It from Linux Windows 11 Installation on KVM with Virt-Manager KVM Windows 11 Installation on KVM with Virt-Manager Configure NTP Server on Windows Server 2025 / 2022 (w32tm Guide) Windows Configure NTP Server on Windows Server 2025 / 2022 (w32tm Guide) Windows 11 Keyboard Shortcuts Cheat Sheet (Printable PDF) Cheat Sheets Windows 11 Keyboard Shortcuts Cheat Sheet (Printable PDF) Add DNS Reverse Lookup Zone in Windows Server 2025 Networking Add DNS Reverse Lookup Zone in Windows Server 2025

Leave a Comment

Press ESC to close