Back to Checks
azure

Azure subscription has at least one Bastion Host

network_bastion_host_exists
Service: networkSeverity: medium
OfficialBuilt-inVerified
Categories:internet-exposed
Resource Type:microsoft.network/bastionhosts

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.

Prowler logoProwlerprowler-provider-azure

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 Cloud
Run
prowler azure --checks network_bastion_host_exists

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.

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

1In the Azure portal, go to Networking > Bastions > Create
2Select your Subscription and a Resource group
3Enter a Name and Region
4Under Virtual network, select an existing VNet or click Create new
5Ensure a subnet named AzureBastionSubnet exists with a /27 address space; create it if prompted
6For Public IP address, click Create new and accept defaults
7Click Review + create, then Create
8After deployment completes, the subscription now has a Bastion Host (check passes)

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 findings