Azure Bastion: Azure PowerShell deployment script

This blog post will show you how you can deploy Azure Bastion and all associated resources via the use of an Azure PowerShell script.

Azure Bastion is an Azure PaaS service that you provision inside a virtual network (VNet), recommendable the HUB VNet. It allows you to securely connect to your Azure virtual machines (VMs) via RDP or SSH and this directly from the Azure portal over SSL. When you use Azure Bastion, your VMs do not need a Public IP Address (PIP)agent or special client software to be able to connect to them.

If you want to read some more about Azure Bastion, you can do so via the following Microsoft Docs link: Azure Bastion documentation


To automate the deployment process of this useful resource, I wrote the below Azure PowerShell script which does all of the following:

  • Check if the PowerShell window is running as Administrator (which is a requirement), otherwise the PowerShell window will be closed.
  • Create resource group for the Azure Bastion resources if it not already exists.
  • Create the AzureBastionSubnet with an associated network security group (NSG), if it not already exists. The NSG itself will contain all the required inbound and outbound security rules. If the AzureBastionSubnet exists but does not have an associated NSG, it will attach the newly created NSG.
  • Create a Public IP Address (PIP) for the Bastion host, if it not already exists.
  • Create the Bastion host (it can take up to 8 minutes for the Bastion host to be deployed), if it not already exists.
  • Set the log and metrics settings for the bastion resource if they 
    don’t exist.
  • Lock the Azure Bastion resource group with a CanNotDelete lock.

Prerequisites

  • A VNet
  • A Log Analytics workspace
  • At least Azure Az PowerShell module version 5.9.0 and Az.Network module version 4.7.0
  • In the script change all variables were needed to fit your needs
  • Before running the script logon with “Connect-AzAccount” and select the correct Azure Subscription

Azure PowerShell script

To use the script copy and save it as Build-AzureBastion.ps1 or download it from GitHub. First adjust all variables to your use and afterwards run the script with Administrator privileges from Windows Terminal, Windows PowerShell, or Visual Studio Code.

<pre class="wp-block-syntaxhighlighter-code"><#
.SYNOPSIS
 
A script used to setup and configure Azure Bastion within the HUB spoke VNet.
 
.DESCRIPTION
 
A script used to setup and configure Azure Bastion within the HUB spoke VNet. The script will create a resource group for the Azure Bastion resources (if it not already exists).
Then it will create the AzureBastionSubnet with and will associate a network security group (NSG), which holds all the required inbound and outbound security rules (if it not already exists). 
If the AzureBastionSubnet exists but does not have associated NSG, it will attach the created NSG. The script will also create a Public IP Address (PIP) for the Bastion host (if it not exists).
and create the Bastion host, which can take up to 5 minutes (if it not exists). Set the log and metrics settings for the bastion resource if they 
don't exist. And at the end it will lock the Azure Bastion resource group with a CanNotDelete lock.
 
.NOTES
 
Filename:       Build-AzureBastion.ps1
Created:        01/06/2021
Last modified:  02/07/2021
Author:         Wim Matthyssen
PowerShell:     Azure Cloud Shell or Azure PowerShell
Version:        Install latest Azure Powershell modules (at least Az version 5.9.0 and Az.Network version 4.7.0 is required)
Action:         Change variables were needed to fit your needs. 
Disclaimer:     This script is provided "As IS" with no warranties.
 
.EXAMPLE
 
.\Build-AzureBastion.ps1
 
.LINK
 
<a href="https://wmatthyssen.com/2021/06/02/azure-bastion-azure-powershell-deployment-script/">Azure Bastion: Azure PowerShell deployment&nbsp;script</a>
#>
 
## ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 
## Variables
 
