Just-in-time VM access with Defender for Cloud

Just-in-time VM access keeps management ports like 22 and 3389 closed all the time, and opens them only when someone asks, only for their IP, and only for a few hours. It’s part of Microsoft Defender for Cloud.

Bastion (Lesson 27) is the best answer for admin access, but many companies still have VMs with public IPs: vendor appliances, legacy apps, or servers reached from tools that can’t use Bastion. JIT is how you protect those.

In the lab you switch on the Defender plan JIT needs, protect web01, open SSH for one hour from your IP, and then see the two ways JIT quietly fails.

TimeYou needCostBy the end you can
About 45 minutesweb01, the CLI, a subscription that can start the Defender for Servers trialAbout 1 US cent for a temporary public IP, if the Defender for Servers Plan 2 trial is active; without the trial, Plan 2 costs about 2 US cents per VM-hourEnable JIT, request and audit access, and spot rules that bypass it

What it is, in one line

Just-in-time VM access opens a management port for one IP and a set time, then closes it again by itself.

In Simple Words

The school gate during class hours

The school gate stays locked all day. A parent who needs to come in rings the office, says who they are and why, and the guard opens the gate for that parent only, for twenty minutes.

After twenty minutes the gate locks again, even if the parent forgets to say goodbye. The office book shows who came, when and why.

But if a teacher props the side door open with a chair, the locked gate doesn’t matter any more. That chair is the second break in today’s lab.

Engineer's View

How JIT works under the hood

  • Licence: JIT is a feature of Defender for Servers Plan 2 (about USD 15 per server per month after a 30-day trial). Plan 1 doesn’t include it.
  • Policy: for each VM you list the ports (for example 22 and 3389), the protocol, which source addresses may be requested, and the maximum time (up to 24 hours).
  • Enforcement: Defender adds deny rules for those ports to the NSGs on the VM’s NIC and subnet (and to Azure Firewall if used). Rules that already allow those ports keep winning, because Defender doesn’t remove them.
  • Request: a user with the right RBAC action (Microsoft.Security/locations/jitNetworkAccessPolicies/initiate/action) asks for a port, source IP and duration. Defender adds an allow rule above the deny, then removes it when time is up.
  • Audit: every request lands in the Activity log with the requester, ports, source IP and justification.
  • API: Microsoft.Security/locations/{region}/jitNetworkAccessPolicies, version 2020-01-01. The CLI can list policies, and az rest does the rest.

How it works, step by step

What happens to the NSG during one JIT request:

Just-in-time VM access: what happens to the NSG during a request
  1. Policy saved: Defender adds “deny 22 from any” for web01’s IP to nsg-saat-web.
  2. Request: you ask for 22, from your IP, for one hour, with a reason.
  3. Allow rule: Defender adds “allow 22 from your IP” with a higher priority than its deny rule.
  4. Work: you SSH in. Anyone else still hits the deny rule.
  5. Expiry: after one hour Defender deletes the allow rule. The deny rule stays.

JIT only touches the ports in its policy. Everything else in the NSG is your responsibility, including old rules that already allow the same ports.

Just-in-time VM access bypassed by an older NSG allow rule

Where you can, combine both: Bastion for admins, JIT only for the few VMs that truly need a public management port.

Hands-on lab: just-in-time VM access for web01

Set up the names you’ll use:

$sub = az account show --query id -o tsv
$vmId = az vm show --resource-group rg-saat-vms --name web01 --query id -o tsv
$jit = "https://management.azure.com/subscriptions/$sub/resourceGroups/rg-saat-vms/providers/Microsoft.Security/locations/centralindia/jitNetworkAccessPolicies/default"
$ip = Invoke-RestMethod -Uri https://api.ipify.org
az vm start --resource-group rg-saat-vms --name web01

1Turn on Defender for Servers Plan 2

Run this:

az security pricing create --name VirtualMachines --tier Standard --subplan P2 -o none
az security pricing show --name VirtualMachines --query "{tier:pricingTier, plan:subPlan, trialLeft:freeTrialRemainingTime}" -o table

You should see something like this:

Tier      Plan    TrialLeft
--------  ------  -----------
Standard  P2      29.23:59:00

If TrialLeft shows zero, your subscription already used its trial and Plan 2 is billed per VM-hour. You’ll switch it back off at the end.

2Give web01 a temporary public IP

JIT is for VMs reached over a public IP, so add one just for this lesson:

