Skip to main content

Theoretical Foundation: Implement Azure Bastion


1. Initial Intuition​

Imagine you need to remotely access a server located in a locked room inside a corporate building. There are two approaches: give a copy of the access key to each employee who needs to enter (risky, difficult to control) or create a controlled reception room with a security guard, where all visitors pass through before being escorted to the server room. The security guard checks identity, logs the visit, and doesn't allow direct access.

Azure Bastion is that controlled reception room for VMs in Azure. Instead of opening SSH port (22) or RDP (3389) directly from the internet to VMs, you access all VMs through Bastion, which functions as a secure access point managed by Microsoft, accessible through a web browser without the need for installed RDP/SSH clients.

The most important practical consequence is that your VMs don't need a public IP and don't need SSH/RDP open to the internet. Bastion securely bridges the connection.


2. Context​

2.1 The problem Bastion solves​

Before Bastion, to access a VM remotely, organizations would do one or more of these things:

  • Assign a public IP to the VM and open port 22/3389 in the NSG (highly insecure)
  • Deploy a "jump box" or "bastion host" manually: a VM with a public IP used as an intermediary, which needed to be managed, patched, and monitored
  • Use VPN to connect the entire corporate network to Azure VNet

Azure Bastion eliminates the first two options and complements the third, offering a service managed by Microsoft with high availability, without the need to manage the intermediary server.

2.2 Position in the security ecosystem​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

3. Building the Concepts​

3.1 How Bastion works technically​

Azure Bastion operates as a PaaS (Platform as a Service) deployed within your VNet. It:

  1. Receives HTTPS connections (port 443) from the administrator via browser
  2. Establishes the RDP or SSH session with the target VM within the private network
  3. Converts the protocol: RDP/SSH is transmitted over WebSocket (HTTPS), transparent to the user
  4. Renders the desktop or terminal session directly in the browser

The result is that no RDP/SSH traffic travels over the internet. The only external traffic is HTTPS to Bastion. Bastion then makes the RDP/SSH connection internally within the VNet.


3.2 The AzureBastionSubnet: the most critical requirement​

Bastion must be deployed in a subnet with the mandatory name AzureBastionSubnet. No other name can be used. Additionally:

  • The minimum subnet size is /26 (64 IPs), except in Developer SKU which accepts /27 or smaller
  • The subnet cannot have an associated NSG (only Standard SKU and above allow NSG with specific rules)
  • The subnet cannot have UDR (User Defined Routes) that redirect traffic
  • The subnet cannot have other resources besides Bastion itself

Why /26 minimum? Bastion uses multiple internal instances for high availability and scalability. Each instance needs IPs. With /26, you have 59 usable IPs (64 minus 5 reserved by Azure), sufficient for scaling.


3.3 Azure Bastion SKUs​

Bastion has four SKUs with increasing capabilities:

SKUConcurrent SessionsFeaturesRecommended Use
DeveloperLimited (shared)Basic SSH and RDPPersonal development/testing
Basic25SSH, RDP, copy/pasteSmall environments
Standard50+ (scalable)Basic + file upload/download, kerberos auth, IP-based connection, shareable linksProduction
PremiumHighestStandard + session recording, private-only BastionCompliance and auditing

Developer SKU: Unlike the others, doesn't require the dedicated AzureBastionSubnet subnet. It's deployed more simply but with severe capacity limitations. Suitable only for individual use in development.


3.4 Connectivity with VMs in peered VNets​

One of the most valuable features of Standard SKU is support for VNet peering: a single Bastion deployed in a hub VNet can access VMs in peered spoke VNets.

This eliminates the need to deploy Bastion in each VNet, reducing operational cost in hub-and-spoke architectures.

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

3.5 Standard and Premium SKU features​

File upload and download: Allows transferring files between the administrator's local machine and the VM through the Bastion session, without needing intermediate storage.

IP-based connection: Allows connecting to a VM by private IP address instead of Azure Resource ID. Useful for non-Azure VMs in the same VNet or automation scenarios.

Shareable links: Generates a connection link that can be shared with other users for VM access via Bastion, with configurable validity.