$rgBastion = #<your Bastion rg here> The new Azure resource group in which the new Bastion resource will be created. Example: "rg-hub-myh-bastion"
$bastionName = #<your name here> The name of the new Bastion resource. Example: "bas-hub-myh"
$location = #<your region here> The used Azure public region. Example: "westeurope"
$rgNetworkSpoke = #<your VNet rg here> The Azure resource group in which your existing VNet is deployed. Example: "rg-hub-myh-network"
$vnetName = #<your VNet rg here> The existing VNet in which the Bastion resource will be created. Example: "vnet-hub-myh-weu"
$subnetNameBastion = "AzureBastionSubnet"
$subnetBastionAddress = #<your AzureBastionSubnet range here> The subnet must be at least /27 or larger. Example: "10.1.1.96/27"
$nsgNameBastion = #<your AzureBastionSubnet NSG name here> The name of the NSG associated with the AzureBastionSubnet. Example: "nsg-AzureBastionSubnet"
$bastionPipName = #<your Bastion PIP here> The public IP address of the Bastion resource. Example: "pip-bas-hub-myh"
$bastionPipAllocationMethod = "Static"
$bastionPipSku = "Standard"
$rgLogAnalyticsSpoke = #<your Log Analytics rg here> The Azure resource group your existing Log Analytics workspace is deployed. Example: "rg-hub-myh-management"
$logAnalyticsName = #<your Log Analytics workspace name here> The name of your existing Log Analytics workspace. Example: "law-hub-myh-01"
$bastionDiagnosticsName = "#<your Bastion Diagnostics settings name here> The name of the new diagnostic settings for Bastion. Example: "diag-bas-hub-myh"
 
$tagSpoke ="hub"
$tagCostCenter = "it"
$tagBusinessCriticality = "critical"
$tagPurpose = "bastion"
 
$global:currenttime= Set-PSBreakpoint -Variable currenttime -Mode Read -Action {$global:currenttime= Get-Date -UFormat "%A %m/%d/%Y %R"}
$foregroundColor1 = "Red"
$foregroundColor2 = "Yellow"
$writeEmptyLine = "`n"
$writeSeperatorSpaces = " - "
 
## ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 
## Prerequisites
 
## Check if running as Administrator (when not running from Cloud Shell), otherwise close the PowerShell window
 
if ($PSVersionTable.Platform -eq "Unix") {
    Write-Host ($writeEmptyLine + "# Running in Cloud Shell" + $writeSeperatorSpaces + $currentTime)
} else {
    $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
    $isAdministrator = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
 
    if ($isAdministrator -eq $false) {
    Write-Host ($writeEmptyLine + "# Please run PowerShell as Administrator" + $writeSeperatorSpaces + $currentTime +$writeEmptyLine)`
    -foregroundcolor $foregroundColor1
        Start-Sleep -s 4
    exit} else {
        ## Import Az module into the PowerShell session
        Import-Module Az
        Write-Host ($writeEmptyLine + "# Az module imported" + $writeSeperatorSpaces + $currentTime)`
        -foregroundcolor $foregroundColor1 $writeEmptyLine
    }
}
 
## Suppress breaking change warning messages
 
Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings "true"
 
## ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 
## Deployment started
 
Write-Host ($writeEmptyLine + "# Deployment started" + $writeSeperatorSpaces + $currentTime)`
-foregroundcolor $foregroundColor1 $writeEmptyLine
 
## ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 
# Create a resource group for the Azure Bastion resources if it not exists
 
try {
    Get-AzResourceGroup -Name $rgBastion -ErrorAction Stop
} catch {
    New-AzResourceGroup -Name $rgBastion -Location $location `
    -Tag @{env=$tagSpoke;costCenter=$tagCostCenter;businessCriticality=$tagBusinessCriticality;purpose=$tagPurpose;vnet=$vnetName} -Force
}
 
Write-Host ($writeEmptyLine + "# Resource group $rgBastion available" + $writeSeperatorSpaces + $currentTime)`
-foregroundcolor $foregroundColor2 $writeEmptyLine
 
## ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 
## Create the AzureBastionSubnet with the network security group (with the required inbound and outbound security rules) if it not exists
 
## Inbound rules
 
$inboundRule1 = New-AzNetworkSecurityRuleConfig -Name "Allow_TCP_443_Internet" -Description "Allow_TCP_443_Internet" `
-Access Allow -Protocol TCP -Direction Inbound -Priority 100 -SourceAddressPrefix Internet -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 443
 
$inboundRule2 = New-AzNetworkSecurityRuleConfig -Name "Allow_TCP_443_GatewayManager" -Description "Allow_TCP_443_GatewayManager" `
-Access Allow -Protocol TCP -Direction Inbound -Priority 110 -SourceAddressPrefix GatewayManager -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 443
 
$inboundRule3 = New-AzNetworkSecurityRuleConfig -Name "Allow_TCP_4443_GatewayManager" -Description "Allow_TCP_4443_GatewayManager" `
-Access Allow -Protocol TCP -Direction Inbound -Priority 120 -SourceAddressPrefix GatewayManager -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 4443
 
