Azure Linux SSH Authentication: Entra Id

Access Linux Machines using Entra Id

Introduction

Managing SSH access to Linux virtual machines (VMs) in Azure can be challenging, especially in enterprise environments where traditional methods like SSH keys and passwords become difficult to scale. Key rotation, user onboarding/offboarding, and ensuring secure access across multiple VMs often lead to operational overhead and security risks.

Microsoft Entra ID (formerly Azure Active Directory) provides a centralized authentication solution, eliminating the need for local user accounts and key management. By integrating Entra ID with Azure RBAC (Role-Based Access Control), organizations can streamline SSH access management, improve security, and enforce least-privilege access with minimal administrative effort.

In this guide, we’ll walk through configuring Entra ID authentication for Linux VMs in Azure, covering role assignments, infrastructure automation using Bicep, and best practices for securing access.

For more information about Entra Id and Linux Auth options you can check the Microsoft Docs: here

Why Use Entra ID Authentication?

Authentication MethodProsCons
Username & PasswordSimple to use, familiar to most usersSusceptible to brute force attacks, harder to manage at scale
SSH KeysMore secure than passwords, widely supportedKey distribution and rotation can be complex
Entra ID AuthenticationCentralized access management, integrates with RBAC, eliminates direct credential handlingRequires additional configuration, depends on Entra ID availability

For large organizations, Entra ID authentication is the recommended approach because it streamlines user management, enforces security policies, and integrates seamlessly with Azure’s RBAC model.

Configuring RBAC for Entra ID SSH

To enable Entra ID authentication for Linux VMs, users need specific RBAC roles assigned within Azure. The key roles include:

  • Virtual Machine Administrator Login: 1c0163c0-47e6-4577-8991-ea5c82e286e4
    Grants SSH access with sudo privileges.

  • Virtual Machine User Login: fb879df8-f326-4884-b1cf-06f3ad86be52
    Grants SSH access without sudo privileges.

Best Practice Configuration

Instead of assigning roles directly to users, the best practice is to use an Entra ID security group. This ensures that access is easily managed as team members join or leave.

Steps to configure RBAC using an Entra ID security group:

  1. Create an Entra ID security group
1
2
$groupName = 'sec-azure-auth-vm-administrator'
az ad group create --display-name $groupName --mail-nickname $groupName --security-enabled true
  1. Add relevant users to the group

  2. Assign the appropriate role to the group at the subscription, resource group, or VM level

1
2
3
4
5
6
7
8
$vmName = ''
$resourceGroup = ''
$subscriptionId = ''
$entraIdGroup = ''

az role assignment create --assignee $entraIdGroup \
  --role "Virtual Machine User Login" \
  --scope "/subscriptions/$subscriptionId/resourceGroups/$resourceGroup/providers/Microsoft.Compute/virtualMachines/$vmName"

Infrastructure As Code

For a structured deployment, we use Bicep modules to provision an Ubuntu 24.04 VM with Entra ID authentication enabled. You can check out all the code base over in the BWC Bicep Repository

Bicep Custom Module Required!

If you wish to deploy this solution using IaC, You need to create this local custom module.
This is the Azure AD based SSH Login

NOTE: For this Module, You need to configure a System Assigned Identity on the Machine

If you’re wanting to use the below Infrastructure As Code, Please create a folder structure as follows:

.\modules\compute\virtual-machines\extensions

and save the file as:

azureADSSHExtension.bicep

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
param vmName string
param location string

resource aadSshLogin 'Microsoft.Compute/virtualMachines/extensions@2024-07-01' = {
  name: '${vmName}/AADSSHLogin'
  location: location
  properties: {
    publisher: 'Microsoft.Azure.ActiveDirectory'
    type: 'AADSSHLoginForLinux'
    typeHandlerVersion: '1.0'
    autoUpgradeMinorVersion: true
  }
}

Invoke-AzDeployment
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
<#
.SYNOPSIS
    This script deploys Azure resources using Bicep templates.

.DESCRIPTION
    The script performs the following tasks:
    - Validates input parameters.
    - Checks if Azure CLI is installed.
    - Authenticates the user with Azure CLI.
    - Sets the Azure subscription context.
    - Generates a deployment GUID.
    - Maps Azure location to short codes.
    - Optionally deploys the Bicep template if the deploy switch is provided.

