Windows host through SSH bastion on Ansible

A use case I’ve been asked about a few times is to be able to connect to a Windows host through another bastion host. In the context of this post, a bastion host is “a server that is placed on the boundary of an internal network and provides access to this network from another external network”.

Typical AWS Bastion Setup – source

Because a bastion host provides access to an internal network, great care must be taken to harden this host from malicious actors. The scope of this post is to not cover the hardening of this host but rather how to configure Ansible to use Windows hosts within an environment like this.


The main two protocols that are used for this scenario are SOCKS and SSH which are fairly well known and understood so I’ll briefly talk about them.


When we talk about proxies, typically a standard HTTP proxy comes in mind that can forward HTTP requests from a client to another server that the proxy can reach. As WinRM is a HTTP protocol we can still use a HTTP proxy to route to our Windows host but this guide is about using SSH through a bastion host so it will ignore HTTP proxies. SOCKS stands for SOCKet Secure and the latest standard is SOCKS5 which will be used in this guide.

SOCKS is a proxy that routes traffic back and forth between a client and a server and acts as a middle man between the two. It tries to be as transparent as possible and transfers the packet to the client and server unmodified. This is unlike a HTTP proxy which forwards the the HTTP request and can include more in depth analysis of the HTTP request itself and act accordingly.

The SOCKS protocol does not encrypt or change your data during transit but utilising it with other protocols, like SSH, we can ensure any traffic on a public interface is encrypted.


The special sauce in this mix is Secure SHel (SSH) that is used as the channel to transfer the SOCKS data from the Ansible host to the bastion host. SSH is fairly ubiquitous in today’s IT environment so I won’t bore you with the details of what is actually happening during this connection. Channeling the data over SSH gives us a few benefits, such as;

  • Encryption of data as it goes through the public channel
  • Use SSH keys for authentication, this does not negate the need for WinRM authentication but is used to protect the bastion host
  • Server host verification
  • Compression of data when running over a high latency network

A very basic outline of what this can looks like is;

Excuse my crude diagramming

Unlike a simple Ansible to Windows host over WinRM connection, this scenario has multiple network boundaries that we need to be aware of. These boundaries include;

  • Ansible to the SOCKS listener
    • The port this runs on is dependent on whatever port the listener was configured with, -D <port>
    • This data contains the WinRM payload encapsulated in a SOCKS packet, encryption is dependent on the WinRM/HTTP protocol
    • In this demo, this all runs on the loopback interface but the SOCKS/SSH client could also be on another host if needed
  • SOCKS listener to the SSH client
    • An internal channel that is controlled by the SSH service
  • SSH Channel
    • SSH traffic sent over an unsecured connection like the internet
    • All data is encrypted using the SSH protocol and is seen as normal SSH traffic
    • Typically this is done over port 22 but can be whatever is specified by the user
    • In this demo, this is the data sent over port 2222 on the loopback interface (VBox NAT port forwarding)
  • SSH Server to the WinRM listener
    • The Bastion host acts as the Ansible controller and sends the WinRM traffic to the Windows host
    • For WinRM, this would be done over port 5985 (http) or 5986 (https)
    • The WinRM service sees the bation host as the source and has no idea of the SSH/SOCKS implementation behind it

Any responses from the Windows host are sent back through the same channel and everything should be transparent to the Ansible controller.


I’ve spoken about WinRM and PSRP at lengths in Demystifying WinRM and PowerShell emoting on Python so I won’t go into too much detail here. Basically WinRM is a HTTP protocol and uses a SOAP based API for communication between the client and the server. While encryption can and should be used with either HTTPS or Kerberos with AES256 encryption, it is not as simple to setup and use in comparison to SSH. Windows does offer a Win32 port of OpenSSH that you can use today but it’s still fairly buggy and not currently compatible in Ansible.

As time goes on I expect to see WinRM decline in use but for the immediate and short term future it is here to stay.

In Action

Now that we’ve gone over a high level overview of what is involved, let’s demonstrate this in action.


Here are the following applications that need to be installed for the demo;

If you wish to just read the blog then this isn’t necessary but actually doing each step helped me to understand what is actually going on and I highly recommend it.

Once Vagrant and VirtualBox have been installed, pip can be used to install the Python dependencies;

# recommend you use a Virtual Environment for your testing
pip install virtualenv
virtualenv ansible-bastion
source ansible-bastion/bin/activate

# install the required Python packages
pip install ansible pypsrp requests[socks]

