Azure Linux SSH Authentication: Bastion

Access Linux Machines with Azure Bastion and Key Vault

What is Azure Bastion?

Azure Bastion is a fully managed service that provides secure and seamless RDP/SSH connectivity to virtual machines directly through the Azure portal, without requiring public IP addresses. By eliminating direct exposure to the internet, Azure Bastion significantly enhances security by reducing the attack surface and preventing common threats like brute-force attacks.

Why Use Azure Bastion?

Using Azure Bastion for SSH access to Linux VMs offers several advantages:

  • Eliminates Public IPs: No need to expose SSH ports to the internet.
  • Integrated Azure Portal Access: Securely connect to VMs via the Azure portal.
  • No Client Software Needed: SSH directly from your browser without additional software.
  • Enhanced Security: Supports Azure policies and private networking.

Azure Bastion SKUs

Azure Bastion comes in four SKUs, each offering different capabilities:
For more details, Check the official Microsoft Learn Docs: Bastion Overview Skus

FeatureDeveloper SKUBasic SKUStandard SKUPremium SKU
Connect to target VMs in same virtual networkYesYesYesYes
Connect to target VMs in peered virtual networksNoYesYesYes
Support for concurrent connectionsNoYesYesYes
Access Linux VM Private Keys in Azure Key Vault (AKV)YesYesYesYes
Connect to Linux VM using SSHYesYesYesYes
Connect to Windows VM using RDPYesYesYesYes
Connect to Linux VM using RDPNoNoYesYes
Connect to Windows VM using SSHNoNoYesYes
Specify custom inbound portNoNoYesYes
Connect to VMs using Azure CLINoNoYesYes
Host scalingNoNoYesYes
Upload or download filesNoNoYesYes
Kerberos authenticationNoYesYesYes
Shareable linkNoNoYesYes
Connect to VMs via IP addressNoNoYesYes
VM audio outputYesYesYesYes
Disable copy/paste (web-based clients)NoNoYesYes
Session recordingNoNoNoYes
Private-only deploymentNoNoNoYes

Bastion Pricing

For more details, Check the official Microsoft Docs: Bastion Pricing

Bastion SkuCost Per HourCost Per Month (730)
DevelopersFreeFree
Basic$0.19$138
Standard$0.29$211
Premium$0.45$328

Additional Bastion Unit Pricing

Bastion SkuCost Per HourCost Per Month (730)
Standard *$0.14$102
Premium *$0.22$160

* Azure Bastion Standard and Azure Bastion Premium start with 2 instances as part of base pricing.

Bastion Out Bound Traffic Pricing

Data TransferPricing
First 5GB/MonthFree
5Gb - 10TB/Month$0.087 per GB
Next 40TB (10Tb - 50TB)/Month$0.083 per GB
Next 100TB (50-150TB)/Month$0.07 per GB
Over 150TB/MonthContact Microsoft Support

Why Store SSH Credentials in Key Vault?

Storing SSH keys in Key Vault enhances security by managing and protecting sensitive credentials in a central, secure location. Benefits include:

  • Controlled Access: Use RBAC to restrict who can access SSH keys.
  • Automated Secrets Rotation: Regularly update credentials without manual intervention.
  • Integration with Azure Services: Use Key Vault with Azure Bastion for seamless authentication.
  • Auditing & Monitoring: Track access and usage of stored credentials.

Infrastructure As Code Deployment

To ensure a repeatable and consistent deployment, we use Infrastructure as Code (IaC). Below is a placeholder for deploying Azure Bastion, Key Vault, and associated resources using Bicep.

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
<#
.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
)

# 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"
}

# Create SSH Key Value Pair
Write-Output `r "Creating SSH Key Pair" `r
ssh-keygen -t rsa -b 2048 -f $PWD/azure-ssh-key -q -N ""
$sshKeyPublic = Get-Content -Path "$PWD/azure-ssh-key.pub"
$sshKeyPrivate = Get-Content -Path "$PWD/azure-ssh-key"

# 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"
Write-Output "Customer Name........: $customerName"

# 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 `
        sshPublicKey="$sshKeyPublic" `
        --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"
}

# Upload SSH Key to Azure Key Vault
Write-Output `r "Uploading Private Key to Key Vault: kv-$customerName-workload-$environmentType-$($locationShortCodes.$location)"
az keyvault secret set --vault-name kv-$customerName-workload-$environmentType-$($locationShortCodes.$location) --name "ssh-private-key" --file ./azure-ssh-key --encoding 'ascii' --output none

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
targetScope = 'subscription'