.PARAMETER targetScope
    The scope of the deployment. Valid values are 'tenant', 'mg', 'sub'.

.PARAMETER subscriptionId
    The Azure Subscription ID where the deployment will take place.

.PARAMETER environmentType
    The environment type for the deployment. Valid values are 'dev', 'acc', 'prod'.

.PARAMETER location
    The Azure location for the deployment. Valid values are various Azure regions.

.PARAMETER deploy
    A switch to execute the infrastructure deployment.

.EXAMPLE
    .\Invoke-AzDeployment.ps1 -targetScope 'sub' -subscriptionId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' -environmentType 'dev' -customerName 'bwc' -location 'westeurope' -deploy

.NOTES
    Ensure that Azure CLI is installed and you are logged in before running this script.
#>

param (
    [Parameter(Mandatory = $true, Position = 0, HelpMessage = "Deployment Guid is required")]
    [validateSet('tenant', 'mgmt', 'sub')] [string] $targetScope,

    [Parameter(Mandatory = $true, Position = 1, HelpMessage = "Azure Subscription Id is required")]
    [string] $subscriptionId,

    [Parameter(Mandatory = $true, Position = 2, HelpMessage = "Customer Name")]
    [string] $customerName,

    [Parameter(Mandatory = $true, Position = 3, HelpMessage = "Environment Type is required")]
    [validateSet('dev', 'acc', 'prod')][string] $environmentType,

    [Parameter(Mandatory = $true, Position = 4, HelpMessage = "Azure Location is required")]
    [validateSet("eastus", "eastus2", "westus", "westus2", "centralus", "northcentralus", "southcentralus",
        "westcentralus", "westus3", "eastus3", "northeurope", "westeurope", "swedencentral", "swedensouth",
        "southeastasia", "eastasia", "japaneast", "japanwest", "australiaeast", "australiasoutheast",
        "australiacentral", "australiacentral2", "brazilsouth", "southindia", "centralindia", "westindia",
        "canadacentral", "canadaeast", "uksouth", "ukwest", "koreacentral", "koreasouth", "francecentral",
        "francesouth", "uaecentral", "uaenorth", "southafricanorth", "southafricawest", "southafricaeast",
        "norwayeast", "norwaywest", "germanynorth", "germanywestcentral", "switzerlandnorth", "switzerlandwest",
        "polandcentral", "spaincentral", "qatarcentral", "chinanorth3", "chinaeast3", "indonesiacentral",
        "malaysiawest", "newzealandnorth", "taiwannorth", "israelcentral", "mexicocentral", "greececentral",
        "finlandcentral", "austriaeast", "belgiumcentral", "denmarkeast", "norwaysouth", "italynorth")]
    [string] $location,

    [Parameter(Mandatory = $false, Position = 5, HelpMessage = "Execute Infrastructure Deployment")]
    [switch] $deploy
)

# Function - New-RandomPassword
function New-RandomPassword {
    param (
        [Parameter(Mandatory)]
        [int] $length,
        [int] $amountOfNonAlphanumeric = 2
    )

    $nonAlphaNumericChars = '!@$'
    $nonAlphaNumericPart = -join ((Get-Random -Count $amountOfNonAlphanumeric -InputObject $nonAlphaNumericChars.ToCharArray()))

    $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
    $alphabetPart = -join ((Get-Random -Count ($length - $amountOfNonAlphanumeric) -InputObject $alphabet.ToCharArray()))

    $password = ($alphabetPart + $nonAlphaNumericPart).ToCharArray() | Sort-Object { Get-Random }

    return -join $password
}

# Check if Azure CLI is installed
if (-not (Get-Command az -ErrorAction SilentlyContinue)) {
    Write-Error "Azure CLI (az) is not installed. Please install it from https://aka.ms/azure-cli."
    exit 1
}

# Azure CLI Authentication
az login --output none --only-show-errors

# Change Azure Subscription
Write-Output "Updating Azure Subscription context to $subscriptionId"
az account set --subscription $subscriptionId --output none

# Check if user is logged in to Azure CLI
$azAccount = az ad signed-in-user show | ConvertFrom-Json
if (-not $azAccount) {
    Write-Error "You are not logged in to Azure CLI. Please run 'az login' to login."
    exit 1
}

