- Blog
#Azure
How to plan your Azure budget for 2025-2026 with FinOps
- 11/12/2024
Reading time 5 minutes
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.
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.
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.
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.
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 |
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.
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
$SubscriptionIds = @(
"Subscriptionid1",
"Subscriptionid2"
)
# Add any virtual machines here you want to be excluded from JIT
$ExcludedVMs = @(
"VMName1",
"VMName2"
)
foreach ($SubscriptionId in $SubscriptionIds) {
Write-Output "Attempting to set context for subscription: $SubscriptionId"
try {
Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction Stop
Write-Output "Processing subscription: $SubscriptionId"
}
catch {
Write-Output "Failed to set context for subscription: $SubscriptionId"
Write-Output $_
continue
}
try {
$ResourceGroups = Get-AzResourceGroup
foreach ($ResourceGroup in $ResourceGroups) {
$ResourceGroupName = $ResourceGroup.ResourceGroupName
# Get VMs in the resource group
$VMs = Get-AzVM -ResourceGroupName $ResourceGroupName
# Only write output if there are VMs in the RG
if ($VMs.Count -gt 0) {
Write-Output "Processing resource group: $ResourceGroupName"
}
foreach ($VM in $VMs) {
$VMName = $VM.Name
# Check if the VM is in the exclusion list
if ($ExcludedVMs -contains $VMName) {
Write-Output "Skipping VM (excluded): $VMName"
continue
}
# Check if JIT is already enabled for the VM
$JitPolicies = Get-AzJitNetworkAccessPolicy -ResourceGroupName $ResourceGroupName
$JitPolicyExists = $false
foreach ($JitPolicy in $JitPolicies.VirtualMachines) {
if ($JitPolicy.id -eq "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Compute/virtualMachines/$VMName") {
$JitPolicyExists = $true
break
}
}
if ($JitPolicyExists) {
Write-Output "JIT already enabled for VM: $VMName"
} else {
# JIT policy configuration
$JitPolicyConfig = @(
@{
id = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Compute/virtualMachines/$VMName";
ports = @(
@{
number = 22; # SSH port
protocol = "*";
allowedSourceAddressPrefix = @(""); # Add allowed source address prefix, Azure Bastion subnet
maxRequestAccessDuration = "PT3H" # Set time window of 3 Hours. Maximum is 24 hours.
},
@{
number = 3389; # RDP port
protocol = "*";
allowedSourceAddressPrefix = @(""); # Add allowed source address prefix, Azure Bastion subnet
maxRequestAccessDuration = "PT3H" # Set time window of 3 hours. Maximum is 24 hours.
}
)
}
)
# Enable JIT for VM
Set-AzJitNetworkAccessPolicy -Kind "Basic" -Location $VM.Location -Name "${VMName}-JITPolicy" -ResourceGroupName $ResourceGroupName -VirtualMachine $JitPolicyConfig
Write-Output "JIT enabled for VM: $VMName"
}
}
}
}
catch {
Write-Output "An error occurred while processing subscription: $SubscriptionId"
Write-Output $_
}
}
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
| where OperationNameValue == "MICROSOFT.SECURITY/LOCATIONS/JITNETWORKACCESSPOLICIES/INITIATE/ACTION"
| where ActivityStatusValue == "Start"
| project TimeGenerated, ActivityStatusValue, Caller, _ResourceId, ResourceGroup
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.
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.
Our newsletters contain stuff our crew is interested in: the articles we read, Azure news, Zure job opportunities, and so forth.
Please let us know what kind of content you are most interested about. Thank you!