Azure Bastion in una Landing Zone Azure con Terraform: accesso amministrativo sicuro senza IP pubblici
Azure Bastion fornisce accesso RDP e SSH alle macchine virtuali direttamente dal portale Azure, senza necessita’ di assegnare IP pubblici alle VM ne’ di esporre le porte 3389 e 22 verso Internet. Collocato nell’hub di una topologia hub & spoke, un’unica istanza di Bastion serve le VM di tutti gli spoke grazie al peering, con notevole risparmio rispetto a un’istanza per spoke.
Questa guida descrive il deployment con Terraform di Azure Bastion Standard tramite gli Azure Verified Modules (AVM), nel resource group rg-hub-itn (subscription Connectivity, region Italy North). Presuppone la Hub Virtual Network gia’ deployata con la subnet AzureBastionSubnet.
Cos’è Bastion e perché implementarlo
Per amministrare una macchina virtuale (aprire il desktop remoto su Windows con RDP, o una sessione a riga di comando su Linux con SSH) serve raggiungerla dalla rete. Il modo tradizionale, ma pericoloso, e’ assegnare alla VM un indirizzo IP pubblico e aprire le porte di gestione (3389 per RDP, 22 per SSH) verso Internet. Quelle porte esposte sono pero’ uno dei bersagli piu’ attaccati in assoluto: bot e attaccanti le scandagliano in continuazione cercando password deboli.
Azure Bastion risolve il problema facendo da ponte sicuro. E’ un servizio gestito che vive nell’hub e permette di connettersi alle VM direttamente dal portale Azure, attraverso il browser. Le macchine non hanno bisogno di alcun IP pubblico e le porte di gestione restano chiuse verso Internet: la connessione avviene tutta all’interno della rete Azure, in modo cifrato e tracciato.
I motivi principali per implementare Bastion sono:
- Nessun IP pubblico sulle VM – si elimina la superficie di attacco piu’ comune, perche’ le macchine non sono raggiungibili da Internet.
- Porte di gestione chiuse – RDP e SSH non sono mai esposti verso l’esterno, azzerando il rischio di attacchi di forza bruta su quelle porte.
- Accesso semplice e controllato – la connessione parte dal portale con le normali credenziali Azure, quindi si applicano autenticazione e permessi centralizzati.
- Un’unica istanza per tutti – collocato nell’hub, un solo Bastion serve le VM di tutti gli spoke grazie al peering, senza doverne installare uno per ogni rete.
Prerequisito: la subnet AzureBastionSubnet
Azure Bastion richiede una subnet dedicata denominata esattamente AzureBastionSubnet, con prefisso di almeno /26. Nel piano di indirizzamento dell’hub questa subnet e’ 10.0.0.64/26 ed e’ definita in network.tf. Il nome e’ case-sensitive e non modificabile.
ATTENZIONE: Se la subnet non si chiama esattamente AzureBastionSubnet o e’ piu’ piccola di /26, il deployment di Bastion fallisce. La subnet non deve contenere altre risorse.
Passo 1: L’host Azure Bastion
Bastion e’ creato con il modulo AVM avm-res-network-bastionhost in SKU Standard. Lo SKU Standard aggiunge rispetto al Basic funzionalita’ come copia-incolla file, IP connect, tunneling nativo e scaling automatico. L’host e’ distribuito sulle availability zones 1, 2, 3 per l’alta disponibilita’.
Il modulo crea automaticamente l’indirizzo IP pubblico Standard necessario (pip-bas-hub-itn) tramite il flag create_public_ip, collegandolo alla AzureBastionSubnet. Il file completo bastion.tf:
|
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 |
# Azure Bastion Standard con availability zones abilitate. # Lo SKU Standard aggiunge funzionalita' rispetto al Basic: # copia-incolla file, IP connect, tunneling nativo, scaling automatico. # Il modulo crea automaticamente l'indirizzo IP pubblico Standard necessario. module "bastion" { source = "Azure/avm-res-network-bastionhost/azurerm" version = "~> 0.9" name = var.bastion_name location = var.location parent_id = module.resource_group.resource_id tags = local.global_tags enable_telemetry = false sku = "Standard" zones = ["1", "2", "3"] # La subnet deve chiamarsi AzureBastionSubnet, definita in network.tf. # Il modulo crea il public IP automaticamente. ip_configuration = { subnet_id = module.vnet.subnets["AzureBastionSubnet"].resource_id create_public_ip = true public_ip_name = "pip-${var.bastion_name}" } } |
Passo 2: L’output del bastion
Il file outputs.tf espone il resource ID dell’host, utile per riferimenti in altri moduli o per verifiche.
|
1 2 3 4 5 |
output "bastion_id" { description = "Resource ID dell'host Azure Bastion" value = module.bastion.resource_id } |
Passo 3: Applicare e verificare
Dopo terraform init, applicare il piano. Il provisioning di Bastion richiede tipicamente diversi minuti.
|
1 2 3 4 |
Terraform plan -out tfplan Terraform apply tfplan |
ATTENZIONE: Azure Bastion Standard comporta un costo orario fisso piu’ un costo per le unita’ di scaling, indipendentemente dall’utilizzo. In ambienti non produttivi valutare lo SKU Basic o lo spegnimento quando non necessario.
Al termine, aprire rg-hub-itn nel portale e verificare la presenza di bas-hub-itn (Bastion) e del relativo Public IP pip-bas-hub-itn.

Figura 1: il contenuto di rg-hub-itn: si riconoscono bas-hub-itn (Bastion) e il Public IP pip-bas-hub-itn.
Verifica del deployment
Il deployment di Bastion e’ completo quando:
- bas-hub-itn e’ in stato Succeeded con SKU Standard e zone 1/2/3.
- pip-bas-hub-itn e’ associato all’host.
- Dal portale, selezionando una VM di uno spoke in peering, e’ disponibile il pulsante Connect > Bastion.
COMPLETATO: Azure Bastion e’ operativo nell’hub. Le VM degli spoke sono raggiungibili in RDP/SSH dal portale senza IP pubblici ne’ porte di gestione esposte a Internet.
Conclusioni
Abbiamo deployato Azure Bastion Standard nell’hub, con IP pubblico gestito automaticamente dal modulo e distribuzione su tre zone di disponibilita’. Centralizzarlo nell’hub e’ la scelta che massimizza il risparmio, servendo tutti gli spoke con un’unica istanza.
NOTA: Come evoluzione, valutare l’abilitazione delle funzionalita’ Standard aggiuntive come il native client support per connessioni via Azure CLI, e definire policy RBAC per limitare chi puo’ usare Bastion verso le VM sensibili.