Networking

Azure Bastion: diagnose native client access before opening RDP or SSH

A production runbook for separating local client, SKU, tunneling, RBAC, NSG and guest-service failures when Azure Bastion native access breaks.

07 Oct 2026 azurebastionnetworkingnsgrbacsshrdpsecurityobservabilityrunbookrollbackproduction

An az network bastion ssh, rdp or tunnel connection to a private VM fails. Application probes still reach the VM and no broad Azure outage is visible. The quick workaround is to add a public IP or allow 22 and 3389 from the Internet.

That workaround changes the exposure model without proving the failure. The denial may occur on the operator workstation, in the Azure control plane, on AzureBastionSubnet, between Bastion and the VM, or inside the guest. The running case is administrative access needed during an incident. The goal is to restore a bounded private path, then decide whether to correct it, keep portal access as a temporary fallback, or roll back the latest network change.

Freeze one representative attempt

Preserve one attempt before editing rules. Record the UTC timestamp, Azure identity, subscription, Bastion name, VM ID, connection mode, target port and exact error. Do not put a private key or password in the incident record.

yaml bastion-native-client-incident.yml
incident:
detected_utc: 2026-10-07T15:20:00Z
operator: <entra-object-id>
subscription: <subscription-id>
bastion: bas-hub-prod
target: /subscriptions/<id>/resourceGroups/rg-app-prod/providers/Microsoft.Compute/virtualMachines/vm-api-01
mode: tunnel
target_port: 22
local_port: 22022
symptom: tunnel creation fails before SSH handshake

last_changes:
- AzureBastionSubnet NSG update
- target subnet deny rule
- Bastion SKU or configuration update
- operator role assignment change

containment:
public_ip_on_target: forbidden
internet_ssh_rdp_rule: forbidden
approved_fallback: portal_session_only

Where the error appears immediately narrows the search. If the command never creates a tunnel, check the client, Azure context, RBAC and Bastion configuration. If the local port opens but SSH or RDP fails, focus on the Bastion-to-VM path, the port actually listening and guest authentication.

Compare portal and native client behavior

Test the same account, Bastion resource, VM and port through the portal and the native client. Change one variable at a time.

text bastion-localization-matrix.txt
Portal works, native client fails
Check Azure CLI, Bastion extension, SKU, native client support and local HTTPS path

Portal fails, native client fails
Check Bastion state, AzureBastionSubnet NSG, target NSG, guest service and port

Tunnel opens, SSH/RDP handshake fails
Check target NSG, return route, sshd/TermService, custom port and authentication

One VM fails, other VMs work
Prioritize target subnet/NIC NSG, guest OS, identity and VM configuration

Every VM fails after one change
Prioritize AzureBastionSubnet, SKU/configuration, policy, route or shared dependency

This matrix prevents two wrong conclusions: a healthy Bastion does not prove a healthy guest service, and a failed native client does not prove the VM needs a network change.

Prove the Bastion SKU, tunneling and state

Native client connections require Standard SKU or higher and the corresponding support to be enabled. Read the deployed state rather than the intended template.

bash 01-read-bastion-configuration.sh
RG="rg-connectivity-prod"
BASTION="bas-hub-prod"

az network bastion show \
--resource-group "$RG" \
--name "$BASTION" \
--query '{id:id,sku:sku.name,provisioningState:provisioningState,enableTunneling:enableTunneling,subnet:ipConfigurations[0].subnet.id}' \
--output jsonc

az version --output json

Stop the change if provisioningState is not Succeeded. An update still running or already failed must be qualified first. If portal access works but enableTunneling is false, correct that capability in a controlled change; do not widen the VM NSG.

Then verify the workstation context: tenant, selected subscription, Azure CLI version and Bastion extension. A session authenticated against the wrong tenant can look like a network availability problem.

Verify rights at the effective scope

The client needs read access to the Bastion resource, VM and its network interface. Microsoft Entra sign-in to the guest adds the appropriate login role, but does not replace the read permissions needed to assemble the path.

bash 02-check-context-and-rbac.sh
OPERATOR_OBJECT_ID="<entra-object-id>"
BASTION_ID=$(az network bastion show -g "$RG" -n "$BASTION" --query id -o tsv)
VM_ID="<vm-resource-id>"