# Retrieve the current Azure user account ID
$azUserAccountName = $azAccount.userPrincipalName
$azUserAccountGuid = $azAccount.id

# Generate a deployment GUID
$deployGuid = [guid]::NewGuid().ToString()

# Define location short codes
$locationShortCodes = @{
    "eastus"             = "eus"
    "eastus2"            = "eus2"
    "westus"             = "wus"
    "westus2"            = "wus2"
    "centralus"          = "cus"
    "northcentralus"     = "ncus"
    "southcentralus"     = "scus"
    "westcentralus"      = "wcus"
    "westus3"            = "wus3"
    "eastus3"            = "eus3"
    "northeurope"        = "neu"
    "westeurope"         = "weu"
    "swedencentral"      = "sec"
    "swedensouth"        = "ses"
    "southeastasia"      = "sea"
    "eastasia"           = "eas"
    "japaneast"          = "jpe"
    "japanwest"          = "jpw"
    "australiaeast"      = "aue"
    "australiasoutheast" = "ause"
    "australiacentral"   = "auc"
    "australiacentral2"  = "auc2"
    "brazilsouth"        = "brs"
    "southindia"         = "sai"
    "centralindia"       = "cin"
    "westindia"          = "win"
    "canadacentral"      = "cac"
    "canadaeast"         = "cae"
    "uksouth"            = "uks"
    "ukwest"             = "ukw"
    "koreacentral"       = "krc"
    "koreasouth"         = "krs"
    "francecentral"      = "frc"
    "francesouth"        = "frs"
    "uaecentral"         = "uaec"
    "uaenorth"           = "uaen"
    "southafricanorth"   = "safn"
    "southafricawest"    = "safw"
    "southafricaeast"    = "safe"
    "switzerlandnorth"   = "chn"
    "switzerlandwest"    = "chw"
    "germanynorth"       = "gen"
    "germanywestcentral" = "gewc"
    "norwayeast"         = "noe"
    "norwaywest"         = "now"
    "norwaysouth"        = "nos"
    "polandcentral"      = "plc"
    "spaincentral"       = "spc"
    "qatarcentral"       = "qtc"
    "chinanorth3"        = "chn3"
    "chinaeast3"         = "che3"
    "indonesiacentral"   = "idc"
    "malaysiawest"       = "myw"
    "newzealandnorth"    = "nzn"
    "taiwannorth"        = "twn"
    "israelcentral"      = "ilc"
    "mexicocentral"      = "mxc"
    "greececentral"      = "grc"
    "finlandcentral"     = "fic"
    "austriaeast"        = "ate"
    "belgiumcentral"     = "bec"
    "denmarkeast"        = "dke"
    "italynorth"         = "itn"
}

#
$vmUserPassword = New-RandomPassword -length 16

# Pre Flight Variable Validation
Write-Output `r "Pre Flight Variable Validation:" `r
Write-Output "Deployment Guid......: $deployGuid"
Write-Output "Location.............: $location"
Write-Output "Location Short Code..: $($locationShortCodes.$location)"
Write-Output "Environment..........: $environmentType"