az network public-ip create --resource-group rg-saat-vms --name pip-web01-tmp --sku Standard --location centralindia --tags course=saatvedha-azure lesson=28 -o none
$nic = az vm show --resource-group rg-saat-vms --name web01 --query "networkProfile.networkInterfaces[0].id" -o tsv
$ipc = az network nic show --ids $nic --query "ipConfigurations[0].id" -o tsv
az network nic ip-config update --ids $ipc --public-ip-address pip-web01-tmp -o none
$pip = az network public-ip show --resource-group rg-saat-vms --name pip-web01-tmp --query ipAddress -o tsv
(Test-NetConnection -ComputerName $pip -Port 22 -WarningAction SilentlyContinue).TcpTestSucceeded

You should see this:

False

Port 22 is already closed: nsg-saat-web only allows 80 and 443 from the internet. JIT will make that closure explicit and auditable.

3Create the JIT policy for web01

Run this:

$policy = @{ kind = 'Basic'; properties = @{ virtualMachines = @(@{ id = $vmId; ports = @(@{ number = 22; protocol = 'TCP'; allowedSourceAddressPrefix = '*'; maxRequestAccessDuration = 'PT3H' }) }) } }
$policy | ConvertTo-Json -Depth 6 | Set-Content -Path C:\azure-lab\jit-policy.json
az rest --method put --url "${jit}?api-version=2020-01-01" --body '@C:\azure-lab\jit-policy.json' -o none
Start-Sleep -Seconds 60
az network nsg rule list --resource-group rg-saat-core --nsg-name nsg-saat-web --query "[?contains(name, 'JIT')].{name:name, access:access, port:destinationPortRange, priority:priority}" -o table

You should see something like this:

Name                                           Access    Port    Priority
---------------------------------------------  --------  ------  ----------
SecurityCenter-JITRule-1347516193-B4FC8A3BAB   Deny      22      4096

4Request access for one hour

Ask for port 22 from your current public IP:

$req = @{ virtualMachines = @(@{ id = $vmId; ports = @(@{ number = 22; duration = 'PT1H'; allowedSourceAddressPrefix = $ip }) }); justification = 'Lesson 28 test' }
$req | ConvertTo-Json -Depth 6 | Set-Content -Path C:\azure-lab\jit-request.json
az rest --method post --url "${jit}/initiate?api-version=2020-01-01" --body '@C:\azure-lab\jit-request.json' --query "virtualMachines[0].ports[0].{port:number, source:allowedSourceAddressPrefix, status:status}" -o table
Start-Sleep -Seconds 60
(Test-NetConnection -ComputerName $pip -Port 22 -WarningAction SilentlyContinue).TcpTestSucceeded
ssh azureuser@$pip hostname

You should see something like this:

Port    Source         Status
------  -------------  ---------
22      49.36.x.x      Initiated
True
web01

Defender added an allow rule for your IP above its deny rule. In an hour it removes it, with no action from you.

Defender for Cloud just-in-time VM access configured tab
Just-in-time VM access, Configured tab: web01 with an active request.
Just-in-time VM access lab: port 22 closed, requested, then open for one IP
Port 22 closed, a JIT request for one IP, then SSH works.

✅ Checkpoint: web01 is protected by JIT, your request opened port 22 for your IP only, and SSH works until the time runs out.

Break It, Then Fix It

JIT rarely fails with an error. It fails quietly, which is worse.

1Break it: the request for the wrong IP

A colleague requests access with the office IP while you work from home:

$req = @{ virtualMachines = @(@{ id = $vmId; ports = @(@{ number = 22; duration = 'PT1H'; allowedSourceAddressPrefix = '203.0.113.10' }) }); justification = 'Lesson 28 test' }
$req | ConvertTo-Json -Depth 6 | Set-Content -Path C:\azure-lab\jit-request.json
az rest --method post --url "${jit}/initiate?api-version=2020-01-01" --body '@C:\azure-lab\jit-request.json' --query "virtualMachines[0].ports[0].{port:number, source:allowedSourceAddressPrefix, status:status}" -o table
Start-Sleep -Seconds 60
az network nsg rule list --resource-group rg-saat-core --nsg-name nsg-saat-web --query "[?access=='Allow' && destinationPortRange=='22'].sourceAddressPrefix" -o tsv

You should see something like this:

Port    Source         Status
------  -------------  ---------
22      203.0.113.10   Initiated
49.36.x.x
203.0.113.10

The request “worked”, and Defender shows it as approved. But if your first request had expired, your laptop would get False from Test-NetConnection: the allow rule is for an address you aren’t using. Home internet IPs also change, sometimes several times a day, which causes the same “JIT is broken” ticket.

Fix it: always request with your current public IP (the portal’s “My IP” option does this), and check what the server sees:

$ip = Invoke-RestMethod -Uri https://api.ipify.org
$req = @{ virtualMachines = @(@{ id = $vmId; ports = @(@{ number = 22; duration = 'PT1H'; allowedSourceAddressPrefix = $ip }) }); justification = 'Lesson 28 test' }
$req | ConvertTo-Json -Depth 6 | Set-Content -Path C:\azure-lab\jit-request.json
az rest --method post --url "${jit}/initiate?api-version=2020-01-01" --body '@C:\azure-lab\jit-request.json' --query "virtualMachines[0].ports[0].{port:number, source:allowedSourceAddressPrefix, status:status}" -o table

2Break it: the old rule that makes JIT useless

Someone adds a “temporary” rule to fix a vendor issue, and forgets it:

az network nsg rule create --resource-group rg-saat-core --nsg-name nsg-saat-web --name tmp-vendor-ssh --priority 150 --direction Inbound --access Allow --protocol Tcp --source-address-prefixes Internet --destination-port-ranges 22 -o none
az network nsg rule list --resource-group rg-saat-core --nsg-name nsg-saat-web --query "[?destinationPortRange=='22'].{name:name, access:access, source:sourceAddressPrefix, priority:priority}" -o table

You should see something like this:

Name                                           Access    Source        Priority
---------------------------------------------  --------  ------------  ----------
tmp-vendor-ssh                                 Allow     Internet      150
SecurityCenter-JITRule-1347516193-B4FC8A3BAB   Allow     49.36.x.x     100
SecurityCenter-JITRule-...                     Allow     203.0.113.10  101
SecurityCenter-JITRule-1347516193-B4FC8A3BAB   Deny      *             4096

Priority 150 sits far above JIT’s deny at 4096, so port 22 is open to the whole internet whether anyone requests access or not. Defender doesn’t delete rules it didn’t create; its recommendations will flag the open port, but JIT itself looks fine.

Fix it: delete the rule, and look for any other allow rule on JIT ports:

az network nsg rule delete --resource-group rg-saat-core --nsg-name nsg-saat-web --name tmp-vendor-ssh
az network nsg rule list --resource-group rg-saat-core --nsg-name nsg-saat-web --query "[?destinationPortRange=='22' && access=='Allow' && !contains(name, 'JIT')].name" -o tsv

The last command prints nothing: only JIT controls port 22 again.

Clean up: remove the JIT policy, the public IP and the Defender plan, then stop web01:

az rest --method delete --url "${jit}?api-version=2020-01-01"
az network nic ip-config update --ids $ipc --remove publicIPAddress -o none
az network public-ip delete --resource-group rg-saat-vms --name pip-web01-tmp
az security pricing create --name VirtualMachines --tier Free -o none
az vm deallocate --resource-group rg-saat-vms --name web01
az network nsg rule list --resource-group rg-saat-core --nsg-name nsg-saat-web --query "[?contains(name, 'JIT')].name" -o tsv
C:\azure-lab\Find-LabResources.ps1 -Lesson 28

You should see this (the rule list may take a minute to empty):

Nothing tagged course=saatvedha-azure is left.
Just-in-time VM access problems: wrong source IP and a bypass rule
An approved request for the wrong IP, and an old rule that opens the port anyway.

✅ Checkpoint: No JIT rules, no public IP and no tmp rule remain, Defender for Servers is back on Free, and web01 is deallocated.

This happened at work

JIT was on, and the server was still attacked

A logistics company had JIT enabled on all 120 VMs and passed its security audit. Three months later, a Windows server with a public IP was hit by a password-spraying attack on RDP.

The investigation found an NSG rule named “allow-rdp-vendor” at priority 200, created two years earlier for a printer vendor. JIT’s deny rule sat at 4096, below it, so RDP had been open to the internet the whole time. The JIT dashboard showed the VM as protected.

What they changed: an Azure Policy now denies NSG rules that allow 22 or 3389 from Internet or Any, except rules created by Defender. A weekly Resource Graph query lists every allow rule on management ports. And the audit checklist now asks “what else allows this port?”, not just “is JIT enabled?”.

Common mistakes and troubleshooting

MistakeWhat you seeWhat to do
Old allow rules on JIT portsPort open without any requestDelete them; JIT doesn’t remove existing rules.
Requesting with the wrong source IPJIT approved, connection still failsUse “My IP” or check your public IP first.
Defender for Servers Plan 1 onlyJIT option missingJIT needs Plan 2.
VM without an NSGJIT can’t be enabledAttach an NSG to the NIC or subnet.
Using JIT instead of Bastion everywherePublic IPs on many VMsBastion for admins; JIT only where needed.
Leaving the Plan 2 trial running after labsCharges after 30 daysSwitch the plan back to Free.

