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β
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:
- Receives HTTPS connections (port 443) from the administrator via browser
- Establishes the RDP or SSH session with the target VM within the private network
- Converts the protocol: RDP/SSH is transmitted over WebSocket (HTTPS), transparent to the user
- 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/27or 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
/26minimum? 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:
| SKU | Concurrent Sessions | Features | Recommended Use |
|---|---|---|---|
| Developer | Limited (shared) | Basic SSH and RDP | Personal development/testing |
| Basic | 25 | SSH, RDP, copy/paste | Small environments |
| Standard | 50+ (scalable) | Basic + file upload/download, kerberos auth, IP-based connection, shareable links | Production |
| Premium | Highest | Standard + session recording, private-only Bastion | Compliance and auditing |
Developer SKU: Unlike the others, doesn't require the dedicated
AzureBastionSubnetsubnet. 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.
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β
5. Practical Operationβ
5.1 Prerequisites for deploying Bastionβ
Before creating Bastion, you need:
- An existing VNet where Bastion will be deployed
- The
AzureBastionSubnetsubnet created with at least/26 - A public IP (Standard SKU) for Bastion (automatically created by portal or manually)
- 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:
| Priority | Source | Dest Port | Protocol | Action | Purpose |
|---|---|---|---|---|---|
| 120 | Internet | 443 | TCP | Allow | Administrator HTTPS access |
| 130 | GatewayManager | 443 | TCP | Allow | Azure control plane management |
| 140 | AzureLoadBalancer | 443 | TCP | Allow | Load Balancer health probe |
| 150 | VirtualNetwork | 8080, 5701 | Any | Allow | Communication between Bastion instances |
Mandatory Outbound rules:
| Priority | Destination | Dest Port | Protocol | Action | Purpose |
|---|---|---|---|---|---|
| 100 | VirtualNetwork | 22, 3389 | Any | Allow | SSH/RDP connection to VMs |
| 110 | AzureCloud | 443 | TCP | Allow | Communication with Azure services (diagnostics) |
| 120 | Internet | 80 | TCP | Allow | Certificate CRL checks |
| 130 | VirtualNetwork | 8080, 5701 | Any | Allow | Communication between instances |
5.3 Connecting via Bastion in the Portalβ
- Navigate to the VM in the portal
- Click Connect > Bastion
- If Bastion doesn't exist yet, the portal offers quick creation with default settings
- Enter credentials (username/password or SSH key)
- Choose authentication type (password or private key)
- 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) orSSH 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:
| Role | Permission | Scope |
|---|---|---|
| Reader | View resources | VM, NIC, Bastion (all three) |
| Bastion Reader | Initiate Bastion session | Bastion 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β
| Situation | Best choice | Reason |
|---|---|---|
| VM with public IP and SSH open | Migrate to Bastion | SSH open to internet is critical risk |
| Manually managed jump box | Bastion | Eliminates overhead of managing intermediary VM |
| Many VMs in multiple VNets | Standard Bastion in hub + peering | One Bastion covers all spoke VNets |
| Developer access with local SSH client | Bastion Standard with tunneling | Uses native SSH client through tunnel |
| Environment with session auditing requirement | Bastion Premium | Session recording for compliance |
| Large team without corporate VPN | Bastion Standard + Conditional Access + MFA | Browser access with strong authentication |
| On-premises network with ExpressRoute | Bastion Premium private-only | No public IP, internal access only |
8.2 Which Bastion SKU to chooseβ
| Scenario | SKU | Reason |
|---|---|---|
| Personal development, one VM | Developer | No dedicated subnet cost |
| Small team, occasional access | Basic | Lower cost, sufficient features |
| Production with multiple VMs | Standard | Scalability, upload/download, tunneling |
| Regulated environment (HIPAA, PCI-DSS) | Premium | Session recording, private-only |
| Hub-and-spoke architecture | Standard | VNet 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β
| Error | Why it happens | How to avoid |
|---|---|---|
Subnet with name different from AzureBastionSubnet | Any other name fails | The name is mandatory and case-sensitive |
Subnet smaller than /26 | Bastion doesn't start if there aren't enough IPs | Create subnet with at least /26 |
| NSG on subnet blocking management traffic | GatewayManager and AzureLoadBalancer rules missing | Include all mandatory NSG rules |
| Standard public IP missing | Bastion requires Standard public IP, not Basic | Create public IP with Standard SKU explicitly |
| User without Reader permission on NIC | Bastion connection fails even with VM permission | Grant Reader on VM, NIC and Bastion |
| Access to VMs in peered VNet fails | Basic SKU doesn't support peering | Use Standard SKU for cross-VNet access |
| Tunneling doesn't work | --enable-tunneling not enabled | Create/update Bastion with tunneling enabled |
| Bastion slow with many sessions | Insufficient scale units | Increase scale units (Standard SKU) |
| UDR on AzureBastionSubnet | Custom routing breaks Bastion communication | Never 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β
| Resource | Limit |
|---|---|
| Concurrent sessions (Basic) | 25 |
| Concurrent sessions per scale unit (Standard) | ~20 per unit |
| Maximum scale units (Standard) | 50 |
| Bastions per VNet | 1 |
| VNets accessible via peering (Standard) | No documented limit |
| Supported regions | Most 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:
- Administrator requests JIT access
- JIT temporarily opens port 22/3389 only for Bastion's IP
- Administrator connects via Bastion
- 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
AzureBastionSubnetwith 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
/26subnet, 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.