# Deploy Bicep Template
if ($deploy) {
    $deployStartTime = Get-Date -Format 'HH:mm:ss'

    # Deploy Bicep Template
    $azDeployGuidLink = "`e]8;;https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id/%2Fsubscriptions%2F$subscriptionId%2Fproviders%2FMicrosoft.Resources%2Fdeployments%2Fiac-$deployGuid`e\iac-$deployGuid`e]8;;`e\"
    Write-Output `r "> Deployment [$azDeployGuidLink] Started at $deployStartTime"

    az deployment $targetScope create `
        --name iac-$deployGuid `
        --location $location `
        --template-file ./main.bicep `
        --parameters `
        customerName=$customerName `
        location=$location `
        locationShortCode=$($locationShortCodes.$location) `
        environmentType=$environmentType `
        deployedBy=$azUserAccountName `
        userAccountGuid=$azUserAccountGuid `
        vmUserPassword=$vmUserPassword `
        --confirm-with-what-if `
        --output none

    $deployEndTime = Get-Date -Format 'HH:mm:ss'
    $timeDifference = New-TimeSpan -Start $deployStartTime -End $deployEndTime ; $deploymentDuration = "{0:hh\:mm\:ss}" -f $timeDifference
    Write-Output `r "> Deployment [iac-$deployGuid] Started at $deployEndTime - Deployment Duration: $deploymentDuration"
}

Write-Output `r "Credentials"
Write-Output "VM Password: $vmUserPassword"

Infrastructure As Code
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
targetScope = 'subscription'

@description('Name of the customer')
param customerName string

@description('Location of the resources')
param location string = 'westeurope'

@description('Short code for the location')
param locationShortCode string = 'weu'

@description('Deployed By')
param deployedBy string

@description('User Account GUID')
param userAccountGuid string

@description('Environment type')
@allowed([
  'dev'
  'acc'
  'prd'
])
param environmentType string = 'dev'

@description('Tags for the resources')
param tags object = {
  customer: customerName
  environment: environmentType
  location: location
  deployedBy: deployedBy
}

@description('Resource group names')
param resourceGroupNames array = [
  'rg-${customerName}-hub-${environmentType}-${locationShortCode}'
  'rg-${customerName}-workload-${environmentType}-${locationShortCode}'
]

@description('Hub Virtual Network Name')
param hubVirtualNetworkName string = 'vnet-${customerName}-hub-${environmentType}-${locationShortCode}'

param workloadVirtualNetworkName string = 'vnet-${customerName}-workload-${environmentType}-${locationShortCode}'

@description('Azure Bastion Host Name')
param bastionHostName string = 'bas-${customerName}-hub-${environmentType}-${locationShortCode}'

@description('Azure Bastion Sku')
@allowed([
  'Developer'
  'Basic'
  'Premium'
  'Standard'
])
param bastionSku string = 'Standard'

@description('The Log Analytics Workspace Name')
param logAnalyticsWorkspaceName string = 'law-learning-linux-${locationShortCode}'

@description('The Data Collection Rule Name')
param linuxDataCollectionRuleName string = 'MSVMI-vminsights-linux'

@description('The name of the virtual machine')
param vmHostName string = 'vm-linux-01'

@description('The username for the virtual machine')
param vmUserName string = 'azureuser'

@description('The password for the virtual machine')
@secure()
param vmUserPassword string

// Azure Verified Modules
module createResourceGroup 'br/public:avm/res/resources/resource-group:0.4.1' = [
  for (resourceGroupName, i) in array(resourceGroupNames): {
    name: 'create-resource-group-${i}'
    scope: subscription()
    params: {
      name: resourceGroupName
      location: location
      tags: tags
    }
  }
]

// [AVM Module] - Hub Virtual Network
module createHubVirtualNetwork 'br/public:avm/res/network/virtual-network:0.5.4' = {
  name: 'create-hub-virtual-network'
  scope: resourceGroup(resourceGroupNames[0])
  params: {
    name: hubVirtualNetworkName
    location: location
    addressPrefixes: [
      '10.0.0.0/24'
    ]
    subnets: [
      {
        name: 'AzureBastionSubnet'
        addressPrefix: '10.0.0.0/28'
      }
    ]
    tags: tags
  }
  dependsOn: [
    createResourceGroup
  ]
}

// [AVM Module] - Azure Bastion Public IP
module createAzureBastionPublicIp 'br/public:avm/res/network/public-ip-address:0.8.0' = {
  name: 'create-azure-bastion-public-ip'
  scope: resourceGroup(resourceGroupNames[0])
  params: {
    name: 'pip-${bastionHostName}'
    location: location
    skuName: 'Standard'
    tags: tags
  }
  dependsOn: [
    createResourceGroup
  ]
}

// [AVM Module] - Azure Bastion Host
module createAzureBastionHost 'br/public:avm/res/network/bastion-host:0.6.1' = {
  name: 'create-azure-bastion-host'
  scope: resourceGroup(resourceGroupNames[0])
  params: {
    name: bastionHostName
    location: location
    skuName: bastionSku
    bastionSubnetPublicIpResourceId: createAzureBastionPublicIp.outputs.resourceId
    virtualNetworkResourceId: createHubVirtualNetwork.outputs.resourceId
    tags: tags
  }
  dependsOn: [
    createAzureBastionPublicIp
    createHubVirtualNetwork
  ]
}

// [AVM Module] - Workload
module createKeyVault 'br/public:avm/res/key-vault/vault:0.12.1' = {
  name: 'create-key-vault'
  scope: resourceGroup(resourceGroupNames[1])
  params: {
    name: 'kv-${customerName}-workload-${environmentType}-${locationShortCode}'
    location: location
    enableRbacAuthorization: true
    enablePurgeProtection: false
    enableSoftDelete: true
    softDeleteRetentionInDays: 90
    tags: tags
    roleAssignments: [
      {
        principalId: userAccountGuid
        roleDefinitionIdOrName: 'Key Vault Secrets Officer'
        principalType: 'User'
      }
    ]
  }
  dependsOn: [
    createResourceGroup
  ]
}

module createWorkloadNetworkSecurityGroup 'br/public:avm/res/network/network-security-group:0.5.0' = {
  name: 'create-workload-network-security-group'
  scope: resourceGroup(resourceGroupNames[1])
  params: {
    name: 'nsg-${customerName}-workload-${environmentType}-${locationShortCode}'
    location: location
    tags: tags
  }
  dependsOn: [
    createResourceGroup
  ]
}

module createWorkloadVirtualNetwork 'br/public:avm/res/network/virtual-network:0.5.4' = {
  name: 'create-workload-virtual-network'
  scope: resourceGroup(resourceGroupNames[1])
  params: {
    name: workloadVirtualNetworkName
    location: location
    addressPrefixes: [
      '10.1.0.0/24'
    ]
    subnets: [
      {
        name: 'subnet-${customerName}-workload-${environmentType}-${locationShortCode}'
        addressPrefix: '10.1.0.0/24'
        networkSecurityGroupResourceId: createWorkloadNetworkSecurityGroup.outputs.resourceId
      }
    ]
    peerings: [
      {
        name: 'peering-to-${workloadVirtualNetworkName}'
        remoteVirtualNetworkResourceId: createHubVirtualNetwork.outputs.resourceId
        remotePeeringEnabled: true
        allowVirtualNetworkAccess: true
        allowForwardedTraffic: true
        allowGatewayTransit: false
        useRemoteGateways: false
      }
    ]
    tags: tags
  }
  dependsOn: [
    createWorkloadNetworkSecurityGroup
  ]
}

module createLogAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
  name: 'create-log-analytics-workspace'
  scope: resourceGroup(resourceGroupNames[1])
  params: {
    name: logAnalyticsWorkspaceName
    location: location
    skuName: 'PerGB2018'
    dataRetention: 31
  }
  dependsOn: [
    createResourceGroup
  ]
}