Interview questions

Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.

Fresher level

1. What is just-in-time VM access?

Why they ask this: The basic concept.

How you might answer: A Defender for Cloud feature that keeps management ports closed with NSG deny rules and opens them only on request, for a specific source IP and a limited time.

2. Which licence does JIT need?

Why they ask this: A practical prerequisite check.

How you might answer: Microsoft Defender for Servers Plan 2 on the subscription or the servers. Plan 1 doesn’t include it.

3. What happens when a JIT request expires?

Why they ask this: They want to see you know it’s automatic.

How you might answer: Defender removes the allow rule it added, so the port is closed again by the deny rule. Nobody has to remember to close it.

2–5 years experience

4. JIT shows access approved, but the user still can’t connect. What do you check?

Why they ask this: A common support ticket.

How you might answer: That the requested source IP matches the user’s real public IP, that the allow rule exists in the NSG, any other NSG on the NIC or subnet, the OS firewall, and whether the VM is listening. Changing home IPs are the usual cause.

5. How does JIT work with existing NSG rules?

Why they ask this: They’re checking the bypass risk.

How you might answer: JIT adds deny rules for its ports but doesn’t remove existing rules. An older allow rule with a higher priority still lets traffic in, so JIT looks enabled while the port is open.

6. JIT or Bastion: which do you recommend?

Why they ask this: They want judgement.

How you might answer: Bastion for routine admin access, because VMs need no public IP at all. JIT for the few VMs that must keep a public IP, or where a tool needs a direct connection. Both together is common.

Senior level

7. How would you roll out JIT across hundreds of VMs?

Why they ask this: They want automation and governance.

How you might answer: Enable Plan 2 by policy, create JIT policies with ARM, Bicep or the REST API per resource group, and set maximum durations. Then add a policy that denies other allow rules on management ports, grant the initiate action to on-call groups through PIM, and alert on JIT requests.

8. How do you audit who opened which port and when?

Why they ask this: They want logging knowledge.

How you might answer: JIT requests are in the Activity log under Microsoft.Security jitNetworkAccessPolicies initiate, with the user, ports, source IP and justification. I’d export the Activity log to Log Analytics and keep it for the audit period.

9. What are JIT’s limits?

Why they ask this: They want honesty about the tool.

How you might answer: It only controls the ports in its policy, depends on NSGs or Azure Firewall, can be bypassed by other rules, and doesn’t stop someone who already has a session. It also needs a public path, which Bastion avoids.

Quick quiz

Q1. Which Defender plan includes JIT?

  1. Foundational CSPM only
  2. Defender for Servers Plan 1
  3. Defender for Servers Plan 2
  4. Defender for Storage
Show answer

C — JIT is part of Plan 2.

Q2. What does JIT add to the NSG when the policy is saved?

  1. Allow rules
  2. Deny rules for the chosen ports
  3. Nothing
  4. A public IP
Show answer

B — Deny rules keep the ports closed until requested.

Q3. An old rule allows 22 at priority 150; JIT deny is at 4096. Result?

  1. Port closed
  2. Port open to the rule’s sources
  3. JIT deletes the old rule
  4. The VM stops
Show answer

B — The lower number wins, so the old allow rule bypasses JIT.

Q4. When a JIT request expires, Defender:

  1. Deletes the VM
  2. Removes its allow rule
  3. Rotates keys
  4. Turns off the NSG
Show answer

B — The allow rule is removed automatically.

Q5. Where are JIT requests audited?

  1. Activity log
  2. Boot diagnostics
  3. Storage logs
  4. Nowhere
Show answer

A — Each request is recorded in the Activity log.

Quick recap

You can now open a management port safely and prove it closes again.

  • JIT needs Defender for Servers Plan 2.
  • The policy adds deny rules; requests add timed allow rules for one IP.
  • Old allow rules with higher priority bypass JIT.
  • Request with your current public IP.
  • Bastion first; JIT for the VMs that must keep a public IP.

Next lesson

Lesson 29: VM tools: Run Command, extensions, Custom Script → You manage VMs from outside the OS with Run Command and extensions, and fix a stuck extension.

Further reading: just-in-time VM access

Want to go deeper into just-in-time VM access? See Microsoft’s Understand just-in-time VM access and Enable just-in-time access on VMs. You can also see the full plan for this course on the Azure Cloud Administration course page.

Scroll to Top