$inboundRule4 = New-AzNetworkSecurityRuleConfig -Name "Allow_TCP_443_AzureLoadBalancer" -Description "Allow_TCP_443_AzureLoadBalancer" `
-Access Allow -Protocol TCP -Direction Inbound -Priority 130 -SourceAddressPrefix AzureLoadBalancer -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 443
 
## The rule below denies all other inbound virtual network access
 
$inboundRule5 = New-AzNetworkSecurityRuleConfig -Name "Deny_any_other_traffic" -Description "Deny_any_other_traffic" `
-Access Deny -Protocol * -Direction Inbound -Priority 900 -SourceAddressPrefix * -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange *
 
## Outbound rules
 
$outboundRule1 = New-AzNetworkSecurityRuleConfig -Name "Allow_TCP_3389_VirtualNetwork" -Description "Allow_TCP_3389_VirtualNetwork" `
-Access Allow -Protocol TCP -Direction Outbound -Priority 100 -SourceAddressPrefix * -SourcePortRange * -DestinationAddressPrefix VirtualNetwork -DestinationPortRange 3389
 
$outboundRule2 = New-AzNetworkSecurityRuleConfig -Name "Allow_TCP_22_VirtualNetwork" -Description "Allow_TCP_22_VirtualNetwork" `
-Access Allow -Protocol TCP -Direction Outbound -Priority 110 -SourceAddressPrefix * -SourcePortRange * -DestinationAddressPrefix VirtualNetwork -DestinationPortRange 22
 
$outboundRule3 = New-AzNetworkSecurityRuleConfig -Name "Allow_TCP_443_AzureCloud" -Description "Allow_TCP_443_AzureCloud" `
-Access Allow -Protocol TCP -Direction Outbound -Priority 120 -SourceAddressPrefix * -SourcePortRange * -DestinationAddressPrefix AzureCloud -DestinationPortRange 443
 
## Create the NSG if it not exists
 
try {
    Get-AzNetworkSecurityGroup -Name $nsgNameBastion -ResourceGroupName $rgNetworkSpoke -ErrorAction Stop
} catch {
    New-AzNetworkSecurityGroup -Name $nsgNameBastion -ResourceGroupName $rgNetworkSpoke -Location $location `
    -SecurityRules $inboundRule1,$inboundRule2,$inboundRule3,$inboundRule4,$inboundRule5,$outboundRule1,$outboundRule2,$outboundRule3 `
    -Tag @{env=$tagSpoke;costCenter=$tagCostCenter;businessCriticality=$tagBusinessCriticality} -Force
}
 
Write-Host ($writeEmptyLine + "# NSG $nsgNameBastion available" + $writeSeperatorSpaces + $currentTime)`
-foregroundcolor $foregroundColor2 $writeEmptyLine
 
## Create the AzureBastionSubnet if it not exists
 
try {
    $vnet = Get-AzVirtualNetwork -Name $vnetName -ResourceGroupname $rgNetworkSpoke
 
    $subnet = Get-AzVirtualNetworkSubnetConfig -Name $subnetNameBastion -VirtualNetwork $vnet -ErrorAction Stop
} catch {
    $subnet = Add-AzVirtualNetworkSubnetConfig -Name $subnetNameBastion -VirtualNetwork $vnet -AddressPrefix $subnetBastionAddress
 
    $vnet | Set-AzVirtualNetwork
}
 
## Attach the NSG to the AzureBastionSubnet (also if the AzureBastionSubnet exsists and misses and NSG)
 
# $subnet = Get-AzVirtualNetworkSubnetConfig -Name $subnetNameBastion -VirtualNetwork $vnet
 
$subnet = Get-AzVirtualNetworkSubnetConfig -Name $subnetNameBastion -VirtualNetwork $vnet
$nsg = Get-AzNetworkSecurityGroup -Name $nsgNameBastion -ResourceGroupName $rgNetworkSpoke
$subnet.NetworkSecurityGroup = $nsg
$vnet | Set-AzVirtualNetwork
 
Write-Host ($writeEmptyLine + "# Subnet $subnetNameBastion available with attached NSG $nsgNameBastion" + $writeSeperatorSpaces + $currentTime)`
-foregroundcolor $foregroundColor2 $writeEmptyLine
 
## ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 
## Create a Public IP Address (PIP) for the Bastion host if it not exists
 
try {
    $bastionPip = Get-AzPublicIpAddress -Name $bastionPipName -ResourceGroupName $rgBastion -ErrorAction Stop
} catch {
    $bastionPip = New-AzPublicIpAddress -Name $bastionPipName -ResourceGroupName $rgBastion -Location $location -AllocationMethod $bastionPipAllocationMethod -Sku $bastionPipSku `
    -Tag @{env=$tagSpoke;costCenter=$tagCostCenter;businessCriticality=$tagBusinessCriticality;purpose=$tagPurpose;vnet=$vnetName} -Force
}
 