module createLinuxDataCollectionRule 'br/public:avm/res/insights/data-collection-rule:0.5.0' = {
  name: 'create-linux-data-collection-rule'
  scope: resourceGroup(resourceGroupNames[1])
  params: {
    name: linuxDataCollectionRuleName
    location: location
    dataCollectionRuleProperties: {
      kind: 'Linux'
      description: 'Data collection rule for VM Insights.'
      dataFlows: [
        {
          streams: [
            'Microsoft-InsightsMetrics'
          ]
          destinations: [
            createLogAnalyticsWorkspace.outputs.name
          ]
        }
        {
          streams: [
            'Microsoft-ServiceMap'
          ]
          destinations: [
            createLogAnalyticsWorkspace.outputs.name
          ]
        }
      ]
      dataSources: {
        performanceCounters: [
          {
            streams: [
              'Microsoft-InsightsMetrics'
            ]
            samplingFrequencyInSeconds: 60
            counterSpecifiers: [
              '\\VmInsights\\DetailedMetrics'
            ]
            name: 'VMInsightsPerfCounters'
          }
        ]
        extensions: [
          {
            streams: [
              'Microsoft-ServiceMap'
            ]
            extensionName: 'DependencyAgent'
            extensionSettings: {}
            name: 'DependencyAgentDataSource'
          }
        ]
      }
      destinations: {
        logAnalytics: [
          {
            workspaceResourceId: createLogAnalyticsWorkspace.outputs.resourceId
            workspaceId: createLogAnalyticsWorkspace.outputs.logAnalyticsWorkspaceId
            name: createLogAnalyticsWorkspace.outputs.name
          }
        ]
      }
    }
  }
  dependsOn: [
    createLogAnalyticsWorkspace
  ]
}

