Blog

Securing Azure Virtual Machines: Using Azure Bastion and Just-In-Time (JIT) Access

18.11.2024

Technology Blog Azure

As businesses increasingly migrate to the cloud, securing critical workloads becomes more complex. Implementing strong security practices, such as least-privileged access, is essential to reducing risks and ensuring cloud environments are protected. To help achieve this, Azure offers few security solutions to protect your virtual machines: Azure Bastion and Just-In-Time (JIT) Access. These services work together to minimize the attack surface and provide secure, controlled access to VMs. 

What is Azure Bastion?

architecture

 

See the full image by clicking the image

Azure Bastion is fully managed, designed to provide secure, remote access to VMs via RDP or SSH without exposing them to the public internet. Azure Bastion is deployed and connects within your virtual networks, meaning your VMs do not require public IP addresses exposed to the internet. 

How to deploy Azure Bastion using Azure Verified Modules

If you don't have Azure Bastion deployed already, take a look into AVM modules. Azure Verified Modules (AVM) are Microsoft-certified, pre-built IaC templates. Available for Bicep and Terraform. These reusable modules implement best practices, making it easier to deploy secure and consistent infrastructure. 

Just-In-Time (JIT) VM Access with Microsoft Defender for Cloud

While Azure Bastion ensures secure connectivity, JIT VM access, part of Microsoft Defender for Cloud, takes security a step further by controlling when and how users can access VMs. JIT restricts unnecessary inbound traffic by locking down management ports like RDP and SSH at the network security group (NSG) level. By default, JIT denies all inbound traffic to these ports. When a user requests access to these VMs, JIT adds an allow rule to the NSG with higher priority. Rule allows these ports from Bastion subnet prefix and the destination as VMs private IP, for a limited time. Once the configured time period expires, the allow rule is automatically removed.

image

Implementing Role-Based Access Control (RBAC) for JIT and Bastion Access

As there are multiple resources involved with this architecture, Azure custom roles enable ways to expand those built-in roles. Multiple actions/roles are needed for your employees to use this solution. Custom roles can be tailored to minimum possible permissions, but remember that there is management overhead in administering them.

Resource Role/Actions Description
Azure Bastion Reader role on Bastion, VM(s), VNet(s), NICs  Using Azure Bastion 
Virtual Machines (VMs)  Virtual Machine User Login (Built-in role)  Reading, logging in to VMs 
Just-in-Time (JIT)  Custom security actions. Read more Allowing users to request JIT VM access

Setting Up Defender for Cloud with JIT Access

Just-in-Time (JIT) access is available as part of Defender for Servers (Plan 2) and can be enabled through several methods: directly via the Defender for Cloud dashboard, REST API, or by leveraging Azure Policy. Microsoft provides built-in policies to configure Defender for Servers at the Subscription or Management Group (MG) scope, allowing current and future subscriptions to be managed and enforced at scale. Enabling Defender for Servers (Plan 2) at the Subscription level applies across all supported resources within that subscription. More information on features, deployment, and pricing available here.

Configuring JIT Policies

To begin configuring JIT policies, few options are available. You can enable and configure JIT directly from the Azure portal or via PowerShell. Microsoft provides a PowerShell cmdlet to enable JIT on individual virtual machines. 

