Azure subscription has at least one Bastion Host
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.
Run this check with Prowler
Run in Prowler Cloudprowler azure --checks network_bastion_host_existsRecommendation
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.
Remediation
CLI
az network bastion create --name <BASTION_NAME> --public-ip-address <PUBLIC_IP_NAME> --resource-group <RESOURCE_GROUP> --vnet-name <VNET_NAME> --location <LOCATION>Terraform
# 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
}
}NativeIaC
// 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
}
}
]
}
}Other
Check Code
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.network.network_client import network_client
class network_bastion_host_exists(Check):
def execute(self) -> Check_Report_Azure:
findings = []
for subscription, bastion_hosts in network_client.bastion_hosts.items():
subscription_name = network_client.subscriptions.get(
subscription, subscription
)
if not bastion_hosts:
report = Check_Report_Azure(metadata=self.metadata(), resource={})
report.subscription = subscription
report.resource_name = subscription
report.resource_id = f"/subscriptions/{subscription}"
report.status = "FAIL"
report.status_extended = f"Bastion Host from subscription {subscription_name} ({subscription}) does not exist"
findings.append(report)
else:
for bastion_host in bastion_hosts:
report = Check_Report_Azure(
metadata=self.metadata(), resource=bastion_host
)
report.subscription = subscription
report.status = "PASS"
report.status_extended = f"Bastion Host {bastion_host.name} exists in subscription {subscription_name} ({subscription})."
findings.append(report)
return findingsReferences
- https://learn.microsoft.com/en-us/powershell/module/az.network/get-azbastion?view=azps-9.2.0
- https://learn.microsoft.com/en-us/azure/templates/microsoft.network/bastionhosts
- https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/Network/bastion-host-exists.html
- https://learn.microsoft.com/en-us/azure/bastion/bastion-overview#sku
- https://learn.microsoft.com/en-us/azure/firewall/deploy-ps