Ansible con bastion host

Andrea Dainese
30 Ottobre 2022
Post cover

Questa è la seconda parte della mia panoramica su IaC basata su un esperimento personale: la costruzione di una Cyber range utilizzando il paradigma IaC. Ecco la prima e la terza parte.

Dal punto di vista puramente progettuale, l’approccio VPN da client a sito è ancora il migliore. Ma dal punto di vista dell’automazione, ho dovuto ridisegnarlo includendo un bastion host. L’idea non mi piace molto, ma i vantaggi superano i svantaggi.

Scenario

Rispetto allo scenario con il concentratore VPN da client a sito, questo sembra più semplice: le VM interne sono raggiungibili tramite il bastion host Linux. In pratica, il bastion host fa da proxy per le connessioni SSH. Il bastion host non richiede alcuna configurazione.

Poiché il mio scenario richiede che i partecipanti possano raggiungere le VM interne, copio nel bastion host la chiave privata SSH necessaria per accedere alle VM interne. In futuro, penso che sia meglio se il bastion host funge anche da concentratore OpenVPN.

Diagramma della Cyber range con bastion host

Bastion host e inventario dinamico AWS EC2

A questo punto Ansible dovrebbe:

  • configurare il bastion host utilizzando l’indirizzo IP pubblico;
  • configurare gli host interni utilizzando l’indirizzo IP privato tramite l’indirizzo IP pubblico del bastion host.

A causa del design di Spacelift, ho dovuto configurare tutto utilizzando un unico playbook Ansible. Ciò significa che l’inventario Ansible di AWS EC2 deve:

  • restituire l’indirizzo IP pubblico per il bastion host;
  • restituire gli indirizzi IP privati per le VM interne.

Inoltre, Ansible dovrebbe configurare il proxy SSH appena prima di accedere a ciascun host interno.

Dopo diversi tentativi, trovo una ricetta funzionante. Iniziamo con l’inventario di AWS EC2:

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

Ricorda che ho scritto che IaC è l'80% pianificazione e standardizzazione? Assumo che in ogni scenario userò “bastion” come nome host per il bastion host e etichetto l’hostname nella configurazione AWS EC2. Questa è una delle mie “standard” (assunzioni).

Nella configurazione dell’inventario Ansible di AWS EC2 sopra, restituisco l’indirizzo IP pubblico solo se l’etichetta Name è uguale a bastion. L’inventario restituisce l’indirizzo IP privato per qualsiasi altra VM.

Il mio playbook Ansible inizia configurando l’host Ansible:

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

Nel ruolo, trovo la chiave SSH disponibile e la carico sul bastion host per i partecipanti. Ricorda che ho scritto che voglio una “soft” lock-in? È qui che rendo il playbook compatibile con gli ambienti Spacelift e il mio. Ho anche usato l’etichetta “always” perché configuro ansible_ssh_private_key_file (vedi dopo).

Configurazione degli host interni tramite bastion

A questo punto posso configurare gli host interni. Un’altra assunzione che ho fatto è che le etichette contengano eventuali attributi interessanti che uso in Ansible per raggruppare, interrogare e configurare gli host. In pratica sto usando le seguenti etichette:

  • Os:ubuntu: per le VM Ubuntu;
  • Database:mariadb: per le VM MariaDB;
  • Webapp:wordpress: per le VM con Wordpress.

Il mio playbook Ansible esegue più passaggi:

- 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

# [...]

I fatti di Ansible sono specifici dell’host: ciò significa che se imposto ansible_ssh_private_key_file sul bastion host, è indefinito per gli altri host.

Come potrei configurare il proxy SSH per qualsiasi host interno escludendo il bastion host?

La magia avviene nel file default.yaml utilizzando facts e magic variables di Ansible. Il file default.yaml è incluso in qualsiasi playbook rivolto agli host interni e configura il proxy SSH utilizzando le informazioni dall’inventario:

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"] }}"  
[...]

Ricorda che ho scritto che IaC è l'80% pianificazione e standardizzazione? Sto ancora assumendo che il bastion host si chiami bastion e l’etichetta User contenga l’utente remoto per ciascuna VM. Quindi, per ogni VM:

  • ansible_user: contiene il nome utente remoto configurato nell’etichetta User;
  • ansible_ssh_private_key_file: contiene la chiave privata SSH locale (archiviata nella VM di Ansible) e le informazioni sono prese dall’entry del bastion host configurata nell’inventario;
  • ansible_ssh_common_args: contiene il comando proxy SSH utilizzato da Ansible e configurato utilizzando l’indirizzo IP pubblico del bastion preso dall’inventario.

Entrambe le chiavi SSH devono essere locali all’host di Ansible.

A questo punto, ho un singolo playbook Ansible che configura il mio scenario di Cyber range utilizzando un bastion host.

Conclusioni

Trovo che i bastion host siano comunemente utilizzati. Non mi piacciono molto perché dal punto di vista della sicurezza il bastion host è un altro host con superpoteri. Ma dal punto di vista dell’automazione, questo è effettivamente l’unico modo di successo.