Kerberos authentication: Allows integrated authentication with Active Directory when connecting via RDP to domain-joined Windows VMs.

Session recording (Premium): Records RDP/SSH sessions for auditing and compliance, storing videos in Storage Account.

Private-only Bastion (Premium): Disables Bastion's public IP; access is only via private network (ExpressRoute/VPN).


4. Structural View​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

5. Practical Operation​

5.1 Prerequisites for deploying Bastion​

Before creating Bastion, you need:

  1. An existing VNet where Bastion will be deployed
  2. The AzureBastionSubnet subnet created with at least /26
  3. A public IP (Standard SKU) for Bastion (automatically created by portal or manually)
  4. The user initiating connections needs Reader permission on the VM, NIC, and Bastion

5.2 The AzureBastionSubnet NSG​

While Basic Bastion doesn't allow NSG on the subnet, Standard SKU allows and recommends NSG with the following mandatory rules:

Mandatory Inbound rules:

PrioritySourceDest PortProtocolActionPurpose
120Internet443TCPAllowAdministrator HTTPS access
130GatewayManager443TCPAllowAzure control plane management
140AzureLoadBalancer443TCPAllowLoad Balancer health probe
150VirtualNetwork8080, 5701AnyAllowCommunication between Bastion instances

Mandatory Outbound rules:

PriorityDestinationDest PortProtocolActionPurpose
100VirtualNetwork22, 3389AnyAllowSSH/RDP connection to VMs
110AzureCloud443TCPAllowCommunication with Azure services (diagnostics)
120Internet80TCPAllowCertificate CRL checks
130VirtualNetwork8080, 5701AnyAllowCommunication between instances

5.3 Connecting via Bastion in the Portal​

  1. Navigate to the VM in the portal
  2. Click Connect > Bastion
  3. If Bastion doesn't exist yet, the portal offers quick creation with default settings
  4. Enter credentials (username/password or SSH key)
  5. Choose authentication type (password or private key)
  6. Click Connect

A new browser tab opens with the RDP or SSH session directly in the browser.


5.4 Authentication with SSH key​

For Linux VMs using key authentication:

In the Bastion connection form:

  • Authentication Type: SSH Private Key from Azure Key Vault (recommended) or SSH Private Key from Local File
  • If using Key Vault: specify the Key Vault and secret containing the private key

Best practice: Store SSH keys in Azure Key Vault and configure Bastion to retrieve the key from there. This eliminates the need to store private keys locally or transfer them through the browser.


6. Implementation Methods​

6.1 Azure Portal​

When to use: Initial deployment, environments with few VNets, when visual creation is preferred.

Quick Create:

VM > Connect > Bastion > Create Azure Bastion using defaults

