Automatizzare la configurazione differenziata dell'accesso SSH con Ansible

Introduzione

Nel precedente articolo abbiamo esplorato come implementare una configurazione SSH modulare su un server Ubuntu/Debian, differenziando i metodi di accesso a seconda della provenienza (interna o esterna), degli utenti e dei metodi di autenticazione. Abbiamo separato la configurazione in file multipli all’interno di sshd_config.d, applicando politiche restrittive per l’accesso esterno e più permissive per la rete interna, oltre a integrare fail2ban, firewall (ufw) e il port forwarding su porte alte per aumentare la sicurezza.

In questo nuovo articolo ci concentreremo sull’automazione dello stesso identico approccio utilizzando Ansible, uno strumento di automazione dell’infrastruttura che ci consentirà di distribuire e mantenere le medesime configurazioni su uno o più server in modo semplice, ripetibile e privo di errori umani. L’uso di Ansible garantisce che le configurazioni siano idempotenti: eseguendo il playbook più volte otterremo sempre lo stato desiderato, senza dover ritoccare manualmente i file o rischiare inconsistenze.

Obiettivi principali:

  • Automatizzare l’installazione e configurazione di OpenSSH Server.
  • Gestire i file di configurazione (/etc/ssh/sshd_config e /etc/ssh/sshd_config.d/*.conf) tramite Ansible.
  • Impostare regole ufw e configurazione base di fail2ban.
  • Creare utenti, impostare chiavi SSH, differenziare l’accesso da interno ed esterno.
  • Integrare il port forwarding su porte alte e mostrare come questa configurazione rimane coerente nel tempo.
  • Mantenere la stessa logica del precedente articolo, ma ora tramite playbook Ansible.

Prerequisiti

  • Un controller Ansible (ad esempio un sistema locale con Ansible installato).
  • Connessione SSH al server o ai server bersaglio, con utente avente privilegi sudo.
  • Chiavi SSH già installate sul controller per l’utente amministrativo che si collegherà al target in modo da non dover usare password negli ansible-playbook.
  • Un server Ubuntu/Debian di destinazione, aggiornato e con un utente sudo.
  • Configurazione base di rete, con la possibilità di configurare il router per il port forwarding (come descritto nel precedente articolo).
  • Conoscenza base di Ansible e della struttura di un playbook.

Struttura del Progetto Ansible

Possiamo organizzare il nostro progetto con la seguente struttura:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ansible/
├─ inventory.ini
├─ playbook.yaml
└─ roles/
└─ ssh_config/
├─ tasks/
│ ├─ main.yaml
│ ├─ firewall.yaml
│ ├─ fail2ban.yaml
│ ├─ sshd.yaml
│ └─ users.yaml
├─ templates/
│ ├─ sshd_config.j2
│ ├─ 00-internal.conf.j2
│ └─ 01-external.conf.j2
└─ files/
└─ authorized_keys_deploy
  • inventory.ini: Contiene l’inventario dei nostri host da configurare.
  • playbook.yaml: Il file principale che esegue il ruolo ssh_config.
  • roles/ssh_config/tasks/: Directory con i task divisi per argomento (firewall, fail2ban, sshd, utenti).
  • roles/ssh_config/templates/: I template Jinja2 per generare i file di configurazione.
  • roles/ssh_config/files/: Eventuali file statici come chiavi autorizzate.

È possibile variare la struttura secondo le preferenze, ma questa offre una buona separazione delle responsabilità.

L’inventario di Ansible

Nel file inventory.ini definiremo i server target:

1
2
[ssh_servers]
myserver ansible_host=aa.bb.cc.dd ansible_user=mio_utente

Sostituite aa.bb.cc.dd con l’IP pubblico del server o un dominio che lo punti. L’ansible_user è l’utente sudo locale creato in precedenza.

Variabili e Considerazioni

Nelle variabili potremmo definire:

  • La rete interna da cui consentire l’accesso con password (es. 192.168.0.0/24).
  • L’IP esterno o range ammesso all’accesso via chiave.
  • L’utente dedicato all’accesso esterno (es. deploy).

Queste variabili possono essere definite nel playbook.yaml o in group_vars/ssh_servers.yaml per maggiore scalabilità.

Esempio di variabili (in playbook.yaml o in un file vars.yaml):

1
2
3
4
5
6
vars:
internal_network: "192.168.0.0/24"
external_ip: "aa.bb.cc.dd"
external_user: "deploy"
ssh_port: 22
external_port: 22222 # porta esterna sul router

Il Playbook Principale: playbook.yaml

Questo playbook eseguirà il ruolo ssh_config sui server del gruppo ssh_servers:

1
2
3
4
5
6
7
8
9
10
11
- name: Configure SSH access via Ansible
hosts: ssh_servers
become: true
vars:
internal_network: "192.168.0.0/24"
external_ip: "aa.bb.cc.dd"
external_user: "deploy"
ssh_port: 22
external_port: 22222
roles:
- ssh_config

Il Ruolo ssh_config

Il ruolo si occupa di:

  1. Installare e configurare OpenSSH Server:

    • Assicurarsi che openssh-server sia installato.
    • Copiare i template di configurazione in /etc/ssh/.
    • Riavviare il demone sshd.
  2. Configurare il firewall (ufw):

    • Consentire la porta 22 (interna) se serve.
    • Limitare l’accesso esterno eventualmente per IP specifici.
    • Gestire le regole per consentire l’accesso dalla LAN e limitare l’esterno.
  3. Configurare fail2ban:

    • Installare fail2ban se non presente.
    • Copiare una configurazione base.
    • Riavviare fail2ban.
  4. Creare utenti, chiavi SSH, assegnare i privilegi:

    • Creare l’utente deploy se non esiste.
    • Aggiungere la chiave pubblica a ~deploy/.ssh/authorized_keys.
    • Configurare l’utente amministrativo.
  5. Template per sshd_config e sshd_config.d/*.conf:

    • sshd_config.j2 contenente configurazioni generali.
    • 00-internal.conf.j2 e 01-external.conf.j2 per differenziare gli accessi.

Esempio di tasks/main.yaml

Questo file includerà gli altri file di task:

1
2
3
4
5
6
7
8
9
10
11
- name: Include firewall tasks
include_tasks: firewall.yaml

- name: Include fail2ban tasks
include_tasks: fail2ban.yaml

- name: Include sshd tasks
include_tasks: sshd.yaml

- name: Include users tasks
include_tasks: users.yaml

Esempio di tasks/firewall.yaml

Questa sezione:

  • Installa ufw se non presente.
  • Resetta le regole, permette SSH interno, limita l’esterno all’IP definito.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- name: Ensure ufw is installed
apt:
name: ufw
state: present
update_cache: yes

- name: Reset ufw rules
command: ufw --force reset

- name: Allow internal SSH access
ufw:
rule: allow
port: "{{ ssh_port }}"
proto: tcp
source: "{{ internal_network }}"

- name: Allow external SSH from specified IP only
ufw:
rule: allow
port: "{{ ssh_port }}"
proto: tcp
source: "{{ external_ip }}"

- name: Enable ufw
ufw:
state: enabled

Qui apriamo la 22 solo per la rete interna e per l’IP specificato per l’esterno. Questo riflette la logica precedente: da esterno accede solo un IP e uno specifico utente.

Esempio di tasks/fail2ban.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- name: Ensure fail2ban is installed
apt:
name: fail2ban
state: present
update_cache: yes

- name: Configure fail2ban
copy:
dest: /etc/fail2ban/jail.local
content: |
[sshd]
enabled = true
port = {{ ssh_port }}
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
findtime = 600
bantime = 3600
ignoreip = 127.0.0.1/8 {{ internal_network }}

- name: Restart fail2ban
service:
name: fail2ban
state: restarted

Abbiamo ignorato il range interno in modo che la LAN non venga bannata da tentativi multipli.

Esempio di tasks/users.yaml

Creiamo l’utente deploy e aggiungiamo la sua chiave pubblica. Presumiamo di avere una chiave pubblica pronta nel ruolo (in files/authorized_keys_deploy):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- name: Ensure deploy user exists
user:
name: "{{ external_user }}"
shell: /bin/bash
create_home: yes

- name: Ensure .ssh directory exists
file:
path: "/home/{{ external_user }}/.ssh"
state: directory
owner: "{{ external_user }}"
group: "{{ external_user }}"
mode: '0700'

- name: Deploy the authorized_keys file
copy:
src: authorized_keys_deploy
dest: "/home/{{ external_user }}/.ssh/authorized_keys"
owner: "{{ external_user }}"
group: "{{ external_user }}"
mode: '0600'

Esempio di tasks/sshd.yaml

Qui gestiamo i file di configurazione sshd_config e sshd_config.d tramite template:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
- name: Ensure openssh-server is installed
apt:
name: openssh-server
state: present
update_cache: yes

- name: Backup original sshd_config if present
copy:
src: /etc/ssh/sshd_config
dest: /etc/ssh/sshd_config.bak
remote_src: yes
force: no
when: ansible_stat.exists

vars:
ansible_stat: "{{ lookup('ansible.builtin.stat', '/etc/ssh/sshd_config') }}"

- name: Deploy main sshd_config
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: '0644'

- name: Ensure sshd_config.d directory exists
file:
path: /etc/ssh/sshd_config.d
state: directory
mode: '0755'

- name: Deploy 00-internal.conf
template:
src: 00-internal.conf.j2
dest: /etc/ssh/sshd_config.d/00-internal.conf
owner: root
group: root
mode: '0644'

- name: Deploy 01-external.conf
template:
src: 01-external.conf.j2
dest: /etc/ssh/sshd_config.d/01-external.conf
owner: root
group: root
mode: '0644'

- name: Restart ssh service
service:
name: ssh
state: restarted

Template sshd_config.j2

Questo ricalca le configurazioni base:

1
2
3
4
5
6
UsePAM yes
PermitRootLogin no
ChallengeResponseAuthentication no
PasswordAuthentication yes
PubkeyAuthentication yes
Include /etc/ssh/sshd_config.d/*.conf

Template 00-internal.conf.j2

Consente da rete interna l’uso della password e delle chiavi a qualsiasi utente:

1
2
3
Match address {{ internal_network }}
PasswordAuthentication yes
PubkeyAuthentication yes

Template 01-external.conf.j2

Limitato all’utente deploy e all’indirizzo IP esterno, solo chiave:

1
2
3
Match User {{ external_user }}, Address {{ external_ip }}
PasswordAuthentication no
PubkeyAuthentication yes

Port Forwarding su Porta Alta (22222)

Dal lato del router, come nel precedente articolo, si configura il port forwarding della porta 22222 esterna verso la 22 interna del server. Ansible non può effettuare questa operazione sul router a meno di avere plugin specifici o un router programmabile (ad esempio via API). Presumiamo che questa modifica sia stata già fatta a mano sul router:

  • Dall’esterno: ssh -p 22222 deploy@mio_dominio
    Il router inoltra la 22222 → 22 del server interno.

Nella configurazione di Ansible non cambia nulla per sshd, rimane in ascolto sulla 22 interna. Il firewall filtra gli accessi, l’utente deploy può accedere solo dall’external_ip consentito con chiave, mentre la rete interna può accedere con password.

Se si volesse essere più coerenti, potremmo cambiare la Port in sshd_config.j2. Tuttavia, il port forwarding è una soluzione migliore, perché non richiede modifiche al demone SSH e non influisce sugli accessi LAN.

Eseguire il Playbook

Dopo aver preparato tutto:

1
ansible-playbook -i inventory.ini playbook.yaml

Se il vostro accesso SSH al server è pronto, Ansible applicherà in pochi secondi tutte le configurazioni descritte. Eseguite nuovamente il playbook in futuro per mantenere lo stato o dopo modifiche alle variabili: le configurazioni saranno sempre coerenti.

Possibili Estensioni e Conclusione

In questo articolo abbiamo replicato l’approccio modulare e differenziato dell’accesso SSH mediante la creazione e applicazione automatizzata di playbook Ansible. Questo consente di scalare la stessa configurazione su più server e di mantenere un controllo centralizzato, coerente e privo di rischi di errore umano.

Per ulteriori estensioni future, si potrebbero creare ruoli separati per la gestione delle chiavi SSH, per l’applicazione di policy di sicurezza più complesse, per l’integrazione con sistemi di gestione delle identità o per la distribuzione automatica di fail2ban con configurazioni personalizzate.
Inoltre, si potrebbe integrare Ansible con strumenti CI/CD per testare automaticamente le configurazioni prima del deployment in produzione.

Con l’introduzione di Ansible, abbiamo compiuto un passo significativo verso l’automazione dell’infrastruttura e la riduzione del carico di lavoro amministrativo, garantendo un accesso SSH sicuro, modulare e facile da mantenere.