diff --git a/.gitignore b/.gitignore
index 506313fe0b5143a054504e632c5ad958bc2e6744..8eae4884bd40bcc99926937a0c19fb74fd344280 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ temp
 .idea
 *.tfstate
 *.tfstate.backup
+/ssh-bastion.conf
\ No newline at end of file
diff --git a/ansible.cfg b/ansible.cfg
index f0e4ef6523518dbb1add11e4535fbbcb996e847e..86e1d6a223ac1993beb918c3682a3c0e08a2186f 100644
--- a/ansible.cfg
+++ b/ansible.cfg
@@ -1,5 +1,7 @@
 [ssh_connection]
 pipelining=True
+ssh_args = -F ./ssh-bastion.conf -o ControlMaster=auto -o ControlPersist=30m
+control_path = ~/.ssh/ansible-%%r@%%h:%%p
 [defaults]
 host_key_checking=False
 gathering = smart
diff --git a/cluster.yml b/cluster.yml
index cf7efb4bbffd192a0d2049034b514df2cda7c544..7c6f0105adad3e075c170cdb3497e70ce9d4c227 100644
--- a/cluster.yml
+++ b/cluster.yml
@@ -1,4 +1,10 @@
 ---
+- hosts: localhost
+  gather_facts: False
+  roles:
+    - bastion-ssh-config
+  tags: [localhost, bastion]
+
 - hosts: all
   any_errors_fatal: true
   gather_facts: false
@@ -16,7 +22,7 @@
   any_errors_fatal: true
   gather_facts: true
 
-- hosts: all:!network-storage
+- hosts: all:!network-storage:!bastion
   any_errors_fatal: true
   roles:
     - { role: kubernetes/preinstall, tags: preinstall }
diff --git a/docs/ansible.md b/docs/ansible.md
index bed95f108f06fe0471fe0ec618b9681e3038f87a..38fb210565ff9a68deb452be79f04d4db1ff3125 100644
--- a/docs/ansible.md
+++ b/docs/ansible.md
@@ -57,6 +57,7 @@ The following tags are defined in playbooks:
 |--------------------------|---------
 |                     apps | K8s apps definitions
 |                    azure | Cloud-provider Azure
+|                  bastion | Setup ssh config for bastion
 |             bootstrap-os | Anything related to host OS configuration
 |                   calico | Network plugin Calico
 |                    canal | Network plugin Canal
@@ -119,3 +120,17 @@ ansible-playbook -i inventory/inventory.ini cluster.yaml \
 ```
 
 Note: use `--tags` and `--skip-tags` wise and only if you're 100% sure what you're doing.
+
+Bastion host
+--------------
+If you prefer to not make your nodes publicly accessible (nodes with private IPs only),
+you can use a so called *bastion* host to connect to your nodes. To specify and use a bastion,
+simply add a line to your inventory, where you have to replace x.x.x.x with the public IP of the
+bastion host.
+
+```
+bastion ansible_ssh_host=x.x.x.x
+```
+
+For more information about Ansible and bastion hosts, read 
+[Running Ansible Through an SSH Bastion Host](http://blog.scottlowe.org/2015/12/24/running-ansible-through-ssh-bastion-host/)
\ No newline at end of file
diff --git a/inventory/inventory.example b/inventory/inventory.example
index ab085ad4a6ec8b68f69d75959a947cfe787a379a..1d10cdce05f3a50e6135c1ae903e41701e7b33ce 100644
--- a/inventory/inventory.example
+++ b/inventory/inventory.example
@@ -7,6 +7,9 @@
 # node5 ansible_ssh_host=95.54.0.16  # ip=10.3.0.5
 # node6 ansible_ssh_host=95.54.0.17  # ip=10.3.0.6
 
+# ## configure a bastion host if your nodes are not publicly reachable
+# bastion ansible_ssh_host=x.x.x.x
+
 # [kube-master]
 # node1
 # node2
diff --git a/roles/bastion-ssh-config/tasks/main.yml b/roles/bastion-ssh-config/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d1aae5ca82a92069ec09eb6a06b492a361caf413
--- /dev/null
+++ b/roles/bastion-ssh-config/tasks/main.yml
@@ -0,0 +1,18 @@
+---
+- set_fact:
+    has_bastion: "{{ 'bastion' in groups['all'] }}"
+
+- set_fact:
+    bastion_ip: "{{ hostvars['bastion']['ansible_ssh_host'] }}"
+  when: has_bastion
+
+# As we are actually running on localhost, the ansible_ssh_user is your local user when you try to use it directly
+# To figure out the real ssh user, we delegate this task to the bastion and store the ansible_ssh_user in real_user
+- set_fact:
+    real_user: "{{ ansible_ssh_user }}"
+  delegate_to: bastion
+  when: has_bastion
+
+- name: create ssh bastion conf
+  become: false
+  template: src=ssh-bastion.conf dest="{{ playbook_dir }}/ssh-bastion.conf"
diff --git a/roles/bastion-ssh-config/templates/ssh-bastion.conf b/roles/bastion-ssh-config/templates/ssh-bastion.conf
new file mode 100644
index 0000000000000000000000000000000000000000..6bcc65dad84b8561c8dbeb82c34ed28b1ce7d8b7
--- /dev/null
+++ b/roles/bastion-ssh-config/templates/ssh-bastion.conf
@@ -0,0 +1,21 @@
+{% if has_bastion %}
+{% set vars={'hosts': ''} %}
+{% set user='' %}
+
+{% for h in groups['all'] %}
+{% if h != 'bastion' %}
+{% if vars.update({'hosts': vars['hosts'] + ' ' + hostvars[h]['ansible_ssh_host']}) %}{% endif %}
+{% endif %}
+{% endfor %}
+
+Host {{ bastion_ip }}
+  Hostname {{ bastion_ip }}
+  StrictHostKeyChecking no
+  ControlMaster auto
+  ControlPath ~/.ssh/ansible-%r@%h:%p
+  ControlPersist 5m
+
+Host {{ vars['hosts'] }}
+  ProxyCommand ssh -W %h:%p {{ real_user }}@{{ bastion_ip }}
+  StrictHostKeyChecking no
+{% endif %}
\ No newline at end of file