The portal automatically creates the AzureBastionSubnet (if it doesn't exist, with /26), the public IP, and Bastion with Basic SKU. It's the fastest but least controlled method.

Manual creation:

Bastion > + Create

Allows choosing VNet, subnet, public IP, SKU, and advanced settings.


6.2 Azure CLI​

Creating the AzureBastionSubnet subnet:

az network vnet subnet create \
--vnet-name myVNet \
--resource-group myRG \
--name AzureBastionSubnet \
--address-prefix 10.0.10.0/26

Creating public IP for Bastion:

az network public-ip create \
--resource-group myRG \
--name BastionPublicIP \
--sku Standard \
--allocation-method Static \
--location eastus

Creating Bastion (Basic SKU):

az network bastion create \
--name myBastion \
--resource-group myRG \
--vnet-name myVNet \
--public-ip-address BastionPublicIP \
--location eastus \
--sku Basic

Creating Bastion (Standard SKU with extra features):

az network bastion create \
--name myBastion \
--resource-group myRG \
--vnet-name myVNet \
--public-ip-address BastionPublicIP \
--location eastus \
--sku Standard \
--enable-tunneling true \
--enable-ip-connect true \
--enable-shareable-link true \
--scale-units 5

Connecting via Bastion through CLI (tunnel):

With Standard SKU and --enable-tunneling true, you can create a local tunnel and use your native SSH/RDP client:

# Create SSH tunnel to VM
az network bastion tunnel \
--name myBastion \
--resource-group myRG \
--target-resource-id <vm-resource-id> \
--resource-port 22 \
--port 50022

# In another terminal, connect via local SSH using the tunnel
ssh -p 50022 adminuser@localhost

This allows using the local SSH client (with all its features, like SSH agent forwarding) through Bastion.

Connecting via SSH directly through CLI:

az network bastion ssh \
--name myBastion \
--resource-group myRG \
--target-resource-id <vm-resource-id> \
--auth-type password \
--username adminuser

6.3 Azure PowerShell​

# Create Bastion
$vnet = Get-AzVirtualNetwork `
-Name "myVNet" `
-ResourceGroupName "myRG"

$publicIp = New-AzPublicIpAddress `
-ResourceGroupName "myRG" `
-Name "BastionPublicIP" `
-Location "eastus" `
-AllocationMethod Static `
-Sku Standard

New-AzBastion `
-ResourceGroupName "myRG" `
-Name "myBastion" `
-PublicIpAddress $publicIp `
-VirtualNetwork $vnet `
-Sku "Standard" `
-ScaleUnit 5

6.4 Bicep​

// Dedicated subnet for Bastion
resource bastionSubnet 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = {
parent: vnet
name: 'AzureBastionSubnet'
properties: {
addressPrefix: '10.0.10.0/26'
}
}

// Standard Public IP for Bastion
resource bastionPublicIp 'Microsoft.Network/publicIPAddresses@2023-05-01' = {
name: 'BastionPublicIP'
location: location
sku: {
name: 'Standard'
}
properties: {
publicIPAllocationMethod: 'Static'
}
}

// Azure Bastion Standard SKU
resource bastion 'Microsoft.Network/bastionHosts@2023-05-01' = {
name: 'myBastion'
location: location
sku: {
name: 'Standard'
}
properties: {
enableTunneling: true
enableIpConnect: true
enableShareableLink: true
scaleUnits: 5
ipConfigurations: [
{
name: 'IpConf'
properties: {
subnet: {
id: bastionSubnet.id
}
publicIPAddress: {
id: bastionPublicIp.id
}
}
}
]
}
}

7. Control and Security​

7.1 Bastion access control​

Access to Bastion is controlled by RBAC at each resource level:

RolePermissionScope
ReaderView resourcesVM, NIC, Bastion (all three)
Bastion ReaderInitiate Bastion sessionBastion resource

The user needs at least Reader on all three resources: the target VM, the VM's NIC, and the Bastion resource. Without permission on Bastion, the connection option doesn't appear.

For granular security, use Azure AD Conditional Access to require MFA when accessing the Azure portal before initiating Bastion sessions.


7.2 Session logs and auditing​

All sessions initiated via Bastion generate logs in Azure Monitor / Activity Log:

# View Bastion sessions in Activity Log
az monitor activity-log list \
--resource-group myRG \
--query "[?contains(operationName.value, 'bastionHosts')].{Time:eventTimestamp, Operation:operationName.value, User:claims.upn}" \
--output table

For more detailed auditing (who connected, to which VM, for how long), enable Bastion diagnostics:

az monitor diagnostic-settings create \
--name "bastion-audit" \
--resource <bastion-resource-id> \
--logs '[
{"category": "BastionAuditLogs", "enabled": true}
]' \
--workspace <log-analytics-workspace-id>

Audit logs include: user, target VM, session duration, connection type (SSH/RDP), and source IP address.


7.3 Session Recording (Premium SKU)​

With Premium SKU, RDP sessions can be automatically recorded to video and stored in an Azure Storage Account:

az network bastion create \
--name myBastion \
--resource-group myRG \
--vnet-name myVNet \
--public-ip-address BastionPublicIP \
--sku Premium \
--enable-session-recording true \
--storage-account <storage-account-id>

Session recording is especially valuable in financial and healthcare environments where auditing administrative actions is a regulatory requirement.


7.4 Private-only Bastion (Premium SKU)​

In maximum security environments, Bastion can be configured without a public IP. Access is only via private network (VPN or ExpressRoute):

az network bastion create \
--name myBastion \
--resource-group myRG \
--vnet-name myVNet \
--sku Premium \
--disable-copy-paste false \
--enable-private-only true

In this mode, administrators need to be on the corporate network (via VPN or ExpressRoute) to access Bastion, adding another layer of security.


8. Decision Making​

8.1 Bastion vs remote access alternatives​

SituationBest choiceReason
VM with public IP and SSH openMigrate to BastionSSH open to internet is critical risk
Manually managed jump boxBastionEliminates overhead of managing intermediary VM
Many VMs in multiple VNetsStandard Bastion in hub + peeringOne Bastion covers all spoke VNets
Developer access with local SSH clientBastion Standard with tunnelingUses native SSH client through tunnel
Environment with session auditing requirementBastion PremiumSession recording for compliance
Large team without corporate VPNBastion Standard + Conditional Access + MFABrowser access with strong authentication
On-premises network with ExpressRouteBastion Premium private-onlyNo public IP, internal access only

8.2 Which Bastion SKU to choose​

ScenarioSKUReason
Personal development, one VMDeveloperNo dedicated subnet cost
Small team, occasional accessBasicLower cost, sufficient features
Production with multiple VMsStandardScalability, upload/download, tunneling
Regulated environment (HIPAA, PCI-DSS)PremiumSession recording, private-only
Hub-and-spoke architectureStandardVNet peering support

8.3 Cost vs benefit​

Bastion has hourly execution cost plus per-session cost (outbound data). For environments where VMs are accessed rarely, it may be more economical to create and destroy Bastion on-demand via IaC:

# Create Bastion when needed
az deployment group create --template-file bastion.bicep ...

# Destroy Bastion when no longer needed
az network bastion delete --name myBastion --resource-group myRG

The AzureBastionSubnet subnet can remain even without Bastion, at no additional cost.


9. Best Practices​

  • Remove public IPs from VMs when deploying Bastion. The purpose of Bastion is to eliminate direct VM exposure.
  • Close SSH (22) and RDP (3389) ports in NSGs of VM subnets. With Bastion, the only SSH/RDP traffic comes from within the VNet.
  • Use Standard SKU in production for VNet peering and tunneling support.
  • Centralize Bastion in the hub in hub-and-spoke architectures to avoid costs with multiple Bastions.
  • Require MFA via Azure AD Conditional Access before accessing the portal and starting Bastion sessions.
  • Enable audit diagnostics on all production Bastions and send logs to Log Analytics.
  • Use Azure Key Vault to store SSH keys used by Bastion.
  • Consider destruction on-demand for Bastions in development/test environments where access is occasional.
  • Size scale units appropriately: each scale unit supports ~20 concurrent RDP sessions.
  • Don't install additional software in AzureBastionSubnet nor place other resources in it.

10. Common Errors​

ErrorWhy it happensHow to avoid
Subnet with name different from AzureBastionSubnetAny other name failsThe name is mandatory and case-sensitive
Subnet smaller than /26Bastion doesn't start if there aren't enough IPsCreate subnet with at least /26
NSG on subnet blocking management trafficGatewayManager and AzureLoadBalancer rules missingInclude all mandatory NSG rules
Standard public IP missingBastion requires Standard public IP, not BasicCreate public IP with Standard SKU explicitly
User without Reader permission on NICBastion connection fails even with VM permissionGrant Reader on VM, NIC and Bastion
Access to VMs in peered VNet failsBasic SKU doesn't support peeringUse Standard SKU for cross-VNet access
Tunneling doesn't work--enable-tunneling not enabledCreate/update Bastion with tunneling enabled
Bastion slow with many sessionsInsufficient scale unitsIncrease scale units (Standard SKU)
UDR on AzureBastionSubnetCustom routing breaks Bastion communicationNever add UDR to AzureBastionSubnet

11. Operation and Maintenance​

11.1 Monitoring active sessions​

# View active Bastion sessions
az network bastion list-sessions \
--name myBastion \
--resource-group myRG \
--output table

11.2 Disconnecting active sessions​

# Disconnect a specific session
az network bastion delete-session \
--name myBastion \
--resource-group myRG \
--session-ids <session-id>

Useful when a session gets stuck or when a user needs to be disconnected immediately for security reasons.


11.3 Updating Bastion SKU​

# Update from Basic to Standard
az network bastion update \
--name myBastion \
--resource-group myRG \
--sku Standard \
--enable-tunneling true

SKU upgrade is possible from Basic to Standard or Standard to Premium. Downgrade is not possible (from Standard to Basic).


11.4 Important limits​

ResourceLimit
Concurrent sessions (Basic)25
Concurrent sessions per scale unit (Standard)~20 per unit
Maximum scale units (Standard)50
Bastions per VNet1
VNets accessible via peering (Standard)No documented limit
Supported regionsMost Azure regions

12. Integration and Automation​

12.1 Bastion with Just-in-Time (JIT) VM Access​

Microsoft Defender for Cloud offers JIT VM Access: SSH/RDP ports are opened only when needed, for a limited time, after approval. Combined with Bastion:

  1. Administrator requests JIT access
  2. JIT temporarily opens port 22/3389 only for Bastion's IP
  3. Administrator connects via Bastion
  4. After timeout, the port closes automatically

This combination is the highest security standard for administrative access.

# Enable JIT for a VM
az security jit-policy create \
--resource-group myRG \
--vm-ids <vm-resource-id> \
--name "jit-policy-vm01" \
--ports "[{\"number\": 22, \"protocol\": \"TCP\", \"allowedSourceAddressPrefix\": \"VirtualNetwork\", \"maxRequestAccessDuration\": \"PT3H\"}]"

12.2 Terraform for Bastion​

resource "azurerm_subnet" "bastion" {
name = "AzureBastionSubnet"
resource_group_name = var.resource_group
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.10.0/26"]
}

resource "azurerm_public_ip" "bastion" {
name = "BastionPublicIP"
location = var.location
resource_group_name = var.resource_group
allocation_method = "Static"
sku = "Standard"
}

resource "azurerm_bastion_host" "main" {
name = "myBastion"
location = var.location
resource_group_name = var.resource_group
sku = "Standard"
tunneling_enabled = true
ip_connect_enabled = true

ip_configuration {
name = "IpConf"
subnet_id = azurerm_subnet.bastion.id
public_ip_address_id = azurerm_public_ip.bastion.id
}
}

12.3 Azure Policy to ensure Bastion usage​

# Policy: VMs should not have public IPs (forces Bastion usage)
az policy assignment create \
--name "deny-public-ip-on-vms" \
--policy "83a86a26-fd1f-447c-b59d-daf3f72c3ae1" \
--scope "/subscriptions/<sub-id>/resourceGroups/production-rg"

This policy denies creation of NICs with public IPs, forcing the use of Bastion or VPN for administrative access.


13. Final Summary​

Essential concepts:

  • Azure Bastion is a managed PaaS service that provides secure RDP/SSH connectivity to VMs via web browser, without the need for public IPs on VMs and without opening SSH/RDP ports to the internet.
  • Bastion is deployed in a subnet that must be called AzureBastionSubnet with a minimum size of /26.
  • User communication with Bastion is via HTTPS (443); Bastion connects to VMs via SSH/RDP internally in the private network.

Critical differences between SKUs:

  • Developer: No dedicated subnet, personal use, not for production.
  • Basic: Mandatory /26 subnet, 25 sessions, no peering, upload/download or tunneling support.
  • Standard: VNet peering support, tunneling, upload/download, IP-based connection, shareable links, scalable.
  • Premium: Everything from Standard plus session recording and private-only (no public IP).

What needs to be remembered:

  • The subnet MUST be called exactly AzureBastionSubnet (case-sensitive).
  • Minimum subnet size: /26 (except Developer SKU).
  • Bastion's public IP MUST be Standard SKU, not Basic.
  • Never add UDR to AzureBastionSubnet.
  • User needs Reader on VM, NIC and Bastion resource to start sessions.
  • Standard SKU is required to access VMs in peered VNets.
  • SKU downgrade is not possible (Standard β†’ Basic); only upgrade is allowed.
  • Close ports 22/3389 in production VM NSGs and remove public IPs when using Bastion.