Ansible, Bastion Host, ProxyCommand

When designing your infrastructure it is common to limit the number of access points to the bare minimum, this is to remove as many attack vectors as possible and it makes it easier to secure and monitor the few you have.

Another best practice is to automate as much of your infrastructure deployment and configuration as you can; now let’s combine these two philosophies by accessing application servers through a bastion host in Ansible.

Ansible Playbook

The Ansible playbook we’ll use in this example only contains the bare necessities and has the following structure:



Let’s define our servers in the inventory file:


These are obviously just examples, replace them with your actual server IPs; assuming you’ve already set up a bastion server in a public subnet and application servers in private subnets, and your public key being on both.


Ansible uses SSH for virtually all its operations and since it allows us to specify a custom configuration file we can utilize SSH’s tried and proven ProxyCommand which was created for exactly what we want to achieve.

The SSH client allows for a lot of customization, but luckily it has fairly sane defaults so we only need to change a few settings for our ssh.cfg file:

Host 172.16.*
ProxyCommand ssh -W %h:%p ubuntu@
Host *
ControlMaster auto
ControlPath ~/.ssh/mux-%r@%h:%p
ControlPersist 15m

This is where we tell the 172.16.* hosts to proxy their commands through our bastion host. As you can see, it supports the use of partial wildcards; this opens up the possibility for some application servers to proxy commands through one bastion, while other application servers go through another. We can utilize this when we configure Ansible for multiple environments .

The Control settings are configured to apply to every host and lets SSH re-use already established connections for a set amount of time, a simple way of speeding up the execution of playbooks.

Ansible Config

Now we want to tell Ansible to use our custom ssh.cfg file, thankfully the Ansible guys have made this pretty easy through ansible.cfg:

ssh_args = -F ssh.cfg
control_path = ~/.ssh/mux-%r@%h:%p

ssh_args is where we’re free to pass any arguments to Ansible’s SSH client, in this case we simply tell it to use our config file; control_path should match what we specified in ssh.cfg.

We can now confirm that our setup is working by running the following command from the bastion_playbook directory:

$ ansible app_servers -i inventory -u ubuntu -m ping


Here’s a gist with all the code used in this example.