@description('SSH Public Key')
param sshPublicKey string

@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')
var tags = {
  customer: customerName
  environment: environmentType
  location: location
  deployedBy: deployedBy
}

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

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

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

@description('Azure Bastion Host Name')
var bastionHostName = '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'

// Azure Verified Modules
module createResourceGroup 'br/public:avm/res/resources/resource-group:0.4.0' = [
  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.2' = {
  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.0' = {
  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.11.3' = {
  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.2' = {
  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.9.0' = {
  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.4.2' = {
  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.8.0' = {
  name: 'create-virtual-machine'
  scope: resourceGroup(resourceGroupNames[1])
  params: {
    name: vmHostName
    adminUsername: vmUserName
    //adminPassword: vmUserPassword
    disablePasswordAuthentication: true
    location: location
    osType: 'Linux'
    vmSize: 'Standard_B2ms'
    zone: 0
    bootDiagnostics: true
    secureBootEnabled: true
    vTpmEnabled: true
    securityType: 'TrustedLaunch'
    imageReference: {
      publisher: 'Canonical'
      offer: 'ubuntu-24_04-lts'
      sku: 'server'
      version: 'latest'
    }
    publicKeys: [
      {
        path: '/home/${vmUserName}/.ssh/authorized_keys'
        keyData: sshPublicKey
      }
    ]
    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
  ]
}

Deployment Note

Please ensure Azure CLI and Bicep are installed.

AzCLI: winget install --exact --id Microsoft.AzureCLI
AzCLI Bicep: az bicep upgrade

Once you’ve saved the files locally you can execute the deployment using the following command:

1
.\Invoke-AzDeployment.ps1 -targetscope [ tenant | mgmt | sub ] -subscriptionId 00000000-0000-0000-0000-000000000000 -customerName <customerName> -environmentType [dev | acc | prod] -location <location>  -deploy

Resource Deployment

Resource Group: Hub
Virtual Network
Bastion Host
Bastion Public IP
Resource Group: Spoke
Virtual Network
Key Vault
Log Analytics
Data Collection Rule
Network Security Group
Virtual Machine
Virtual Disk (OS)
Network Interface
Public IP Address

Private Key Upload Notes

During the deploynent, We utilise ssh-keygen to generate the key value pair, using RSA and a key legnth of 2048.

1
ssh-keygen -t rsa -b 2048 -f $PWD/azure-ssh-key -q -N ""

This will create the key value pair as:

  • azure-ssh-key.pub (Public Key)
  • azure-ssh-key (Private Key)

The Public Key, will be passed into the Virtual Machine during the creation

Lines 316 - 321

1
2
3
4
5
6
    publicKeys: [
      {
        path: '/home/${vmUserName}/.ssh/authorized_keys'
        keyData: sshPublicKey
      }
    ]

If you wanted to manually update the key at a later date, you can use the following Azure CLI Command, and for the Public Key, I recently wrote a post on how you can update the Public Key using the RunShellScript Lost SSH Keys

1
az keyvault secret set --vault-name kv-$customerName-workload-$environmentType-$($locationShortCodes.$location) --name "ssh-private-key" --file './azure-ssh-key' --encoding ascii --output none

Key Vault Role Based Access Control

During the deployment, The Azure CLI User will be given Key Vault Secret Officer.

If you require Read Only Access to the secrets you can use:

Key Vaylt Secret User: 4633458b-17de-408a-b874-0445c86b69e6

If you require Read/Write Access to the secrets you can use:

Key Vault Secret Officer: b86a8fe4-44ce-4948-aee5-eccb2c155cd7

Accessing the Machine via Azure Portal

Deployment Note

For this example default user is: azureuser

During the deploying of the Infrastcture, the Private Key is Uploaded into the Key Vault. Once the deployment has completed, head to the resource group: 'rg-x-${customerName}-workload-${environmentType}-${locationShortCode}'

From the Virtual Machine Menu Blade, Under Connect choose Bastion

If everything has worked, you should get a sucessful authentication and find yourself connected to a Ubuntu Machine,

Wrap Up

Azure Bastion simplifies and secures SSH access to Linux VMs by eliminating public exposure. When combined with Azure Key Vault, it further enhances security by centrally managing and protecting SSH credentials. Leveraging Infrastructure as Code (IaC) ensures consistent, scalable, and repeatable deployments, making your cloud environment more robust and secure.

Share with your network!

Built with Hugo - Theme Stack designed by Jimmy