Introduction

WinRM (Windows Remote Management) is Ansible's default transport for Windows targets, but it requires opening ports 5985/5986 and managing certificates. An alternative approach tunnels WinRM through SSH using the PowerShell Remoting Protocol (PSRP) — combining WinRM's Windows-native capabilities with SSH's proven security model.

Why Tunnel WinRM via SSH?

ApproachPorts RequiredEncryptionCertificate Management
WinRM HTTP5985NoneNone
WinRM HTTPS5986TLSRequired
WinRM via SSH (PSRP)22SSHSSH keys only
Native SSH22SSHSSH keys only

Benefits of SSH tunneling:

  • Single port — only port 22 needed (often already allowed through firewalls)
  • SSH key authentication — no certificate management
  • Existing infrastructure — reuse SSH bastion hosts and jump servers
  • Encryption — SSH provides proven end-to-end encryption

Prerequisites

On the Windows Target

1. Install OpenSSH Server

# Windows 10/11 and Server 2019+
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0

# Start and enable the SSH service
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic

# Verify
Get-Service sshd

2. Install PowerShell 7+

# Install via winget
winget install Microsoft.PowerShell

# Or download from GitHub
# https://github.com/PowerShell/PowerShell/releases

3. Configure SSH for PowerShell Remoting

Edit C:\ProgramData\ssh\sshd_config:

# Add PowerShell subsystem
Subsystem powershell c:/progra~1/powershell/7/pwsh.exe -sshs -nologo

# Enable password authentication (or use key-based)
PasswordAuthentication yes
PubkeyAuthentication yes

Restart SSH:

Restart-Service sshd

On the Ansible Controller

# Install the PSRP connection plugin
pip install pypsrp

# Or install with SSH transport support
pip install pypsrp[credssp,kerberos]

Ansible Configuration

Inventory with PSRP over SSH

[windows_ssh]
win01.example.com
win02.example.com

[windows_ssh:vars]
ansible_connection=psrp
ansible_psrp_protocol=http
ansible_psrp_proxy=socks5h://localhost:1080
ansible_user=ansible_admin
ansible_password="{{ vault_win_password }}"

SSH Tunnel Method

Option 1: Manual SSH Tunnel

# Create SSH tunnel from local port 5985 to Windows WinRM
ssh -L 5985:localhost:5985 ansible_admin@win01.example.com -N -f

Then configure Ansible to connect through localhost:

[windows_tunneled]
win01 ansible_host=127.0.0.1 ansible_port=5985

[windows_tunneled:vars]
ansible_connection=winrm
ansible_winrm_transport=basic
ansible_winrm_scheme=http
ansible_user=ansible_admin
ansible_password="{{ vault_win_password }}"

Option 2: Native SSH Connection (PowerShell 7+)

[windows_native_ssh]
win01.example.com
win02.example.com

[windows_native_ssh:vars]
ansible_connection=ssh
ansible_shell_type=powershell
ansible_user=ansible_admin
ansible_ssh_private_key_file=~/.ssh/windows_key

This uses native SSH without WinRM at all — the simplest approach when PowerShell 7 is available.

Option 3: PSRP with SSH Transport

[windows_psrp_ssh]
win01.example.com

[windows_psrp_ssh:vars]
ansible_connection=psrp
ansible_psrp_protocol=http
ansible_psrp_auth=negotiate
ansible_user=ansible_admin@DOMAIN
ansible_password="{{ vault_win_password }}"

Connection Method Comparison

MethodConnection PluginRequiresBest For
WinRM HTTPSwinrmPort 5986 + certsStandard Windows automation
SSH + PowerShellsshOpenSSH + PS7Simple SSH-only environments
PSRP directpsrpPort 5985/5986High-performance WinRM
SSH tunnel + WinRMwinrmSSH tunnelRestrictive firewalls
PSRP over SSHpsrpSSH + PSRPMaximum flexibility

Test Connectivity

---
- name: Test Windows SSH connectivity
  hosts: windows_native_ssh
  tasks:
    - name: Ping via SSH
      ansible.windows.win_ping:

    - name: Run PowerShell command
      ansible.windows.win_shell: |
        $PSVersionTable.PSVersion
        hostname
      register: ps_output

    - name: Show result
      ansible.builtin.debug:
        var: ps_output.stdout_lines
ansible-playbook test_win_ssh.yml -i inventory.ini

SSH Key Authentication

Deploy Keys to Windows

# Copy public key to Windows authorized_keys
ssh-copy-id ansible_admin@win01.example.com

# Or manually add to Windows
# C:\Users\ansible_admin\.ssh\authorized_keys
# C:\ProgramData\ssh\administrators_authorized_keys  (for admin users)

Fix Permissions for Admin Users

On Windows, admin user keys require a special file:

# Create administrators authorized keys file
$authKeys = "C:\ProgramData\ssh\administrators_authorized_keys"
Add-Content $authKeys "ssh-ed25519 AAAA... ansible@controller"

# Fix permissions (required!)
icacls $authKeys /inheritance:r /grant "SYSTEM:F" /grant "BUILTIN\Administrators:F"

Through a Bastion/Jump Host

[windows_via_bastion]
win01.internal ansible_host=10.0.1.10

[windows_via_bastion:vars]
ansible_connection=ssh
ansible_shell_type=powershell
ansible_user=ansible_admin
ansible_ssh_common_args='-o ProxyJump=bastion.example.com'

Troubleshooting

"Connection Refused" on Port 22

# Verify SSH is running
Get-Service sshd

# Check firewall
Get-NetFirewallRule -Name *ssh*

# Add firewall rule if missing
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server' `
  -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22

PowerShell Subsystem Not Found

no matching subsystem found

Verify sshd_config has the subsystem line and the path is correct:

# Find PowerShell 7 path
(Get-Command pwsh).Source
# Should be: C:\Program Files\PowerShell\7\pwsh.exe

Permission Denied

# Test SSH manually first
ssh -v ansible_admin@win01.example.com

# Check Windows Event Viewer for auth errors
# Applications and Services Logs → OpenSSH → Operational

Conclusion

Tunneling WinRM via SSH eliminates certificate management and reduces firewall exposure to a single port. For the simplest setup, use native SSH with PowerShell 7 (ansible_connection=ssh + ansible_shell_type=powershell). For environments requiring WinRM features, create SSH tunnels and point Ansible at localhost. Always use SSH key authentication over passwords, and leverage bastion hosts for internal Windows servers.