This installs Ansible, pypsrp, and a library for requests to support SOCKS proxies.

Setting up the hosts

Once the pre-requisite applications have been installed, the next step is to setup the test environment. Create a file called 'Vagrantfile' with the following content:

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.define "bastion" do |bastion|
    ssh_pub_key = File.readlines("#{Dir.home}/.ssh/").first.strip = "centos/7" "private_network", ip: "",
      virtualbox__intnet: true "forwarded_port", guest: 22, host: 2222, id: "ssh"
    bastion.vm.provision "shell",
      inline: "echo ' windows-server' >> /etc/hosts"
    bastion.vm.provision "shell", privileged: false,
      inline: "echo #{ssh_pub_key} >> $HOME/.ssh/authorized_keys"
  config.vm.define "windows" do |windows| = "jborean93/WindowsServer2016" "private_network", ip: "",
      virtualbox__intnet: true

This Vagrantfile will create two VMs with the following configurations;

  • A Centos 7 host that acts as our bastion host
    • Has a NAT adapter with a forwarded SSH port bound to 2222 for external access
    • A VirtualBox internal network adapter set to an IP of, any IP will do here
    • A manual entry in the hosts file that points to our Windows host IP
    • Our current user’s public key set as an authorized key for SSH logon, this isn’t strictly needed but it saves us from managing SSH password authentication
  • A Windows Server 2016 host that is our intended target
    • Has a NAT adapter for Vagrant to configure but will not be used as part of the demo
    • A VirtualBox internal network adapter set to an IP of, any IP will do as long as it matches the host entry on our bastion host

This scenario is not limited to a Centos 7 or Windows Server 2016 host, the main requirement is that the bastion host can resolve our Windows host and that bastion can then be accessed through SSH from our Ansible controller host. Once the file been created, start the Vagrant process with the command 'vagrant up'.

Get a cup of coffee this could take awhile

This step can take some time to complete as it will download the Vagrant boxes required and then setup the VirtualBox VMs. Once the hosts are setup, you should be able to connect to the bastion host over SSH with the command 'ssh -p 2222 vagrant@'. Otherwise to log on manually, open the VirtualBox console for the VM and logon with the username vagrant and password vagrant. To remove the VMs or to start again, run 'vagrant destroy'.

The SSH connection is targeted towards due to how the VirtualBox NAT network adapter works. What happens is that it sets up a listener on the localhost and listens on the port configured (2222). Any requests to this port are then forwarded to the port on the VM (22). While our new Windows host also has a NAT adapter for Vagrant to use in its initial configuration, we will be ignoring that and pretend only the internal network adapter exists for this demo. This effectively means we cannot access that Windows host on our Ansible controller host but our bastion host can through the internal network that they are both connected to.

Configuring the SSH Proxy

The next step is to setup the SSH proxy that exposes a SOCKS5 proxy to channel the WinRM requests through the bastion host. This can be as simple as running 'ssh -C -D 1234 -p 2222 vagrant@' in another session and keeping that running for the tests. To have something that runs in the background you can use SSH Multiplexing with ControlMaster. To setup the background connection run;

# any folder will do, as long as it is the same in ControlPath
mkdir ~/.ssh/cp
ssh -o "ControlMaster=auto" -o "ControlPersist=no" -o "ControlPath=~/.ssh/cp/ssh-%r@%h:%p" -CfNq -D -p 2222 vagrant@

Let’s break this down a bit more;

  • -o "ControlMaster=auto": Allow multiplexing when possible
  • -o "ControlPersist=no": The master connection will be closed when the initial connection is also closed
  • -o "ControlPath=...": The path to the control socket, uses some substituted values to derive the filename but is configurable
  • -C: Compress the data that goes over this channel, good for high latency networks
  • -f: Run SSH in the background after the connection is setup
  • -N: Do not execute a remote command
  • -q: Quiet mode
  • -D The bind address and port for the SOCKS proxy, the host address can be omitted which indicates the port should be available from all interfaces
  • -p 2222: The remote port of our bastion host
  • vagrant@ The username and hostname of our bastion host

Once you have finished the connection and wish to close it, simply run 'ssh -o "ControlPath=~/.ssh/cp/ssh-%r@%h:%p" -O stop -p 2222 vagrant@'. This closes the background connection that’s stored at the ControlPath option based on the host we specified (our bastion host). You can substitute '-O stop' with '-O check' to check the status of a connection as well.

Creating a SOCKS proxy is not limited to OpenSSH, there are various applications out there that can also achieve this goal. OpenSSH was chosen in this guide as it is fairly universal when it comes to using Ansible.

Connecting with Ansible

Once the proxy has been setup, the last step is to create an Ansible inventory that will use the SSH proxy to talk to the Windows host. The inventory would be set like Ansible is connecting directly to the Windows host but with the addition of the ansible_psrp_proxy variable that points to our SSH proxy server.

For this demo, create a file called inventory.ini with the following content:



Instead of using the typical winrm connection plugin, we will try out the new psrp plugin which exposes a variable that can define a proxy for Ansible to use. This can also be done with the winrm connection plugin but requires global environment variables to be set which is a lot more messy if you have different proxy requirements per host. Breaking down the proxy variable we can see that it’s split into 3 parts <scheme>://<hostname>:<port>;

  • scheme: Set to either socks5 or socks5h, the former means DNS resolution is done on the client while the latter means resolution happens on the bastion host
  • hostname: The hostname of the SSH proxy, in this demo this is the same host as the Ansible controller
  • port: The port the SSH proxy is listening on, corresponds to the -D argument when starting the proxy

I haven’t been able to test it, but if your SOCKS proxy server requires user authentication then the credentials would be specified like socks5://user:pass@host:port.

As the bastion host contains the hosts entry to resolve the Windows host, we use the socks5h schema, this should be typical of most scenarios. Once the inventory has been created, simply run ansible -i inventory.ini windows -m win_ping and watch it connect.

Successful connection

Now that we can successfully connect Ansible to our Windows host through the bastion host, you can easily see that Ansible runs as normal with the exception of the proxy variable being set. I would love to have the actual SOCKS/SSH proxy set up as part of Ansible to get rid of that manual step but that’s a nice to have feature and not a must have.

Notify of

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Inline Feedbacks
View all comments
G Mohr
G Mohr
1 year ago

Can you do an example without vagrant? I’m looking for a simple example with a jumphost in the public subnet/SG, Windows instance in the private subnet/SG. Seems not to be possible. Without a straightforward way to do this, Ansible does not seem to be the right tool to manage Windows boxes in the cloud.

Craig St George
Craig St George
1 year ago

Thanks so MUCH I have this working with my bastion and certificate auth on the winrm connection
But yes would be good if ansible could start the tunnel . I m thinking maybe something like redsocks may work on the ansible host to make this easier

Gil Shinar
Gil Shinar
1 year ago

Additional thing I would would have add to the blog is When running ansible on a non-ssh machine, ssh configurations are not being read during the connection phase. Meaning, the only way to use the socks proxy is by running the command you’ve written in your blog before running the ansible-playbook command.
One cannot add that socks tunnel to an ssh configuration file or the ssh_args parameter under the ssh_connection section in the ansible.cfg file.

1 year ago

I’m using ansible to manage Windows servers and I wanted to contact servers in our DMZ passing by our bastion server but it is a Windows server too. How could I process ?
Thanks in advance for your answers.

11 months ago

I tried this same but I am facing some issues. I tried this with windows 2012, windows 2016.

fatal: []: UNREACHABLE! => {
“changed”: false,
“msg”: “Failed to connect to the host via PSRP: SOCKSHTTPConnectionPool(host=’′, port=5985): Max retries exceeded with url: /wsman (Caused by ConnectTimeoutError(<urllib3.contrib.socks.SOCKSConnection object at 0x7f8b7cabd940>, ‘Connection to timed out. (connect timeout=30)’))”,
“unreachable”: true

PLAY RECAP ************************************************************************************************************************************************************ : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0

Shital Patil
Shital Patil
11 months ago

Plz help me to solve this issue.

fatal: []: UNREACHABLE! => {
“changed”: false,
“msg”: “Failed to connect to the host via PSRP: SOCKSHTTPConnectionPool(host=’′, port=5985): Max retries exceeded with url: /wsman (Caused by ConnectTimeoutError(<urllib3.contrib.socks.SOCKSConnection object at 0x7f8b7cabd940>, ‘Connection to timed out. (connect timeout=30)’))”,
“unreachable”: true

PLAY RECAP ************************************************************************************************************************************************************ : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0

9 months ago

Thanks for sharing. I want to know is it possible to use a Windows jump host to connect to a Windows server in a private subnet? Can psrp or winRM do that? thanks

4 months ago

Hello, First, I’d like to thank you for your tutorial but I have some issues when testing it. There’s the output of a simple win_ping : [centos@AnsibleTest ansible]$ ansible -m win_ping windows -vvv ansible 2.8.5 config file = /etc/ansible/ansible.cfg configured module search path = [u’/home/centos/.ansible/plugins/modules’, u’/usr/share/ansible/plugins/modules’] ansible python module location = /usr/lib/python2.7/site-packages/ansible executable location = /usr/bin/ansible python version = 2.7.5 (default, Aug 7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] Using /etc/ansible/ansible.cfg as config file host_list declined parsing /etc/ansible/hosts as it did not pass it’s verify_file() method script declined parsing /etc/ansible/hosts as it did not pass it’s verify_file() method… Read more »

4 months ago

So how does the bastion host know where to forward the ansible requests to if you have multiple windows endpoints? How does the bastion host know where to forward to?

Jesty Danie
Jesty Danie
3 months ago

Hi Jordon Thanks for your blog. i tried setting this not in vagrant but on my Azure environment. Below in my configuration [windows] ansible_host= On the group_vars file : ansible_user : (removed) ansible_password : (removed) ansible_connection : psrp ansible_psrp_protocol : https ansible_psrp_proxy : socks5h://localhost:1234 My proxy command that I used to set up the SOCKS which i have configured on my Ansible server mkdir ~/.ssh/cp ssh -o “ControlMaster=auto” -o “ControlPersist=no” -o “ControlPath=~/.ssh/cp/ssh-%r@%h:%p” -CfNq -D -p 22 jumphost@ I get the below error while running the win_ping. Kindly assist, root@AWXServerDev:/etc/ansible# ansible -i inventory.ini windows -m win_ping -vvv ansible 2.9.6 config… Read more »

3 months ago

i have already installed them.
Requirement already satisfied: requests[socks] in ./.local/lib/python2.7/site-packages (2.23.0)
Requirement already satisfied: certifi>=2017.4.17 in ./.local/lib/python2.7/site-packages (from requests[socks]) (2019.11.28)
Requirement already satisfied: idna<3,>=2.5 in ./.local/lib/python2.7/site-packages (from requests[socks]) (2.9)
Requirement already satisfied: chardet<4,>=3.0.2 in ./.local/lib/python2.7/site-packages (from requests[socks]) (3.0.4)
Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in ./.local/lib/python2.7/site-packages (from requests[socks]) (1.25.8)
Requirement already satisfied: PySocks!=1.5.7,>=1.5.6; extra == “socks” in ./.local/lib/python2.7/site-packages (from requests[socks]) (1.7.1)

3 months ago

After installing the pysocks the dependency error has gone. Below is the content of the ini file. [windows] [windows:vars] ansible_user=[removed] ansible_password=[removed] ansible_connection=psrp ansible_psrp_protocol=https ansible_psrp_proxy=socks5h://localhost:1234 ansible_psrp_cert_validation : ignore validate_certs=false | UNREACHABLE! => { “changed”: false, “msg”: “Failed to connect to the host via PSRP: SOCKSHTTPSConnectionPool(host=’′, port=5986): Max retries exceeded with url: /wsman (Caused by SSLError(SSLError(1, u'[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:727)’),))”, “unreachable”: true } #changed protocol to 5985 | UNREACHABLE! => { “changed”: false, “msg”: “Failed to connect to the host via PSRP: SOCKSHTTPConnectionPool(host=’′, port=5985): Max retries exceeded with url: /wsman (Caused by ConnectTimeoutError(<urllib3.contrib.socks.SOCKSConnection object at 0x7f0f861b3110>, ‘Connection… Read more »

3 months ago

Do i need to configure a socks5 proxy server/squid proxy on the bastion host? could be the reason its not port forwarding from bastion host to Windows machine

1 month ago
Reply to  Jordan Borean

Thanks Jordan. It worked for me. But has someone performed any prototype using Windows jump host, i could see that you have already responded that using the new Win32-OpenSSH is the only option?

2 months ago

“changed”: false,
“msg”: “Failed to connect to the host via PSRP: HTTPConnectionPool(host=’′, port=5985): Max retries exceeded with url: /wsman (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x7f7e40498210>, ‘Connection to timed out. (connect timeout=30)’))”,
“unreachable”: true

Atif Farrukh
Atif Farrukh
1 month ago

Hi, how would i be able to do it, if I dont know the IP address(es) of Windows VM? Lets say Windows VMs are behind a Scale Set and created/destroyed?