Ansible - erweiterte Konfigurationsbeispiele: Ansible-Controll-Node und SSH-Jumphosts
Ausgangssituation
In verteilten und getrennten Netz(werk)umgebungen ist es sehr oft nicht ohne Aufwand möglich zu Administrations- und Orchestrierungsaufgaben alle Hosts zu erreichen.
Schauen wir uns hierzu einfach mal nachstehende exemplarische Skizze an.
Von der Admin-Workstation bzw. dem Ansible-Controll-Node aus, wollen wir nun nicht nur zum nächstgelegenen Host erreichen, sondern auch zum übernächsten oder gar zu einem Host im Internet, den wir aber aus Sicherheitsgründen nicht direkt erreichen dürfen und auch können.
Die Komfortabelste Variante ist nun die Nutzung der Option ProxyCommand. Bei „nur“ einem Jump-Host kann man dies im Inventory noch recht bequem und einfach abbilden. Weitaus schwieriger wird die Sache, bei Umgebungen, bei denen mehrere Jumphost beteiligt sind. Hier bietet es sich an, die SSH-Client Konfiguration des Ansible Control- bzw Admin-Users zu verwenden!
Weitaus einfacher gestaltet sich o.g. Szenario in dem man auf die SSH-Clientkonfigurationsdatei ~/.ssh/config
zurückgreift. Nachfolgendes Beispiel zeigt exemplarisch solch eine Clientspezifische Konfigurationsdatei:
$ vim ~/.ssh/config
- ~/.ssh/config
# Default Werte Host * Port 22 Protocol 2 user admin # Django : 2012-06-13 # ssh-jumps über mehrere Sprunghosts # Erster Sprunghost (fwc) - direkt erreichbar # Host --> fwc Host fwc Hostname firewall-c.idmz.nausch.org IdentityFile ~/.ssh/id_ed25519_idmz # Zweiter Sprunghost (fwb) - nur über fwc erreichbar # Host --> fwc --> fwb Host fwb Hostname firewall-b.edmz.nausch.org IdentityFile ~/.ssh/id_ed25519_edmz ProxyCommand ssh -A -q -W %h:%p fwc # Dritter Sprunghost (fwa) - nur über fwb erreichbar # Host --> fwc --> fwb --> fwa Host fwa Hostname firewall-a.nausch.org Port 22222 user sysadmin IdentityFile ~/.ssh/id_ed25519_edmz ProxyCommand ssh -A -q -W %h:%p fwc # externer Server im Internet nur über externe Firewall "A" erreichbar # also: Host --> fwc --> fwb --> fwa --> daxie Host s1u7 Hostname <was-das-auch-immer-für-ein geiler-FQDN-sein-mag> Port 42422 user n3rd IdentityFile ~/.ssh/id_ed25519_n3rd ProxyCommand ssh -A -q -W %h:%p fwa
Nun können wir ganz einfach direkt einen Tunnel zu unserem Zielhost aufspannen, genauso also würden wir den Zielhost direkt „sehen“.
$ ssh fwa
Auch können wir nun ohne grossen Aufwand Dateien von einem Ende zum anderen Ende kopieren bzw. Ansible dies ermöglichen.
$ scp ~/Downloads/enigmail-1.4-sm+tb.xpi 51u7:/tmp/
$ scp 51u7:/home/8483/51lv14/04x.png .
Somit ist natürlich auch Ansible in der Lage die definierten Zielhost aus dem Inventory ohne Probleme zu erreichen, da hier die SSH-Client-Konfiguration unseres Admin-Accounts auf unserem Ansible-Controll-Node verwendet wird.
Die Pflege der Konfigurationsdatei ~/.ssh/config
überlassen wir nun natürlich nicht jedem Admin einzeln, da dies viel zu fehleranfällig bzw. zeitaufwändig wäre.
Die aktuelle SSH-Clientconfigurationsdatei auf Djangos-Ansible-Controll-Node hat immerhin eine stattliche Anzahl an Konfigurationszeilen, Bsp.:
$ cat ~/.ssh/config | wc -l
1860
Das will sicherlich niemand, auch wenn er noch so fleissig und gewissenhaft ist, per Hand erledigen!
Wir werden dies mit den Informationen aus dem zentral gepflegten Inventory und einem Ansible-Playbook bewerkstelligen. Somit ist sichergestellt, dass auf jedem Ansible-Controll-Node und jedem Admin die betreffende Datei zur Verfügung steht und somit die Ansible- Playbooks auch überall unter den gleichen (Labor-)Bedingungen laufen können!
Lösungsmöglichkeit
Grundsätzlich alle möglichen Konfigurationsoptionen zur SSH-Clientkonfiguration finden wir in der zugehörigen man-page.
$ man ssh_config
Sehen wir uns unsere bisher manuell gepflegte SSH-Clientkonfigurationsdatei mal etwas genauer an und blenden mal ggf. besondere Fälle mal aus, so stellen wir zwei Dinge fest.
Host-Beispiele
Direkt erreichbare Hosts
Wenn wir uns einmal folgendes Beispiel betrachten, dann sehen wir folgende Dinge.
- ~/.ssh/config
... Host pml010003 Hostname 10.10.10.3 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_intranet ...
Wir haben demnach in Summe sechs gesetzte Parameter:
Host
: Kurzname (alias) über den das Ziel erreichbar ist,Hostname
: FQDN oder IP-Adresse des Ziels,User
: User der zum Verbinden benötigt wird,Port
: SSH-Port des Zielsystems,Protocol
: verwendetes SSH-Protokoll sowieIdentityFile
: Datei des SSH-Keys, der bei der Verbindung verwendet werden muss.
Hosts erreichbar nur via Jump-Host
Bei einem Host, den wir nicht direkt erreichen können, sondern lediglich über einen Jump-Host stellen wir fest dass wir hier einen Parameter mehr haben.
- ~/.ssh/config
... Host fwi Hostname 10.30.30.20 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_idmz ProxyCommand ssh -q -W %h:%p vml030020 Host fw3 Hostname 10.50.50.250 User django Port 10022 Protocol 2 IdentityFile ~/.ssh/id_edmz ProxyCommand ssh -q -W %h:%p fwi ...
Bei diesem Beispiel haben wir zwei Jumphost. Der erste Host fwi
kann nicht direkt, sondern nur über den Jump-Host vml030020
erreicht werden. Der zweite Jump-Host fw3
wiederum ist nur über den ersten Jump-Host fwi
erreichbar. Die entscheidende Konfigzeile mit ProxyCommand
ist dabei jeweils:
ProxyCommand ssh -q -W %h:%p <-Sprung-Host->
Der Wert <-Sprung-Host->
kennzeichnet nun, über welchen Jump-Host das Ziel erreichbar sein wird.
Zusätzlich zu den Parametern eines Standardhosts haben wir hier eine weitere Option ProxyCommand
, der Definition wie und über welchen Jump-Host das Ziel erreicht werden kann. Wie Eingangs bereits erwähnt, werden wir nun die komplette SSH-Client-Konfigurationsdatei nicht manuell pflegen, sondern wir werden Ansible mit dieser Aufgabe betrauen, genauers hierzu finden wir in nachfolgenden Abschnitt dieses WIKI-Artikels.
Ansible-Playbook zum Erstellen der SSH-Client-Konfigurationsdatei
Playbook: ssh_client_config.yml
Unser Playbook legen wir nun wie gewohnt in unserem Playbook-Verzeichnis ~/ansible/playbooks/
an.
$ vim ~/ansible/playbooks/ssh_client_config.yml
- ssh_client_config.yml
--- # YAML Start # Ansible Playbook zum automatisierten Anlegen der # SSH Client Konfigurationsdatei ~/.ssh/config basierend # auf den Inhalten des Inventories. # # Aufruf aus dem entsprechenden Arbeits-Verzeichnis via: # ansible-playbook playbooks/ssh_client_config.yml # - name: ssh_client_config.yml # Name des Playbook hosts: localhost # Host die zur Anwendung kommen sollen vars: # ssh_user: "{{ lookup('env','USER') }}" # aktuellen Bneutzer ermitteln und Variable übergeben # # roles: # Definition der zugehörigen Rollen - ssh_client # Prometheus Server: Installation und Konfiguration ... # YML Ende
Dieses Playbook unterscheidet sich nun erst einmal gewaltig von denen der bisher hier gezeigten. Warum? Das werden wir uns nun im nachfolgendem Abschnitt ansehen.
Rolle: ssh_client
Im Gegensatz zu den bisherigen Playbook-Beispielen werden wir nun dem nächstem Ansible Kapitel "Rollen" hier etwas vorgreifen und das Playbook, welches zum Erzeugen der SSH-Client-Konfigurationsdatei verwendet wird, nicht mehr in einem flatfile schreiben, sondern dies entsprechend strukturiert aufbauen.
Die Grundvoraussetzungen in Form der betreffenden Verzeichnisstruktur haben wir ja bereits bei der initialen Ansible-Konfiguration erzeugt.
$ tree ~/ansible/roles/common/ -d
/home/django/ansible/roles/common/
├── defaults
├── files
├── handlers
├── library
├── lookup_plugins
├── meta
├── module_utils
├── tasks
├── templates
└── vars
Dies nehmen wir nun als Kopiervorlage und erstellen eine neue Rolle ssh_client
.
$ cp -avr ~/ansible/roles/common/ ~/ansible/roles/ssh_client
In unserem Playbook haben wir bereits auf diese role verwiesen. Im Verzeichnis tasks
legen wir nun die YML-Dateien mit den Aufgaben ab, die abgearbeitet werden sollen. Standardmäßig wird Ansible dort die Datei main.yml
suchen und den Anweisungen dort folgen. Wir legen nun diese Datei an.
$ vim ~/ansible/roles/ssh_client/tasks/main.yml
- main.yml
--- # Playbook/Rollen zur Generierung der SSH Client-Config - include_tasks: client_config.yml # SSH Client Configdatei erzeugen und kopieren. tags: clientconfiguration # ... # YML Ende
Wie wir sehen, wird hier erst einmal nur ein weiterer task inkludiert, dessen YML-Datei noch fehlt. Diese Datei legen wir nun gleich noch als nächstes an.
$ vim ~/ansible/roles/ssh_client/client_config.yml
- client_config.yml
--- # YAML Start # SSH Client Configdatei erzeugen und kopieren. - name: "Generieren und kopieren der SSH Client Konfiguration ~/.ssh/config." ansible.builtin.template: src: templates/ssh_client_config.j2 dest: /home/{{ ssh_user }}/.ssh/config owner: '{{ ssh_user }}' group: '{{ ssh_user }}' mode: '0640' ... # YML Ende
Mit Hilfe dieses Tasks wird nun, mit Hilfe des Ansible Modules template, die Datei ~/.ssh/config
im $HOME
-Verzeichnis des aktuellen Admins angelegt. Die Variable ssh_user
haben wir im Playbook definiert und füllen diese dann entsprechend beim Ausführen des Playbooks.
Man könte jetzt natürlich sagen, warum definiert man denn diesen Task nicht gleich direkt in der main.yml
, ist doch eh' „nur eine“ Aufgabe. Ja, das könnte man durchaus so sagen. Aber wir gewöhnen uns am Besten gleich von Haus aus an, und strukturieren unsere Rollen entsprechend. Denn so hat man es später einfacher, wenn man bestimmte Teile nur bei der Ausführung haben möchte oder wenn man bestimmte Schritte z.B. ausschließen möchte.
Jinja2-Template: ssh_client_config.j2
In dem Task client_config
hatten wir definiert, dass unsere SSH-Client-Konfiguration auf Basis eines Jinja2-Templates erstellt werden soll. Diese Datei legen wir nun als nächstes an.
$ vim ~/ansible/roles/ssh_client/templates/ssh_client_config.j2
- ssh_client_config.j2
# Generiert mit Hilfe von Ansible am {{ ansible_date_time.date }} - diese Datei nicht manuell bearbeiten! # Clientkonfigurationsbeispiel für unterschiedliche Zielsysteme ## statische Konfiguration # localhost Host localhost Hostname 127.0.0.1 IdentityFile ~/.ssh/id_intra # externer Einwahl-Hosts Host example Hostname 93.184.216.34 Port 12345 Protocol 2 ForwardX11 yes ForwardAgent yes IdentityFile ~/.ssh/id_example ## dynamisch aus dem Inventory generierte Konfiguration # interne Systeme - DMZ {% for host in groups['DMZ'] %} Host {{ host }} Hostname {{ hostvars[host]['host_ipv4'] }} User {{ hostvars[host]['ssh_user'] }} Port {{ hostvars[host]['ssh_port'] }} Protocol {{ hostvars[host]['ssh_protocol'] }} IdentityFile {{ hostvars[host]['ssh_keyfile'] }} Host {{ hostvars[host]['host_alias'] }} Hostname {{ hostvars[host]['host_ipv4'] }} User {{ hostvars[host]['ssh_user'] }} Port {{ hostvars[host]['ssh_port'] }} Protocol {{ hostvars[host]['ssh_protocol'] }} IdentityFile {{ hostvars[host]['ssh_keyfile'] }} {% endfor %} # interne Systeme - Intranet {% for host in groups['intranet'] %} Host {{ host }} Hostname {{ hostvars[host]['host_ipv4'] }} User {{ hostvars[host]['ssh_user'] }} Port {{ hostvars[host]['ssh_port'] }} Protocol {{ hostvars[host]['ssh_protocol'] }} IdentityFile {{ hostvars[host]['ssh_keyfile'] }} Host {{ hostvars[host]['host_alias'] }} Hostname {{ hostvars[host]['host_ipv4'] }} User {{ hostvars[host]['ssh_user'] }} Port {{ hostvars[host]['ssh_port'] }} Protocol {{ hostvars[host]['ssh_protocol'] }} IdentityFile {{ hostvars[host]['ssh_keyfile'] }} {% endfor %} # externe System {% for host in groups['freifunk'] %} Host {{ host }} Hostname [{{ hostvars[host]['host_ipv6'] }}] User {{ hostvars[host]['ssh_user'] }} Port {{ hostvars[host]['ssh_port'] }} Protocol {{ hostvars[host]['ssh_protocol'] }} IdentityFile {{ hostvars[host]['ssh_keyfile'] }} ProxyCommand ssh -q -W %h:%p {{ hostvars[host]['host_sshjump'] }} Host {{ host }}-extern Hostname {{ hostvars[host]['host_ipv6'] }} User {{ hostvars[host]['ssh_user'] }} Port {{ hostvars[host]['ssh_port'] }} Protocol {{ hostvars[host]['ssh_protocol'] }} IdentityFile {{ hostvars[host]['ssh_keyfile'] }} {% endfor %} {% for host in groups['raspbian'] %} Host {{ host }} Hostname {{ hostvars[host]['host_ipv4'] }} User {{ hostvars[host]['ssh_user'] }} Port {{ hostvars[host]['ssh_port'] }} Protocol {{ hostvars[host]['ssh_protocol'] }} IdentityFile {{ hostvars[host]['ssh_keyfile'] }} ProxyCommand ssh -q -W %h:%p {{ hostvars[host]['host_sshjump'] }} Host {{ host }}-intern Hostname {{ hostvars[host]['host_ipv4'] }} User {{ hostvars[host]['ssh_user'] }} Port {{ hostvars[host]['ssh_port'] }} Protocol {{ hostvars[host]['ssh_protocol'] }} IdentityFile {{ hostvars[host]['ssh_keyfile'] }} {% endfor %}
Das Beispiel passen wir natürlich unserer Umgebung entsprechend an. Einzelne Netzsegmente haben wir in unserem Inventory in Hostgruppen unterteilt. Basierend auf den Host-Definitionen im Inventory werden die Variablen entsprechend mit den spezifischen Hostdaten gefüllt um damit dann beim Lauf des Playbooks die passende SSH-Client-Konfigurationsdatei zu schreiben. Mit Hilfe der Schleifen, welche jeweils durch {% for host in groups['<--HOST-GROUP-NAME-->'] %}
und {% endfor %}
eingeschlossen sind, werden beim Abarbeiten des Playbooks für jeden Host aus der spezifischen Gruppe (repräsentiert durch den Namen der Hostgruppe <--HOST-GROUP-NAME-->
) Abschnitte in der ~/.ssh/config
-Datei hinterlegt.
Inventory
Die Konfigruationsdaten unserer Host halten wir in unserem Inventory vor. Hierbei spielt es grundlegend keine Frage ob dies manuell gepflegt, oder scriptiert aus einer CMBD exportiert und aufbereitet wurde.
Für unser Playbook-Beispiel hier greifen wir auf das exemplarische Inventory, mit welchem wir uns im Kapitel Ansible - Erweiterte Konfigurationsbeispiel: Inventory bereits intensiv beschäftigt hatten, zurück. Nachfolgend sehen wir, wie solch eine Inventory Datei aufgebaut und strukturiert sein könnte. Das exemplarische Beispiel ist hier entsprechend gekürzt und die Stellen mit Hilfe von ...
markiert wiedergegeben„
$ less ~/ansible/inventories/production/hosts
- hosts
# Generiert mit Hilfe von Ansible am 2022-09-20 - diese Datei nicht manuell bearbeiten! # Inventory Datei für die System-Umgebung bei nausch.org # # Hinweise: # Kommentare beginnen mit einem '#'-Zeichen # leere Zeilen werden ignoriert # Host- und Gruppendefinitionen werden mit [] abgegrenzt # Hosts können über ihren Hostnamen, FQN oder ihrer IP-Adresse definiert # übergeordnete Gruppen werden durch [:children] abgegrenzt # # Host-Definitionen # Hosts ohne Gruppenzuordnung localhost [intranet] pml010002 pml010003 pml010004 ... ... pml010124 pml010125 pml010126 [IDMZ] vml030010 vml030020 vml030030 vml030040 ... ... vml030230 vml030240 vml030250 [EDMZ] vml050010 vml050020 vml050030 vml050040 vml050250 [TKDMZ] vml070010 vml070020 vml070030 [external] customer_no_001 customer_no_002 ... ... customer_no_042 [gluon] ff_pliening_gbw__ug_ ff_pliening_gbw_egod ff_pliening_gbw_ogod ff_pliening_gbw_dgod ff_pliening_gbw_cpod ff_roding_fwg_nausch [raspbian] ff_pliening_rpb4_ol_v6 # Host-Gruppen-Definitionen # (zu welcher Gruppe gehören Untergruppen bzw. Hosts) [freifunk:children] gluon raspbian [linux:children] intranet IDMZ EDMZ TKDMZ external
Die betreffenden Hostspezifischen Variablen halten wir hier in entsprechenden Dateien bzw. Unterverzeichnissen vor. Das nachfolgende Beispiel hier zeigt die Host-spezifischen Variablen eines Hosts im Intranet.
$ less ~/ansible/inventories/production/host_vars/pml111002
- vml030010
# Generated by Ansible on 2022-09-20, do not edit manually! host_alias: fw1 host_mac: "84:3b:de:ad:be:ef" host_ipv4: "10.30.30.2" host_ipv6: "::1" ssh_port: 22 ssh_protocol: 2 ssh_keyfile: ~/.ssh/id_idmz"
Als Beispiel für einen externen Host, der nur via Jump-Host erreichbar ist sehen wir uns die Inventory-Host-Definition des Hosts ff_pliening_gbw__ug_
näher an.
$ less ~/ansible/inventories/production/host_vars/ff_pliening_gbw__ug_
- ff_pliening_gbw__ug_
# Generated by Ansible on 2022-09-20, do not edit manually! host_alias: host_ipv4: host_ipv6: 2001:678:e68:102:32b5:c2ff:fe56:62b1 ssh_user: root ssh_port: 22 ssh_protocol: 2 ssh_keyfile: ~/.ssh/id_freifunk host_sshjump: vml070010 branch: "stable" domain: "ffmuc_muc_ost" director: "ffmuc_muc_ost" node_contact_address: "Django [BOfH] | django@nausch.org | chat: @django" node_hostname: "ff_pliening_gbw_antipode" node_latitude: "-48.19861319429455" node_longitude: "-168.2017571420684" node_model: "TP-Link TL-WDR4300 v1" node_share_location: "True" node_ghostmode: "False" node_release: "experimental" node_autoupdate: "False"
Hier sehen wir nun dass unter anderem ein anderer SSH-User, ein anderes SSH-Keyfile sowie ein Jump-Host benutzt wird. Ferner finden sich im Anschluß noch weitere Host-spezische Variablen, für die Konfiguration des betreffenden Freifunk-Knotens.
Playbook-Lauf
Dank der Vorkonfiguration unseres Ansible-Controll-Nodes, welche wir im Kapitel Ansible - erweiterte Konfigurationsbeispiele: Ansible mit Hilfe von Ansible einrichten hier in Djangos WIKI vorgenommen hatten, reicht zum Starten des Playbooks folgender Befehlsaufruf:
$ ansible-playbook ~/ansible/playbooks/ssh_client_config.yml
PLAY [ssh_client_config.yml] ******************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************
ok: [localhost]
TASK [ssh_client : include_tasks] *************************************************************************************************
included: /home/django/ansible/roles/ssh_client/tasks/client_config.yml for localhost
TASK [ssh_client : Generieren und kopieren der SSH Client Konfiguration ~/.ssh/config.] *******************************************
ok: [localhost]
PLAY RECAP *************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Ergebnis: ~/.ssh/config
Als Ergebnis erhalten wir dann quasi auf Knopfdruck immer eine aktuelle SSH-Client-Konfigurationsdatei ~/.ssh/config
, basierend auf den Konfigurationsdaten aus unsere Inventory.
- ~/.ssh/config
# Generiert mit Hilfe von Ansible am 2022-09-24 - diese Datei nicht manuell bearbeiten! # Clientkonfigurationsbeispiel für unterschiedliche Zielsysteme ## statische Konfiguration # localhost Host localhost Hostname 127.0.0.1 IdentityFile ~/.ssh/id_intra # externer Einwahl-Hosts Host example Hostname 93.184.216.34 Port 12345 Protocol 2 ForwardX11 yes ForwardAgent yes IdentityFile ~/.ssh/id_example ## dynamisch aus dem Inventory generierte Konfiguration # interne Systeme - IDMZ Host vml030010 Hostname 10.30.30.10 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_idmz Host fw1 Hostname 10.30.30.10 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_idmz Host vml030020 Hostname 10.30.30.20 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_idmz Host fw2 Hostname 10.30.30.2 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_idmz Host vml030030 Hostname 10.30.30.30 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_idmz Host Hostname 10.30.30.30 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_idmz ... ... Host vml030250 Hostname 10.30.30.250 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_idmz Host db_clusternode_3 Hostname 10.30.30.250 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_idmz # interne Systeme - Intranet Host pml010002 Hostname 10.10.10.2 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_intra Host kvm_1 Hostname 10.10.10.2 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_intra ... ... Host pml010126 Hostname 10.10.10.126 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_intra Host feinstaubsensor Hostname 10.10.10.126 User django Port 22 Protocol 2 IdentityFile ~/.ssh/id_intra # externe System Host ff_pliening_gbw__ug_ Hostname [2001:678:e68:102:32b5:c2ff:fe56:62b1] User root Port 22 Protocol 2 IdentityFile ~/.ssh/id_freifunk ProxyCommand ssh -q -W %h:%p vml070010 Host ff_pliening_gbw__ug_-extern Hostname 2001:678:e68:102:32b5:c2ff:fe56:62b1 User root Port 22 Protocol 2 IdentityFile ~/.ssh/id_freifunk ... ... Host ff_roding_fwg_nausch Hostname [2001:678:e68:109:8e3b:adff:feeb:f2a6] User root Port 22 Protocol 2 IdentityFile ~/.ssh/id_freifunk ProxyCommand ssh -q -W %h:%p vml070010 Host ff_roding_fwg_nausch-extern Hostname 2001:678:e68:109:8e3b:adff:feeb:f2a6 User root Port 22 Protocol 2 IdentityFile ~/.ssh/id_freifunk
Fazit und Ausblick
Die manuelle, zeitraubende und ggf. Fehlerbehaftete Pflege der SSH-Client-Konfigurations-Datei durch mehrere Admins auf verschiedenen Ansible-Kontroll-Knoten ist somit Geschichte. Ferner sind wir unabhängig und können so viele SSH-Jump-Hosts verwenden, die eben zum Erreichen der Zielhost von Nöten sind.
Die initiale Fragestellung Wie wird sicher gestellt, dass alle Ziele auch erreichbar sind?, die wir bei unseren Vorüberlegungen angestellt hatten, können wir also auch als erfolgreich erledigt abhaken und wir sind bei unserem Ziel von Automatisierung und Orchestrierung einen wesentlichen Schritt weiter gekommen.