Search Shodan for open 3389 and you will understand why brute forced RDP remains one of the top initial access vectors in every incident report. There is no legitimate reason for a VM in 2026 to expose RDP or SSH to the internet, and Azure gives you two complementary tools to end the practice: Bastion for the access path, and Just in Time to close the ports even internally until someone actually needs them.
Bastion Architecture
Bastion is a managed jump host living in a dedicated AzureBastionSubnet (a /26 or larger), reachable over HTTPS. Users connect through the portal in a browser, or, on Standard SKU and above, through the native client: az network bastion ssh and az network bastion rdp tunnel your local tools through Bastion, which restores everything admins miss in browser sessions, real terminals, file transfer, and multi monitor RDP. Because access flows through Azure, every session requires Entra authentication first, which means MFA and Conditional Access apply before any VM credential is even attempted.
resource "azurerm_subnet" "bastion" {
name = "AzureBastionSubnet"
resource_group_name = azurerm_resource_group.hub.name
virtual_network_name = azurerm_virtual_network.hub.name
address_prefixes = ["10.10.1.0/26"]
}
resource "azurerm_public_ip" "bastion" {
name = "pip-bastion-hub"
location = "westeurope"
resource_group_name = azurerm_resource_group.hub.name
allocation_method = "Static"
sku = "Standard"
}
resource "azurerm_bastion_host" "this" {
name = "bas-hub-weu"
location = "westeurope"
resource_group_name = azurerm_resource_group.hub.name
sku = "Standard"
scale_units = 4
tunneling_enabled = true
ip_connect_enabled = true
copy_paste_enabled = true
file_copy_enabled = true
ip_configuration {
name = "ipconf"
subnet_id = azurerm_subnet.bastion.id
public_ip_address_id = azurerm_public_ip.bastion.id
}
}
Deploy one Bastion in the hub with IP connect enabled and it reaches VMs across all peered spokes by private IP, so you do not pay for a Bastion per VNet. For Entra joined Windows VMs, pair this with Entra login so RDP uses your directory identity and local admin passwords can rot in Key Vault unused. On Linux, use AADSSHLoginForLinux and SSH with Entra identities the same way. The Premium SKU adds session recording if your compliance framework wants keystroke level audit of privileged sessions.
Just in Time: Closed Until Requested
Bastion secures the path; JIT, part of Defender for Servers, secures the window. With JIT enabled, the NSG holds a deny rule for the management ports at all times. An admin requests access for a defined duration, Defender validates their RBAC permission, opens a scoped allow rule for their source IP, and closes it automatically when the clock runs out. Every request is logged with who, what, when, and from where, which turns your access audit from log archaeology into a simple query.
resource "azurerm_security_center_jit_network_access_policy" "vms" {
name = "jit-app-vms"
location = "westeurope"
resource_group_name = azurerm_resource_group.app.name
virtual_machine {
virtual_machine_id = azurerm_linux_virtual_machine.app01.id
port {
number = 22
protocol = "TCP"
allowed_source_address_prefix = "10.10.1.0/26"
max_request_duration = "PT3H"
}
}
}
Note the allowed source prefix: the Bastion subnet. Combined, the only path to SSH is through Bastion, only during an approved window, only for an authenticated and authorized identity.
Closing the Estate
Finish the job with policy: deny NSG rules allowing 3389 or 22 from Internet at the landing zone scope, audit VMs with public IPs, and alert on any new public IP association in the activity log. Then run one query in Resource Graph for network interfaces with public IPs and management ports open, and work the list to zero. In most estates I have done this in, the list starts embarrassingly long and the pushback ends the first time someone sees the native client tunneling works exactly like their old direct SSH did.
Cheers
Osama
Leave a comment