Write-Host ($writeEmptyLine + "# Pip " + $bastionPipName + " available" + $writeSeperatorSpaces + $currentTime)`
-foregroundcolor $foregroundColor2 $writeEmptyLine
 
## ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 
## Create the Bastion host (it takses around 5 minutes for the Bastion host to be deployed) if it not exists
 
try {
    $bastion = Get-AzBastion -Name $bastionName -ResourceGroupName $rgBastion -ErrorAction Stop
} catch {
    Write-Host ($writeEmptyLine + "# Bastion host deployment started, this can take up to 5 minutes" + $writeSeperatorSpaces + $currentTime)`
    -foregroundcolor $foregroundColor2 $writeEmptyLine
 
    $vnet = Get-AzVirtualNetwork -Name $vnetName -ResourceGroupname $rgNetworkSpoke
 
    $bastion = New-AzBastion -ResourceGroupName $rgBastion -Name $bastionName -PublicIpAddress $bastionPip -VirtualNetwork $vnet `
    -Tag @{env=$tagSpoke;costCenter=$tagCostCenter;businessCriticality=$tagBusinessCriticality;purpose=$tagPurpose;vnet=$vnetName}
}
 
Write-Host ($writeEmptyLine + "# Bastion host $bastionName available" + $writeSeperatorSpaces + $currentTime)`
-foregroundcolor $foregroundColor2 $writeEmptyLine
 
## ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 
## Set the log and metrics settings for the bastion resource if they don't exist
 
try {
    Get-AzDiagnosticSetting -Name $bastionDiagnosticsName -ResourceId ($bastion.Id) -ErrorAction Stop
} catch {
    $workSpace = Get-AzOperationalInsightsWorkspace -Name $logAnalyticsName -ResourceGroupName $rgLogAnalyticsSpoke
     
    Set-AzDiagnosticSetting -Name $bastionDiagnosticsName -ResourceId ($bastion.Id) -Category BastionAuditLogs -MetricCategory AllMetrics -Enabled $true `
    -WorkspaceId ($workSpace.ResourceId)
}
 
Write-Host ($writeEmptyLine + "# Diagnostic settings set" + $writeSeperatorSpaces + $currentTime)`
-foregroundcolor $foregroundColor2 $writeEmptyLine
 
## ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 
## Lock the Azure Bastion resource group with a CanNotDelete lock
 
$lock = Get-AzResourceLock -ResourceGroupName $rgBastion
 
if ($null -eq $lock){
    New-AzResourceLock -LockName DoNotDeleteLock -LockLevel CanNotDelete -ResourceGroupName $rgBastion -LockNotes "Prevent $rgBastion from deletion" -Force
    }
 
Write-Host ($writeEmptyLine + "# Resource group $rgBastion locked" + $writeSeperatorSpaces + $currentTime)`
-foregroundcolor $foregroundColor2 $writeEmptyLine
 
## ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 
## Deployment completed
 
Write-Host ($writeEmptyLine + "# Deployment completed" + $writeSeperatorSpaces + $currentTime)`
-foregroundcolor $foregroundColor1 $writeEmptyLine
 
## --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------</pre>

I hope this Azure PowerShell script is useful for you and provides you with a good starting point to use Azure Bastion into your Azure environment. If you have any questions or recommendations about it, feel free to contact me through my Twitter handle or to leave a comment.

This blog is part of Azure Week. Check it out for more great content!

About the Author:

Wim is an Azure Technical Advisor, who has more than fifteen years of experience with Microsoft Technologies. As a Microsoft Certified Trainer (MCT), his strength is to assist companies in the transformation of their business to the Cloud by implementing the latest features, services and solutions. Currently his main focus is on the Microsoft Hybrid Cloud Platform and especially on Microsoft Azure and the Azure hybrid services.

Wim is also a Microsoft MVP in the Azure category and a founding board member of the MC2MC user group.

As a passionate community member he regularly writes blogs and speaks about his daily experiences with Azure.

Reference:

Matthyssen, W. (2021Azure Bastion: Azure PowerShell deployment script. Available at: https://wmatthyssen.com/2021/06/02/azure-bastion-azure-powershell-deployment-script/ [Accessed: 8th July 2021].

Share this on...

Rate this Post:

Share: