Using an Ansible playbook with an SSH bastion / jump host

Since I've set this up a number of times, but I just realized I've never documented it on my blog, I thought I'd finally do that.

I have a set of servers that are running on a private network. That network is connected to the Internet through a single reverse proxy / 'bastion' host.

But I still want to be able to manage the servers on the private network behind the bastion from outside.

Method 1 - Inventory vars

The first way to do it with Ansible is to describe how to connect through the proxy server in Ansible's inventory. This is helpful for a project that might be run from various workstations or servers without the same SSH configuration (the configuration is stored alongside the playbook, in the inventory).

In my Ansible project, I had an inventory file like the following:

[proxy]
bastion.example.com

[nodes]
private-server-1.example.com
private-server-2.example.com
private-server-3.example.com

If I am connected to the private network directly, I can just run ansible commands and playbooks, and Ansible can see all the servers and connect to them (assuming my SSH config is otherwise correct).

From the outside, though, I need to modify my inventory to look like the following:

[proxy]
bastion.example.com

[nodes]
private-server-1.example.com
private-server-2.example.com
private-server-3.example.com

[nodes:vars]
ansible_ssh_common_args='-o ProxyCommand="ssh -p 2222 -W %h:%p -q [email protected]"'

This sets up an SSH proxy through bastion.example.com on port 2222 (if using the default port, 22, you can drop the port argument). The -W argument tells SSH it can forward stdin and stdout through the host and port, effectively allowing Ansible to manage the node behind the bastion/jump server.

Method 2 - SSH config

The alternative, which would apply the proxy configuration to all SSH connections on a given workstation, is to add the following configuration inside your ~/.ssh/config file:

Host bastion
   User username
   Hostname bastion.example.com

Host private-server-*.example.com
   ProxyJump bastion

Ansible will automatically use whatever SSH options are defined in the user or global SSH config, so it should pick these settings up even if you don't modify your inventory.

This method is most helpful if you know your playbook will always be run from a server or workstation where the SSH config is present.

Comments

There's a "-J jumphost" option to ssh for that these days.

I configured the bastion host in the .ssh/config where it works not only for ansible:

--8<------
Host *.example.com !bastion.example.com
ProxyJump bastion.example.com
--8<------

The -W argument can be applied too if using ProxyCommand instead of ProxyJump:

--8<------
Host *.example.com !bastion.example.com
ProxyCommand ssh bastion.example.com -W %h:%p
--8<------

The same effect can be achieved by incorporating ProxyCommand or ProxyJump in your .ssh/config file instead of adding this to Ansible inventory. In particular and assuming the above inventory, one could add the following to their .ssh/config

Host bastion
   User username
   Hostname bastion.example.com

Host private-server-*.example.com
   ProxyJump bastion

Therefore, when connects directly to private-server-1.example.com via SSH, ssh will first make an SSH connection to bastion.example.com and then use this, to connect to private-server-1.example.com (establish TCP forwarding behind the scenes etc.). If you use Password Authentication on both bastion.example.com and private-server-1.example.com you will be requested to fill password two times. You can even use ProxyJump multiple times out of the box!

This is really handy, as your inventory is clean from unnecessary vars (e.g in cases you run Ansible both locally and from Jenkins inside private network) and can be used by Ansible out-of-the box. The only prerequisite is for Ansible to use native OpenSSH client (default) instead of paramiko to connect.

Thanks! I actually went ahead and edited the post to show both alternatives, because there are times when putting the config in the inventory is the best option, but I think many people would prefer the 2nd approach since it can help with other SSH interaction as well, assuming they're doing it from their own workstation.

Nice. However I prefer an alternative approach: Which would be to have an ansible.cfg with
[ssh_connection]
ssh_args = -F ./ssh.cfg

And to make use of that ssh.cfg to use the, IMHO, much cleaner ProxyJump jump command. This has a secondary advantage that you can then pass it to ssh itself to debug say a complex jump through multiple jump boxes via `ssh -F ssh.cfg my-far-away-node`. A trivial example here for example: https://github.com/tonykay/dojo-ansible-container-toolkit/blob/main/lab…

It would be easy for example to use different keys and users for each of those 3 hosts and easy to debug.

ii uses .ssh/config for all this stuff, there is ProxyJump option for easier config

What about if it is possible to reach the target device through 2 or multiple bastions ? Thanks for help.

Same, just use the SSH config option and let ssh handle the dependency, so if A require B and B require C (A<-B<-C) ssh will recursively connect to the next ProxyJump node.

I use the inventory approach...however the "username" inside my inventory can vary.
Is there a way to work with a variable here like the current shell user passes his name through this variable ???

thanks for your posts.
I need to install ansible in my company through a bastion.
When i run :

ssh -t -A [my user]@[my bastion] [my user]@[my host]

I reach my host

But when i confugre my file .ssh/config like this :
Host bastion
hostname [my bastion]
User [my user]
port 22
IdentityFile /home/[my user]/.ssh/[private key]

Host *
ProxyJump bastion

or

ProxyCommand ssh [my user]@[my bastion] -W %h:%p

That doesn't work . I have this error :
*------------------------------------------------------------------------------*
|THIS IS A PRIVATE COMPUTER SYSTEM, UNAUTHORIZED ACCESS IS STRICTLY PROHIBITED.|
|ALL CONNECTIONS ARE LOGGED. IF YOU ARE NOT AUTHORIZED, DISCONNECT NOW. |
*------------------------------------------------------------------------------*
channel 0: open failed: administratively prohibited: open failed
stdio forwarding failed
kex_exchange_identification: Connection closed by remote host
Connection closed by UNKNOWN port 65535

Could you help me, please?