Networking
Azure Bastion: diagnose SSH or RDP failures before opening NSGs
A production runbook to separate Bastion sessions, NSGs, routing, target ports, identity and VM health before exposing SSH or RDP.
An Azure Bastion session stays on Connecting, closes after authentication, or fails only for a subset of VMs. The machine is running and no application alert explains the incident. Under pressure, the quickest response appears to be allowing 22 or 3389 from the Internet, adding a broad NSG rule, or redeploying Bastion.
That workaround turns an administration incident into a new exposure without proving the cause. This runbook covers a shared Bastion host providing private-address access to Windows and Linux VMs in a hub-and-spoke network or a single VNet. The objective is to locate the break between the client, Bastion service, AzureBastionSubnet, the path to the VM, the guest OS and identity, then fix the failed layer with an explicit rollback.
Freeze the failing attempt
Preserve one reproducible attempt before changing anything. Record UTC time, user, connection mode, VM, private IP, requested port and exact message. A web session that never opens, a TCP connection that times out, and rejected authentication are different incidents.
Incident: inc-20260815-021
Bastion: bas-hub-prod-weu
Target VM: vm-orders-07
Target private IP: 10.42.6.17
Protocol / port: SSH / 22
Client mode: Azure portal
Attempt time: 2026-08-15T06:35:00Z
Observed result: Connecting then generic failure
Preserve
Correlation and UTC time for every attempt
Bastion SKU and provisioning state
AzureBastionSubnet, associated NSG and route table
VM NIC, subnet, effective NSGs and routes
SSH/RDP service and guest firewall state
Temporary constraints
Do not publish 22 or 3389 to the Internet
Do not add Allow Any Any
Do not detach NSGs or UDRs for testing
Do not restart the VM before evidence collection Replay once with the same parameters. If multiple users or VMs are affected, build a small matrix. “Every VM in one spoke” points to peering or routing. “One VM only” points to its NIC, NSG or operating system. “Every user from one corporate network” can indicate that the client HTTPS path to Bastion is blocked.
Split the path into five checks
Azure Bastion does not remove the administration network path; it makes that path private and mediated. A session crosses five planes that should be tested independently:
- the client reaches the Bastion experience over HTTPS;
- Bastion is provisioned and its subnet retains the service flows it requires;
AzureBastionSubnetreaches the VM private IP on SSH, RDP or a custom port;- the VM listens and its guest firewall accepts the flow;
- the user and authentication method are valid.
A Connecting screen does not select one of these planes. The runbook must produce evidence for each plane before a rule is changed.
Inspect Bastion and its subnet without mutation
Start with the resource and network attachment. A provisioning state other than Succeeded, a renamed subnet, a detached public IP or a recent SKU change should be handled before the target VM.
BASTION_RG="rg-connectivity-prod"
BASTION_NAME="bas-hub-prod-weu"
VNET_RG="rg-connectivity-prod"
VNET_NAME="vnet-hub-prod-weu"
az network bastion show --resource-group "$BASTION_RG" --name "$BASTION_NAME" --query '{state:provisioningState,sku:sku.name,dnsName:dnsName,scaleUnits:scaleUnits,ipConfigurations:ipConfigurations[].{subnet:subnet.id,publicIp:publicIPAddress.id}}' --output json
az network vnet subnet show --resource-group "$VNET_RG" --vnet-name "$VNET_NAME" --name AzureBastionSubnet --query '{prefix:addressPrefix,nsg:networkSecurityGroup.id,routeTable:routeTable.id}' --output json If an NSG protects AzureBastionSubnet, verify the complete expected rule set, not only outbound 22 or 3389. Bastion also depends on HTTPS control flows and internal service communication. A higher-priority deny can shadow an allow rule that looks correct in isolation.
NSG_RG="rg-connectivity-prod"
NSG_NAME="nsg-azure-bastion-prod"
az network nsg rule list --resource-group "$NSG_RG" --nsg-name "$NSG_NAME" --include-default --query "sort_by([].{priority:priority,name:name,direction:direction,access:access,source:sourceAddressPrefix,destination:destinationAddressPrefix,port:destinationPortRange,ports:destinationPortRanges}, &priority)" --output table Compare this inventory with the contract of the Bastion deployment in use. For a classic Bastion protected by an NSG, the usual flows include HTTPS 443, internal communication on 8080/5701, outbound access to VMs on 22/3389, and Azure dependencies. Do not copy an old rule set without checking the SKU, enabled features and current service documentation at change time.
Prove the Bastion-to-VM flow
Retrieve the primary NIC, subnet and attached protections. A subnet rule can allow traffic while a NIC rule denies it, or the reverse. Effective rules are stronger evidence than reading one NSG in isolation.
VM_RG="rg-workload-prod"
VM_NAME="vm-orders-07"
NIC_ID=$(az vm show --resource-group "$VM_RG" --name "$VM_NAME" --query 'networkProfile.networkInterfaces[0].id' --output tsv)
az network nic show --ids "$NIC_ID" --query '{privateIps:ipConfigurations[].privateIPAddress,subnets:ipConfigurations[].subnet.id,nsg:networkSecurityGroup.id}' --output json
az network nic list-effective-nsg --ids "$NIC_ID" --output json
az network nic show-effective-route-table --ids "$NIC_ID" --output table The target flow should allow the AzureBastionSubnet CIDR to the actual listening port. Avoid using VirtualNetwork as an automatic answer in a complex topology: that service tag can cover more prefixes than the administration subnet alone. A rule scoped to the Bastion CIDR gives operators a clearer contract.
Then inspect the path: peering in both directions, forwarded traffic when the architecture requires it, UDRs on the relevant subnets, and the return route to the Bastion prefix. A default route injected by an appliance or Azure Route Server can divert a path that previously worked. If the symptom followed a routing change, compare before and after configuration instead of adding an NSG exception.
Separate network, guest service and authentication
When rules and routes are sound, verify the listener inside the VM. Use Run Command or serial console only as a bounded recovery channel, starting with read-only commands. SSH from another VM does not reproduce the Bastion source, but it does confirm whether the service is listening.
sudo systemctl status sshd --no-pager || sudo systemctl status ssh --no-pager
sudo ss -lntp | awk '$4 ~ /:22$/'
sudo journalctl -u sshd --since "2026-08-15 06:20:00 UTC" --no-pager
sudo nft list ruleset
sudo iptables -S
df -h / /var
# Replace 22 in listener and rule checks when a custom port is used. On Windows, check TermService, the listener on the configured port, fDenyTSConnections, the firewall profile and authentication events. A full disk, stopped service or NLA policy can resemble a network timeout.
If TCP succeeds but authentication fails, stop changing the network. Check the identity presented, local rights, Remote Desktop Users membership for RDP, expected SSH key, account state and the Entra method when used. A 401, NLA rejection or rejected key is not fixed by a broader NSG rule.
Correct the failed layer
The correction should match the collected evidence:
- missing Bastion rule: restore only the required rule and priority;
- target NSG too restrictive: allow the Bastion CIDR to the real port, never the Internet source;
- inconsistent UDR or peering: restore the validated symmetric path;
- guest firewall: allow the Bastion source on the listening port;
- stopped service: fix its cause, then start it;
- authentication: repair the account or method without a network change.
Apply one family of changes at a time. Tie every mutation to the incident identifier and preserve the previous rule, priority and owner. A temporary opening without an owner or expiry date quickly becomes permanent.
Validate and decide rollback
Replay the frozen attempt from the same client mode to the same private IP. Minimum validation proves session establishment, resulting identity, absence of public VM exposure and stability on a second connection. Test a control VM as well to detect a shared Bastion regression.
Validate the correction
Bastion remains in provisioningState Succeeded
Session opens to target VM over private IP
Allowed source is limited to AzureBastionSubnet
No Internet inbound rule exists on 22 or 3389
Guest service and authentication are healthy
Control VM remains reachable
Rollback
Remove the new rule or restore its previous priority
Restore the previous route table, peering or guest firewall state
Replay target and control tests
Keep public access closed
Escalate without widening access
Every check passes but Bastion still fails
Attach UTC times, correlation, inventory, effective routes and NSGs
Preserve the secured configuration during analysis Conclusion
An Azure Bastion failure is not permission to reopen SSH or RDP to the Internet. It requires locating the break between client, Bastion service, subnet, NSG, routing, guest port and identity. Each plane has distinct evidence; mixing them produces broad rules and fragile diagnosis.
The incident closes on a verifiable decision: restored private flow, a rule limited to AzureBastionSubnet, a session validated with the expected identity, and a documented rollback. If the cause remains unknown, keep public ports closed and escalate with evidence instead of turning a workaround into architecture.