az account show --query '{tenant:tenantId,subscription:id,user:user.name}' -o jsonc

az role assignment list \
--assignee "$OPERATOR_OBJECT_ID" \
--all \
--query "[?scope=='$BASTION_ID' || scope=='$VM_ID'].{role:roleDefinitionName,scope:scope}" \
--output table

Complete the inventory with inherited rights and the NIC scope. If an assignment just changed, record the time and retest with a fresh session after propagation. Do not grant Owner and turn an RBAC investigation into permanent excess access.

Read the two NSG planes separately

An NSG on AzureBastionSubnet must retain the service’s complete required flow set: HTTPS and management ingress, internal communication on 8080/5701, egress to VMs on 22/3389 or an approved custom port, and the documented Azure dependencies. One missing rule can break sessions even while the resource remains visible in the portal.

The target subnet or NIC NSG answers a different question: does it allow the administration port from the Bastion subnet? Do not open the source to Internet, and do not replace a precise rule with VirtualNetwork without understanding what that service tag includes in the actual topology.

bash 03-inventory-nsg-and-routes.sh
BASTION_NSG="nsg-azure-bastion-prod"
TARGET_NIC="nic-vm-api-01"
TARGET_RG="rg-app-prod"

az network nsg rule list \
--resource-group "$RG" \
--nsg-name "$BASTION_NSG" \
--include-default \
--query "sort_by([].{priority:priority,name:name,direction:direction,access:access,source:sourceAddressPrefix,destination:destinationAddressPrefix,ports:destinationPortRanges}, &priority)" \
--output table

az network nic list-effective-nsg \
--resource-group "$TARGET_RG" \
--name "$TARGET_NIC" \
--output json

az network nic show-effective-route-table \
--resource-group "$TARGET_RG" \
--name "$TARGET_NIC" \
--output table

The NIC’s effective rules combine subnet and NIC controls. Find the rule that actually wins for the Bastion source, private destination and exact port. Read routes as well when the target subnet forces a next hop: the session may reach the VM and lose its return path through an appliance that has no matching flow state.

Confirm the guest service without bypassing Bastion

When the local tunnel opens, test the target protocol through that tunnel. A failure here does not justify a public IP: prove that the service listens and that the guest firewall accepts it.

bash 04-open-tunnel-and-test-ssh.sh
az network bastion tunnel \
--resource-group "$RG" \
--name "$BASTION" \
--target-resource-id "$VM_ID" \
--resource-port 22 \
--port 22022

# In a second terminal, without exposing the VM publicly.
ssh -vv -p 22022 <user>@127.0.0.1

On Linux, inspect sshd, the listening port and the local firewall through the serial console or approved automation. On Windows, inspect TermService, the RDP listener and Windows Firewall. Finally separate transport from authentication: a handshake reached the OS; a rejected key, missing login right or Entra policy is handled after the network path is clean.

Canary the smallest correction

Correct one layer and replay a minimal matrix: one Linux or Windows target, one expected port, one authorized identity and one negative control. The negative control can be another VM or a port that must remain denied.

text bastion-validation-gate.txt
Resume native access
Bastion is Succeeded with the expected SKU and tunneling state
AzureBastionSubnet retains every required rule
The target rule allows only the required source and port
The tunnel opens and the protocol reaches the guest service
An unauthorized identity or target remains denied
No public IP or Internet 22/3389 rule was added

Keep the portal fallback
The local client or HTTPS path is not yet qualified
Effective rights remain unproven
The shared network change would have too wide a blast radius

Rollback
Restore the previous NSG, route or Bastion configuration version
Close the canary change if the negative control becomes reachable
Revoke every temporary role and retain before/after evidence

Conclusion

An Azure Bastion native client failure should not turn a private VM into a public endpoint. A useful diagnosis locates the denial across the operator workstation, control plane, Bastion configuration, AzureBastionSubnet, target network path and guest OS.

The decision is then reversible: correct tunneling, RBAC scope, one NSG rule, a route or the guest service; keep portal access temporarily when the native path remains ambiguous; or roll back the latest shared change. Recovery is complete only after a positive canary, an expected denial and confirmation that no direct Internet access was introduced.