Ansible mit Bastion Host

Andrea Dainese
30 October 2022
Post cover

Dies ist der zweite Teil meines IaC-Überblicks basierend auf einem persönlichen Experiment: dem Aufbau einer Cyber Range mit dem IaC-Paradigma. Hier sind die erste und dritte Teile.

Aus rein gestalterischer Sicht ist der Ansatz mit VPN von Client zu Site immer noch der beste. Aber aus automatisierungstechnischer Sicht musste ich ihn neu gestalten und einen Bastion Host einbeziehen. Die Idee gefällt mir nicht so sehr, aber die Vorteile überwiegen die Nachteile.

Szenario

Verglichen mit dem Szenario mit dem VPN-Konzentrator von Client zu Site scheint dieses einfacher zu sein: Interne VMs sind über den Linux-Bastion-Host erreichbar. In der Praxis fungiert der Bastion-Host als Proxy für SSH-Verbindungen. Der Bastion-Host erfordert überhaupt keine Konfiguration.

Da mein Szenario erfordert, dass Teilnehmer auf interne VMs zugreifen können, kopiere ich den SSH-Privatschlüssel, der zum Anmelden an die internen VMs erforderlich ist, in den Bastion-Host. In Zukunft denke ich, dass es besser ist, wenn der Bastion-Host auch als OpenVPN-Konzentrator dient.

Diagramm der Cyber Range mit Bastion Host

Bastion Host und dynamisches AWS EC2-Inventar

An diesem Punkt sollte Ansible:

  • den Bastion-Host mit der öffentlichen IP-Adresse konfigurieren;
  • die internen Hosts mit der privaten IP-Adresse über die öffentliche IP-Adresse des Bastion-Hosts konfigurieren.

Aufgrund des Spacelift-Designs musste ich alles mit einem einzigen Ansible-Playbook konfigurieren. Das bedeutet, dass das AWS EC2 Ansible-Inventar:

  • die öffentliche IP-Adresse für den Bastion-Host zurückgeben muss;
  • die privaten IP-Adressen für die internen VMs zurückgeben muss.

Darüber hinaus sollte Ansible den SSH-Proxy kurz vor dem Anmelden an jeden internen Host konfigurieren.

Nach mehreren Versuchen finde ich ein funktionierendes Rezept. Fangen wir mit dem Ansible-Inventar an:

plugin: aws_ec2
regions:
  - eu-central-1
filters:
  instance-state-name: running
keyed_groups:
  - key: tags
    prefix: tag
hostnames:
  - tag:Name
compose:
  ansible_host: public_ip_address if tags.Name == "bastion" else private_ip_address

Erinnern Sie sich daran, dass ich geschrieben habe, dass IaC zu 80% Planung und Standardisierung ist? Ich gehe davon aus, dass ich in jedem Szenario “bastion” als Hostnamen für den Bastion-Host verwenden werde, und ich kennzeichne den Hostnamen in der AWS EC2-Konfiguration. Dies ist eine meiner “Standards” (Annahmen).

In der obigen Konfiguration des AWS EC2 Ansible-Inventars gebe ich die öffentliche IP-Adresse nur zurück, wenn das Tag Name gleich bastion ist. Das Inventar gibt die private IP-Adresse für alle anderen VMs zurück.

Mein Ansible-Playbook beginnt mit der Konfiguration des Ansible-Hosts:

- hosts: tag_Name_bastion
  gather_facts: no
  remote_user: ubuntu
  roles:
    - role: linux-bastion
      tags: always

In der Rolle finde ich den verfügbaren SSH-Schlüssel und lade ihn auf den Bastion-Host für die Teilnehmer hoch. Erinnern Sie sich daran, dass ich geschrieben habe, dass ich eine “weiche” Bindung wünsche? Hier mache ich das Playbook kompatibel mit Spacelift-Umgebungen und meiner Umgebung. Ich habe auch das Tag “always” verwendet, weil ich ansible_ssh_private_key_file konfiguriere (siehe später).

