Check ID: network_bastion_host_exists
Provider: Azure
Service: network
Severity: MEDIUM
Categories: internet-exposed
Resource type: microsoft.network/bastionhosts
What this check verifies
Azure subscription contains an Azure Bastion host for secure RDP/SSH brokering over TLS on 443/TCP to virtual machines using private IPs. The assessment identifies whether such a bastion is available.
Risk
Absent Bastion, admins often assign public IPs or open 22/3389, expanding attack surface.
This enables Internet brute force, credential stuffing, and RDP/SSH exploits, leading to unauthorized access, data exfiltration, and lateral movement. CIA impact: confidentiality/integrity loss and potential downtime from ransomware.
Recommendation
Standardize on Azure Bastion for admin access.
Remove VM public IPs and deny inbound 22/3389 via perimeter controls and NSGs. Apply least privilege and just-in-time access, integrate Entra ID with MFA and conditional access, monitor sessions/logs, and segment networks so only Bastion can reach management ports.
CLI
Terraform
Native IaC
Dashboard steps
az network bastion create —name <BASTION_NAME> —public-ip-address <PUBLIC_IP_NAME> —resource-group <RESOURCE_GROUP> —vnet-name <VNET_NAME> —location <LOCATION>
# Minimal Terraform to create one Bastion Host (fixes FAIL by ensuring existence)
resource "azurerm_resource_group" "example" {
name = "<example_resource_name>"
location = "eastus"
}
resource "azurerm_virtual_network" "example" {
name = "<example_resource_name>-vnet"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
address_space = ["10.0.0.0/24"]
}
resource "azurerm_subnet" "bastion" {
name = "AzureBastionSubnet"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.0.0/27"]
}
resource "azurerm_public_ip" "example" {
name = "<example_resource_name>-pip"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
allocation_method = "Static"
sku = "Standard"
}
resource "azurerm_bastion_host" "example" {
name = "<example_resource_name>"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
# Critical: creating the Bastion Host resource is what changes the check to PASS
sku = "Basic" # Critical: required for Bastion creation
ip_configuration {
name = "IpConf"
subnet_id = azurerm_subnet.bastion.id # Critical: attaches Bastion to AzureBastionSubnet
public_ip_address_id = azurerm_public_ip.example.id # Critical: associates required Public IP
}
}
// Minimal Bicep to ensure at least one Bastion Host exists in the subscription
param location string = resourceGroup().location
resource vnet 'Microsoft.Network/virtualNetworks@2022-07-01' = {
name: '<example_resource_name>-vnet'
location: location
properties: {
addressSpace: { addressPrefixes: ['10.0.0.0/24'] }
subnets: [
{
name: 'AzureBastionSubnet'
properties: { addressPrefix: '10.0.0.0/27' }
}
]
}
}
resource pip 'Microsoft.Network/publicIPAddresses@2022-07-01' = {
name: '<example_resource_name>-pip'
location: location
sku: { name: 'Standard' }
properties: { publicIPAllocationMethod: 'Static' }
}
resource bastion 'Microsoft.Network/bastionHosts@2024-10-01' = {
name: '<example_resource_name>'
location: location
sku: { name: 'Basic' }
properties: {
ipConfigurations: [
{
name: 'IpConf'
properties: {
subnet: { id: resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, 'AzureBastionSubnet') } // Critical: attaches Bastion to required AzureBastionSubnet so resource can be created
publicIPAddress: { id: pip.id } // Critical: associates required Public IP with Bastion
}
}
]
}
}
- In the Azure portal, go to Networking > Bastions > Create
- Select your Subscription and a Resource group
- Enter a Name and Region
- Under Virtual network, select an existing VNet or click Create new
- Ensure a subnet named AzureBastionSubnet exists with a /27 address space; create it if prompted
- For Public IP address, click Create new and accept defaults
- Click Review + create, then Create
- After deployment completes, the subscription now has a Bastion Host (check passes)
References
Where this check fires
This check runs on every scheduled scan against your Azure account. Findings appear at Cloud Security → Findings filterable by Check ID = network_bastion_host_exists. To re-evaluate after a fix, hit Run now on the account’s schedule under Cloud Security → Schedules — the next scan re-checks this control and marks the finding fixed or keeps it persistent.
← Back to Azure checks