Configuring JIT via PowerShell offers flexibility, including the option to assign a descriptive name to the configuration rather than using the default name. To apply JIT at scale, for example, at the Subscription level, PowerShell can be used to iterate through all VMs within a specified scope. We set parameters such as the allowed source address prefix (e.g., a Bastion subnet), authorized ports, and the access time period. To exclude specific VMs from JIT, simply add them to an exclusion array in the configuration.

				
# Add Subscription IDs here that will be iterated<br /> $SubscriptionIds = @(<br /> "Subscriptionid1",<br /> "Subscriptionid2"<br /> )<br /> # Add any virtual machines here you want to be excluded from JIT<br /> $ExcludedVMs = @(<br /> "VMName1",<br /> "VMName2"<br /> )<br /> foreach ($SubscriptionId in $SubscriptionIds) {<br /> Write-Output "Attempting to set context for subscription: $SubscriptionId"<br /> try {<br /> Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction Stop<br /> Write-Output "Processing subscription: $SubscriptionId"<br /> }<br /> catch {<br /> Write-Output "Failed to set context for subscription: $SubscriptionId"<br /> Write-Output $_<br /> continue<br /> }<br /> try {<br /> $ResourceGroups = Get-AzResourceGroup<br /> foreach ($ResourceGroup in $ResourceGroups) {<br /> $ResourceGroupName = $ResourceGroup.ResourceGroupName<br /> # Get VMs in the resource group<br /> $VMs = Get-AzVM -ResourceGroupName $ResourceGroupName<br /> # Only write output if there are VMs in the RG<br /> if ($VMs.Count -gt 0) {<br /> Write-Output "Processing resource group: $ResourceGroupName"<br /> }<br /> foreach ($VM in $VMs) {<br /> $VMName = $VM.Name<br /> # Check if the VM is in the exclusion list<br /> if ($ExcludedVMs -contains $VMName) {<br /> Write-Output "Skipping VM (excluded): $VMName"<br /> continue<br /> }<br /> # Check if JIT is already enabled for the VM<br /> $JitPolicies = Get-AzJitNetworkAccessPolicy -ResourceGroupName $ResourceGroupName<br /> $JitPolicyExists = $false<br /> foreach ($JitPolicy in $JitPolicies.VirtualMachines) {<br /> if ($JitPolicy.id -eq "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Compute/virtualMachines/$VMName") {<br /> $JitPolicyExists = $true<br /> break<br /> }<br /> }<br /> if ($JitPolicyExists) {<br /> Write-Output "JIT already enabled for VM: $VMName"<br /> } else {<br /> # JIT policy configuration<br /> $JitPolicyConfig = @(<br /> @{<br /> id = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Compute/virtualMachines/$VMName";<br /> ports = @(<br /> @{<br /> number = 22; # SSH port<br /> protocol = "*";<br /> allowedSourceAddressPrefix = @(""); # Add allowed source address prefix, Azure Bastion subnet<br /> maxRequestAccessDuration = "PT3H" # Set time window of 3 Hours. Maximum is 24 hours.<br /> },<br /> @{<br /> number = 3389; # RDP port<br /> protocol = "*";<br /> allowedSourceAddressPrefix = @(""); # Add allowed source address prefix, Azure Bastion subnet<br /> maxRequestAccessDuration = "PT3H" # Set time window of 3 hours. Maximum is 24 hours.<br /> }<br /> )<br /> }<br /> )<br /> # Enable JIT for VM<br /> Set-AzJitNetworkAccessPolicy -Kind "Basic" -Location $VM.Location -Name "${VMName}-JITPolicy" -ResourceGroupName $ResourceGroupName -VirtualMachine $JitPolicyConfig<br /> Write-Output "JIT enabled for VM: $VMName"<br /> }<br /> }<br /> }<br /> }<br /> catch {<br /> Write-Output "An error occurred while processing subscription: $SubscriptionId"<br /> Write-Output $_<br /> }<br /> }

Auditing JIT Requests Using KQL

When JIT access is initiated by employees, we can monitor those activities from Defender for Cloud dashboard. Besides the dashboard, we can also stream the activity logs to Storage Account, Event Hub, other partner solutions, or Log Analytics Workspace. When you export Administrative logs to LAW, a table named 'AzureActivity' will be created. We can then query who initiated a policy, which policy, and when. This simple query lists all JIT activity events. We can then create alerts based on these results to get notified when access request is made on one of your VM's hosting critical workloads or holding sensitive data.

				
AzureActivity<br /> | where OperationNameValue == "MICROSOFT.SECURITY/LOCATIONS/JITNETWORKACCESSPOLICIES/INITIATE/ACTION"<br /> | where ActivityStatusValue == "Start"<br /> | project TimeGenerated, ActivityStatusValue, Caller, _ResourceId, ResourceGroup

Conclusion: A Multilayered Security Approach For Azure VMs

This solution establishes a comprehensive and multilayered security approach for managing Azure VMs, a starting point for Microsoft's Well-Architected Framework for securing Azure resources. Close down your network, grant access for authenticated users only when needed, and auditing. 

Interested in PoC, or simply need help with building a secure environment following the Well-Architected Framework?

We'd be happy to help you out in implementing these technologies to secure your remote VM access. The blog describes the main areas of setting up the access, but there's naturally planning and assessing involved in the project as there pretty much always is.

Janne Enberg

Janne Enberg

Janne has over a decade of experience as a systems administrator, managing everything from networks and servers to developing automation tools. In recent years, he has transitioned his focus to Microsoft Azure, where he specializes in designing, building, and managing complex cloud solutions.

Use H2 for the title

Write your content

Use H2 for the title

Write your content