Challenge 48: Network segmentation and Just-in-Time access
75-90 minutes | ~$0.30/hour (Bastion Standard + VMs) | Exam weight: 10-15%
Scenario
Your organization is implementing a zero-trust network design. All management access to VMs must be Just-in-Time (JIT) controlled through Microsoft Defender for Cloud, and administrative sessions must traverse Azure Bastion. Azure Virtual Network Manager (AVNM) enforces organization-wide security admin rules that cannot be overridden by local NSG administrators -- ensuring a deny-by-default posture across all network groups.
You must demonstrate that:
- Direct SSH/RDP access to VMs is denied by default
- AVNM security admin rules take precedence over local NSGs
- JIT access enables temporary, audited management access
- Bastion provides the only path for interactive VM administration
Architecture overview
Task 1: Deploy Azure Virtual Network Manager
Azure CLI
# Variables
RG="rg-avnm-lab"
LOCATION="eastus"
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
# Create resource group
az group create --name $RG --location $LOCATION
# Create Azure Virtual Network Manager
az network manager create \
--name "avnm-enterprise" \
--resource-group $RG \
--location $LOCATION \
--description "Enterprise network security manager" \
--scope-accesses "SecurityAdmin" \
--network-manager-scopes subscriptions=$SUBSCRIPTION_ID
# Create VNets for the lab
az network vnet create \
--resource-group $RG \
--name vnet-prod \
--address-prefixes 10.1.0.0/16 \
--subnet-name workload-subnet \
--subnet-prefixes 10.1.1.0/24 \
--location $LOCATION \
--tags Environment=Production
az network vnet create \
--resource-group $RG \
--name vnet-dev \
--address-prefixes 10.2.0.0/16 \
--subnet-name workload-subnet \
--subnet-prefixes 10.2.1.0/24 \
--location $LOCATION \
--tags Environment=Development
# Create AzureBastionSubnet in prod VNet
az network vnet subnet create \
--resource-group $RG \
--vnet-name vnet-prod \
--name AzureBastionSubnet \
--address-prefixes 10.1.254.0/26
Azure PowerShell
$rg = "rg-avnm-lab"
$location = "eastus"
$subscriptionId = (Get-AzContext).Subscription.Id
New-AzResourceGroup -Name $rg -Location $location
# Create AVNM
$scope = New-AzNetworkManagerScope -Subscription @($subscriptionId)
New-AzNetworkManager -Name "avnm-enterprise" -ResourceGroupName $rg `
-Location $location -Description "Enterprise network security manager" `
-NetworkManagerScope $scope -NetworkManagerScopeAccess @("SecurityAdmin")
# Create VNets
$prodSub = New-AzVirtualNetworkSubnetConfig -Name "workload-subnet" -AddressPrefix "10.1.1.0/24"
$bastionSub = New-AzVirtualNetworkSubnetConfig -Name "AzureBastionSubnet" -AddressPrefix "10.1.254.0/26"
$prodVnet = New-AzVirtualNetwork -Name "vnet-prod" -ResourceGroupName $rg `
-Location $location -AddressPrefix "10.1.0.0/16" `
-Subnet $prodSub, $bastionSub -Tag @{Environment="Production"}
$devSub = New-AzVirtualNetworkSubnetConfig -Name "workload-subnet" -AddressPrefix "10.2.1.0/24"
$devVnet = New-AzVirtualNetwork -Name "vnet-dev" -ResourceGroupName $rg `
-Location $location -AddressPrefix "10.2.0.0/16" `
-Subnet $devSub -Tag @{Environment="Development"}
Task 2: Create network groups with dynamic membership
Azure CLI
# Create network group for production VNets (dynamic membership by tag)
az network manager group create \
--name "ng-production" \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--description "All production VNets"
# Create network group for development VNets
az network manager group create \
--name "ng-development" \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--description "All development VNets"
# Add static members (alternatively use Azure Policy for dynamic membership)
PROD_VNET_ID=$(az network vnet show --resource-group $RG --name vnet-prod --query id -o tsv)
DEV_VNET_ID=$(az network vnet show --resource-group $RG --name vnet-dev --query id -o tsv)
az network manager group static-member create \
--network-group-name "ng-production" \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--name "vnet-prod-member" \
--resource-id $PROD_VNET_ID
az network manager group static-member create \
--network-group-name "ng-development" \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--name "vnet-dev-member" \
--resource-id $DEV_VNET_ID
For production deployments, use Azure Policy-based dynamic membership. VNets matching a condition (such as a tag) are automatically added to the network group:
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Network/virtualNetworks" },
{ "field": "tags['Environment']", "equals": "Production" }
]
},
"then": { "effect": "addToNetworkGroup" }
}
Task 3: Configure security admin rules (deny-by-default)
AVNM security admin rules are evaluated before NSG rules. The evaluation order is:
- AVNM security admin rules (AlwaysAllow > Deny > Allow)
- NSG rules
A Deny rule in AVNM cannot be overridden by an NSG Allow rule. An AlwaysAllow rule in AVNM bypasses both security admin Deny rules and NSG rules.
Azure CLI
# Create security admin configuration
az network manager security-admin-config create \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--name "config-deny-default" \
--description "Deny all inbound by default"
# Get network group ID for applies-to-groups
NG_PROD_ID=$(az network manager group show \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--name "ng-production" \
--query id -o tsv)
# Create rule collection
az network manager security-admin-config rule-collection create \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--config-name "config-deny-default" \
--name "rc-deny-inbound" \
--applies-to-groups network-group-id=$NG_PROD_ID \
--description "Deny all inbound management traffic"
# Rule: Deny all inbound SSH from internet
az network manager security-admin-config rule-collection rule create \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--config-name "config-deny-default" \
--rule-collection-name "rc-deny-inbound" \
--name "deny-ssh-inbound" \
--access Deny \
--direction Inbound \
--priority 100 \
--protocol Tcp \
--source-address-prefixes "*" \
--destination-address-prefixes "*" \
--destination-port-ranges 22
# Rule: Deny all inbound RDP from internet
az network manager security-admin-config rule-collection rule create \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--config-name "config-deny-default" \
--rule-collection-name "rc-deny-inbound" \
--name "deny-rdp-inbound" \
--access Deny \
--direction Inbound \
--priority 110 \
--protocol Tcp \
--source-address-prefixes "*" \
--destination-address-prefixes "*" \
--destination-port-ranges 3389
# Rule: AlwaysAllow Bastion traffic (cannot be blocked by other rules)
az network manager security-admin-config rule-collection rule create \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--config-name "config-deny-default" \
--rule-collection-name "rc-deny-inbound" \
--name "always-allow-bastion" \
--access AlwaysAllow \
--direction Inbound \
--priority 50 \
--protocol Tcp \
--source-address-prefixes "10.1.254.0/26" \
--destination-address-prefixes "*" \
--destination-port-ranges 22 3389
# Commit the configuration to apply it
az network manager post-commit \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--target-locations $LOCATION \
--configuration-ids "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.Network/networkManagers/avnm-enterprise/securityAdminConfigurations/config-deny-default" \
--commit-type "SecurityAdmin"
Azure PowerShell
# Create security admin configuration
$config = New-AzNetworkManagerSecurityAdminConfiguration `
-NetworkManagerName "avnm-enterprise" -ResourceGroupName $rg `
-Name "config-deny-default" -Description "Deny all inbound by default"
# Create rule collection
$ngProd = Get-AzNetworkManagerGroup -NetworkManagerName "avnm-enterprise" `
-ResourceGroupName $rg -Name "ng-production"
$appliesToGroup = New-AzNetworkManagerSecurityGroupItem -NetworkGroupId $ngProd.Id
New-AzNetworkManagerSecurityAdminRuleCollection `
-NetworkManagerName "avnm-enterprise" -ResourceGroupName $rg `
-SecurityAdminConfigurationName "config-deny-default" `
-Name "rc-deny-inbound" -AppliesToGroup $appliesToGroup
# Create deny SSH rule
New-AzNetworkManagerSecurityAdminRule `
-NetworkManagerName "avnm-enterprise" -ResourceGroupName $rg `
-SecurityAdminConfigurationName "config-deny-default" `
-RuleCollectionName "rc-deny-inbound" `
-Name "deny-ssh-inbound" -Access "Deny" -Direction "Inbound" `
-Priority 100 -Protocol "Tcp" `
-SourceAddressPrefix "*" -DestinationAddressPrefix "*" `
-DestinationPortRange "22"
Portal steps
- Navigate to Network Manager and select your AVNM instance.
- Under Settings, select Security admin configurations and click Create.
- Add a rule collection targeting the production network group.
- Create rules: Deny SSH (port 22) and RDP (port 3389) inbound from any source.
- Create an AlwaysAllow rule for the Bastion subnet source to ports 22 and 3389.
- Deploy the configuration to the target region.
Task 4: Deploy Azure Bastion (Standard SKU)
Azure CLI
# Create public IP for Bastion
az network public-ip create \
--resource-group $RG \
--name pip-bastion \
--sku Standard \
--allocation-method Static \
--location $LOCATION
# Deploy Azure Bastion with Standard SKU and native client support
az network bastion create \
--resource-group $RG \
--name bastion-prod \
--vnet-name vnet-prod \
--public-ip-address pip-bastion \
--sku Standard \
--enable-tunneling true \
--location $LOCATION
| Feature | Basic | Standard |
|---|---|---|
| Connect via portal (SSH/RDP) | Yes | Yes |
| Native client support (az network bastion ssh/rdp) | No | Yes |
| File transfer | No | Yes |
| Shareable link | No | Yes |
| Host scaling (2-50 instances) | No | Yes |
| IP-based connection | No | Yes |
The Standard SKU with --enable-tunneling true is required for native client connectivity via the Azure CLI.
Connect via native client
# Connect to a Linux VM through Bastion using native SSH client
az network bastion ssh \
--resource-group $RG \
--name bastion-prod \
--target-resource-id "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.Compute/virtualMachines/vm-prod1" \
--auth-type ssh-key \
--username azureuser \
--ssh-key ~/.ssh/id_rsa
Azure PowerShell
$pip = New-AzPublicIpAddress -Name "pip-bastion" -ResourceGroupName $rg `
-Location $location -Sku Standard -AllocationMethod Static
New-AzBastion -Name "bastion-prod" -ResourceGroupName $rg `
-VirtualNetworkId $prodVnet.Id -PublicIpAddressId $pip.Id `
-Sku "Standard" -EnableTunneling $true
Task 5: Configure Just-in-Time VM access
JIT VM access requires Microsoft Defender for Servers Plan 2 (or Defender for Cloud enhanced security). The VM must have an NSG associated with its subnet or NIC.
Deploy a target VM
# Create a VM in the production VNet
az vm create \
--resource-group $RG \
--name vm-prod1 \
--image Ubuntu2404 \
--size Standard_B2s \
--vnet-name vnet-prod \
--subnet workload-subnet \
--admin-username azureuser \
--generate-ssh-keys \
--nsg "" \
--public-ip-address ""
# Create and associate an NSG (required for JIT)
az network nsg create \
--resource-group $RG \
--name nsg-prod-workload \
--location $LOCATION
az network vnet subnet update \
--resource-group $RG \
--vnet-name vnet-prod \
--name workload-subnet \
--network-security-group nsg-prod-workload
Configure JIT policy via REST API
The Azure CLI does not provide a direct command to create JIT policies. Use az rest to call the Defender for Cloud API:
# Create JIT access policy
az rest --method put \
--uri "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.Security/locations/$LOCATION/jitNetworkAccessPolicies/default?api-version=2020-01-01" \
--body '{
"properties": {
"virtualMachines": [
{
"id": "/subscriptions/'$SUBSCRIPTION_ID'/resourceGroups/'$RG'/providers/Microsoft.Compute/virtualMachines/vm-prod1",
"ports": [
{
"number": 22,
"protocol": "TCP",
"allowedSourceAddressPrefix": "*",
"maxRequestAccessDuration": "PT3H"
},
{
"number": 3389,
"protocol": "TCP",
"allowedSourceAddressPrefix": "*",
"maxRequestAccessDuration": "PT3H"
}
]
}
]
},
"kind": "Basic"
}'
Request JIT access
# Initiate a JIT access request (opens port 22 for 1 hour)
MY_IP=$(curl -s https://ifconfig.me)
az rest --method post \
--uri "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.Security/locations/$LOCATION/jitNetworkAccessPolicies/default/initiate?api-version=2020-01-01" \
--body '{
"virtualMachines": [
{
"id": "/subscriptions/'$SUBSCRIPTION_ID'/resourceGroups/'$RG'/providers/Microsoft.Compute/virtualMachines/vm-prod1",
"ports": [
{
"number": 22,
"allowedSourceAddressPrefix": "'$MY_IP'",
"duration": "PT1H"
}
]
}
],
"justification": "Routine maintenance - applying security patches"
}'
List JIT policies
# List existing JIT policies
az security jit-policy list \
--location $LOCATION \
--resource-group $RG
Portal steps
- Navigate to Microsoft Defender for Cloud and then select Workload protections.
- Select Just-in-time VM access from the left menu.
- Click on the VM to configure and select Enable JIT.
- Configure ports (22, 3389), maximum request duration (3 hours), and allowed source IPs.
- To request access: select the VM, click Request access, specify justification and duration.
Task 6: Validate the zero-trust design
Verify AVNM blocks direct access
# Deploy a test VM with a public IP to verify AVNM enforcement
az vm create \
--resource-group $RG \
--name vm-test \
--image Ubuntu2404 \
--size Standard_B1s \
--vnet-name vnet-prod \
--subnet workload-subnet \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-address pip-vm-test
# Attempt direct SSH (should fail due to AVNM deny rule)
ssh azureuser@$(az network public-ip show --resource-group $RG --name pip-vm-test --query ipAddress -o tsv)
# Expected: Connection timed out
# Verify the AVNM rule is enforced (even if NSG allows SSH)
az network nsg rule create \
--resource-group $RG \
--nsg-name nsg-prod-workload \
--name allow-ssh-test \
--priority 100 \
--direction Inbound \
--source-address-prefixes "*" \
--destination-port-ranges 22 \
--protocol Tcp \
--access Allow
# Attempt SSH again (should STILL fail - AVNM deny overrides NSG allow)
ssh azureuser@$(az network public-ip show --resource-group $RG --name pip-vm-test --query ipAddress -o tsv)
# Expected: Connection timed out
Verify Bastion connectivity
# Connect via Bastion native client (should succeed due to AlwaysAllow rule)
az network bastion ssh \
--resource-group $RG \
--name bastion-prod \
--target-resource-id "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.Compute/virtualMachines/vm-prod1" \
--auth-type ssh-key \
--username azureuser \
--ssh-key ~/.ssh/id_rsa
Break & fix
Scenario 1: AVNM security admin rule blocks Bastion
Symptom: Azure Bastion cannot connect to VMs even though the Bastion deployment is successful.
Root cause: The AVNM deny rule blocks all inbound SSH/RDP traffic including traffic from the AzureBastionSubnet. No AlwaysAllow exception was created for the Bastion source prefix.
Diagnosis:
# Check effective security admin rules
az network manager list-effective-security-admin-rules \
--resource-group $RG \
--vnet-name vnet-prod
# Verify Bastion subnet prefix
az network vnet subnet show \
--resource-group $RG \
--vnet-name vnet-prod \
--name AzureBastionSubnet \
--query addressPrefix -o tsv
Fix: Add an AlwaysAllow rule with the Bastion subnet as the source:
az network manager security-admin-config rule-collection rule create \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--config-name "config-deny-default" \
--rule-collection-name "rc-deny-inbound" \
--name "always-allow-bastion" \
--access AlwaysAllow \
--direction Inbound \
--priority 50 \
--protocol Tcp \
--source-address-prefixes "10.1.254.0/26" \
--destination-address-prefixes "*" \
--destination-port-ranges 22 3389
# Commit the updated configuration
az network manager post-commit \
--network-manager-name "avnm-enterprise" \
--resource-group $RG \
--target-locations $LOCATION \
--configuration-ids "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.Network/networkManagers/avnm-enterprise/securityAdminConfigurations/config-deny-default" \
--commit-type "SecurityAdmin"
Scenario 2: JIT request failing
Symptom: JIT access requests return an error or the JIT option is not available for the VM.
Root cause: Microsoft Defender for Servers (Plan 2) is not enabled on the subscription, or the VM does not have an NSG associated.
Diagnosis:
# Check Defender for Cloud pricing tier
az security pricing show --name VirtualMachines --query pricingTier -o tsv
# Should return "Standard" for JIT to work
# Verify NSG is associated with the VM's subnet or NIC
az network vnet subnet show \
--resource-group $RG \
--vnet-name vnet-prod \
--name workload-subnet \
--query networkSecurityGroup.id -o tsv
Fix:
# Enable Defender for Servers (if not enabled)
az security pricing create --name VirtualMachines --tier Standard
# Ensure NSG is associated
az network vnet subnet update \
--resource-group $RG \
--vnet-name vnet-prod \
--name workload-subnet \
--network-security-group nsg-prod-workload
Scenario 3: Bastion native client not working
Symptom: The az network bastion ssh command fails with an error about unsupported features.
Root cause: Bastion was deployed with Basic SKU (tunneling/native client requires Standard SKU), or the --enable-tunneling flag was not set.
Diagnosis:
# Check Bastion SKU and tunneling status
az network bastion show \
--resource-group $RG \
--name bastion-prod \
--query "{sku: sku.name, tunneling: enableTunneling}"
Fix:
# Upgrade Bastion to Standard SKU with tunneling
az network bastion update \
--resource-group $RG \
--name bastion-prod \
--sku Standard \
--enable-tunneling true
Knowledge check
1. How do AVNM security admin rules interact with NSG rules?
2. What is required for Just-in-Time VM access to function?
3. Which Azure Bastion SKU supports native client connectivity (az network bastion ssh)?
4. What are the three access actions available in AVNM security admin rules?
5. You created an AVNM security admin rule that denies all inbound traffic. Azure Bastion can no longer connect to VMs. What should you do?
6. How can you define dynamic membership for an AVNM network group?
Cleanup
az group delete --name $RG --yes --no-wait
Remove-AzResourceGroup -Name "rg-avnm-lab" -Force -AsJob
This lab deploys Azure Bastion (Standard SKU) which costs approximately $0.25/hour plus VMs. Delete the resource group immediately after completing the lab. Additionally, disable Defender for Servers if you enabled it only for this lab to avoid ongoing charges (~$15/server/month).