module createVirtualMachine 'br/public:avm/res/compute/virtual-machine:0.12.2' = {
  name: 'create-virtual-machine'
  scope: resourceGroup(resourceGroupNames[1])
  params: {
    name: vmHostName
    adminUsername: vmUserName
    adminPassword: vmUserPassword
    location: location
    osType: 'Linux'
    vmSize: 'Standard_B2ms'
    zone: 0
    bootDiagnostics: true
    secureBootEnabled: true
    vTpmEnabled: true
    securityType: 'TrustedLaunch'
    managedIdentities: {
      systemAssigned: true
    }
    imageReference: {
      publisher: 'Canonical'
      offer: 'ubuntu-24_04-lts'
      sku: 'server'
      version: 'latest'
    }
    nicConfigurations: [
      {
        ipConfigurations: [
          {
            name: 'ipconfig01'
            pipConfiguration: {
              name: '${vmHostName}-pip-01'
            }
            subnetResourceId: createWorkloadVirtualNetwork.outputs.subnetResourceIds[0]
          }
        ]
        nicSuffix: '-nic-01'
        enableAcceleratedNetworking: false
      }
    ]
    osDisk: {
      caching: 'ReadWrite'
      diskSizeGB: 128
      managedDisk: {
        storageAccountType: 'Premium_LRS'
      }
    }
    extensionMonitoringAgentConfig: {
      dataCollectionRuleAssociations: [
        {
          dataCollectionRuleResourceId: createLinuxDataCollectionRule.outputs.resourceId
          name: 'SendMetricsToLAW'
        }
      ]
      enabled: true
      enableAutomaticUpgrade: true
    }
    tags: tags
  }
  dependsOn: [
    createWorkloadVirtualNetwork
  ]
}

module AzureADSSHExtension 'modules/compute/virtual-machines/extensions/azureADSSHExtension.bicep' = {
  name: 'configure-azure-ad-ssh-extension'
  scope: resourceGroup(resourceGroupNames[1])
  params: {
    vmName: vmHostName
    location: location
  }
  dependsOn: [
    createVirtualMachine
  ]
}

When you are ready to execute the deployment you can use the following PowerShell:

1
.\Invoke-AzDeployment.ps1 -targetscope sub -subscriptionId '' -customerName '' -environmentType ['dev' 'acc' 'prod'] -location 'westeurope' -deploy

Authentication

Bastion

From the Connect Page of the Virtual Machine from the dropdown you will see a list of options

NOTE
If you have Virtual Machine Administrator Login or Virtual Machine User Login the option will auto update to Microsoft Entra Id

Click Connect and you will be logged in with your Entra Id User Account

SSH Authentication

Using SSH Authentication from a terminal session or Cloud Shell, You can use the following commands

If you’re conencting using the Public IP address:

NOTE

Ensure you create an Inbound Security Rule in your Network Security Group (NSG) to allow TCP traffic on port 22 from your specific source IP or network. For enhanced security, restrict the source to your trusted IP range rather than allowing all traffic

1
az ssh vm --name vm-linux-01 --resource-group rg-bwc-workload-dev-weu

If you have a S/P2S connection, can can use the private IP address:

1
az ssh vm --name vm-linux-01 --resource-group rg-bwc-workload-dev-weu --prefer-private-ip

Wrapping Up

Entra ID authentication for Linux VMs in Azure offers a seamless, secure, and scalable way to manage access. By integrating with Azure’s RBAC model, it eliminates the need for local account provisioning, enhances security by reducing credential sprawl, and simplifies management across multiple VMs.

For organizations running Linux workloads in Azure, adopting Entra ID authentication is a best practice that strengthens security, streamlines operations, and minimizes credential management overhead.

Next Steps

This post concludes our mini-series on Linux authentication options in Azure. If you’re exploring other authentication methods, check out the previous posts:

To dive deeper, visit the official Microsoft documentation on Entra ID authentication for Linux VMs.

If you found this series helpful, feel free to share your thoughts or experiences in the comments!

Share with your network!

Built with Hugo - Theme Stack designed by Jimmy