Konfigurieren interner Hosts über die Bastion

An diesem Punkt kann ich interne Hosts konfigurieren. Eine weitere Annahme, die ich getroffen habe, ist, dass Tags alle interessanten Attribute enthalten, die ich in Ansible zum Gruppieren, Abfragen und Konfigurieren von Hosts verwende. In der Praxis verwende ich die folgenden Tags:

  • Os:ubuntu: für Ubuntu-VMs;
  • Database:mariadb: für MariaDB-VMs;
  • Webapp:wordpress: für VMs mit Wordpress.

Mein Ansible-Playbook führt mehrere Plays aus:

- hosts: tag_Name_bastion
  gather_facts: no
  remote_user: ubuntu
  roles:
    - role: linux-bastion
      tags: always

- hosts: tag_Os_ubuntu:!tag_Name_bastion
  gather_facts: yes
  become: yes
  vars_files:
    - default.yaml
  roles:
    - role: set-environment
      tags: always

# [...]

- hosts: tag_Os_ubuntu:&tag_Database_mariadb
  gather_facts: yes
  become: yes
  vars_files:
    - default.yaml
  roles:
    - role: set-environment
      tags: always
    - role: linux-mariadb
      tags: mariadb

# [...]

- hosts: tag_Os_ubuntu:&tag_Webapp_wordpress

# [...]

Ansible-Facts sind hostspezifisch: Das bedeutet, dass wenn ich ansible_ssh_private_key_file auf dem Bastion-Host festlege, es für andere Hosts nicht definiert ist.

Wie könnte ich den SSH-Proxy für alle internen Hosts konfigurieren, den Bastion-Host ausgeschlossen?

Die Magie passiert in der Datei default.yaml, indem Ansible-Facts und magische Variablen verwendet werden. Die default.yaml-Datei wird in jedem Playbook, das sich an interne Hosts richtet, inkludiert und konfiguriert den SSH-Proxy mit Informationen aus dem Inventar:

ansible_user: "{{ tags.User }}"
ansible_ssh_private_key_file: '{{ hostvars["bastion"]["ansible_ssh_private_key_file"] }}'
ansible_ssh_common_args: >-
  -o ProxyCommand="ssh
  -o IdentityFile={{ hostvars["bastion"]["ansible_ssh_private_key_file"] }}
  -o StrictHostKeyChecking=no
  -o UserKnownHostsFile=/dev/null
  -W %h:%p
  -q {{ hostvars["bastion"]["tags"]["User"] }}@{{ hostvars["bastion"]["public_ip_address"] }}"  
[...]

Erinnern Sie sich daran, dass ich geschrieben habe, dass IaC zu 80% Planung und Standardisierung ist? Ich gehe immer noch davon aus, dass der Bastion-Host bastion heißt und das Tag User den Remote-Benutzer für jede VM enthält. Also, für jede VM:

  • ansible_user: enthält den Remote-Benutzernamen, der im Tag User konfiguriert ist;
  • ansible_ssh_private_key_file: enthält den lokalen (in der Ansible-VM gespeicherten) SSH-Privatschlüssel, und die Informationen werden aus dem Bastion-Host-Eintrag konfiguriert, der im Inventar festgelegt ist;
  • ansible_ssh_common_args: enthält den SSH-Proxy-Befehl, der von Ansible verwendet wird, und wird mit der öffentlichen IP-Adresse des Bastion-Hosts aus dem Inventar konfiguriert.

Beide SSH-Schlüssel müssen lokal auf dem Ansible-Host vorhanden sein.

An diesem Punkt habe ich ein einzelnes Ansible-Playbook, das mein Cyber-Range-Szenario mit einem Bastion-Host konfiguriert.

Schlussfolgerungen

Ich finde, dass Bastion-Hosts häufig verwendet werden. Ich mag sie nicht sehr, weil aus Sicherheitssicht der Bastion-Host ein weiterer Host mit Superkräften ist. Aber aus automatisierungstechnischer Sicht ist dies tatsächlich der einzige erfolgreiche Weg.