适用于 PowerShell 5.1 及以上版本
Azure Bastion 是微软在 Azure 平台上提供的全托管 PaaS 服务,它通过 SSL/TLS 隧道为虚拟机提供安全的 RDP 和 SSH 连接,无需在虚拟机上暴露公共 IP 地址或开放入站端口。在企业混合云环境中,Bastion 充当了”跳板机”的安全替代方案,所有远程会话流量都经过加密且不经过公网,从根本上降低了暴力破解和端口扫描的风险。
传统的远程连接方式要求运维人员先通过 VPN 或跳板机接入内网,再使用 RDP/SSH 客户端连接目标虚拟机。这种方式不仅配置复杂,而且跳板机本身也面临安全威胁。Azure Bastion 将这一流程简化为浏览器直连或 CLI 命令行操作,同时与企业目录服务(Entra ID)深度集成,支持条件访问策略和多因素认证(MFA)。
本文将介绍如何通过 PowerShell 和 Azure CLI 管理 Azure Bastion 资源,包括部署 Bastion 主机、建立远程会话、查看连接会话日志以及批量审计 Bastion 配置。每个场景都提供可执行的代码示例和输出演示。
部署 Azure Bastion 主机
Azure Bastion 需要部署在专门的子网(AzureBastionSubnet)中,并且要求关联一个 Standard SKU 的公共 IP 地址。以下代码演示了如何通过 PowerShell 在现有虚拟网络中部署 Bastion 主机,包括前置条件的检查和资源创建的完整流程。
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
| $resourceGroup = 'rg-bastion-demo' $location = 'eastasia' $vnetName = 'vnet-demo' $bastionName = 'bastion-demo' $publicIpName = 'pip-bastion-demo'
$vnet = Get-AzVirtualNetwork -Name $vnetName -ResourceGroupName $resourceGroup
$bastionSubnet = $vnet.Subnets | Where-Object { $_.Name -eq 'AzureBastionSubnet' }
if (-not $bastionSubnet) { $subnetConfig = Add-AzVirtualNetworkSubnetConfig ` -Name 'AzureBastionSubnet' ` -AddressPrefix '10.0.1.0/26' ` -VirtualNetwork $vnet
$vnet | Set-AzVirtualNetwork | Out-Null Write-Host "已创建 AzureBastionSubnet 子网" } else { Write-Host "AzureBastionSubnet 子网已存在,跳过创建" }
$publicIp = New-AzPublicIpAddress ` -ResourceGroupName $resourceGroup ` -Name $publicIpName ` -Location $location ` -AllocationMethod Static ` -Sku Standard
Write-Host "公共 IP 已创建: $($publicIp.IpAddress)"
$bastion = New-AzBastion ` -ResourceGroupName $resourceGroup ` -Name $bastionName ` -PublicIpAddressRgName $resourceGroup ` -PublicIpAddressName $publicIpName ` -VirtualNetworkRgName $resourceGroup ` -VirtualNetworkName $vnetName
Write-Host "Bastion 主机部署完成" Write-Host "名称: $($bastion.Name)" Write-Host "SKU: $($bastion.Sku.Text)" Write-Host "状态: $($bastion.ProvisioningState)"
|
Azure Bastion 的子网名称必须是 AzureBastionSubnet,这是一个硬性要求,不能自定义。子网的最小地址空间为 /26(64 个地址),因为 Azure 会预留部分地址用于内部服务。Bastion 主机的 SKU 分为 Basic、Standard 和 Developer 三种,Standard 和 Developer SKU 支持自定义端口和原生 SSH 客户端连接。
执行结果示例:
1 2 3 4 5 6
| AzureBastionSubnet 子网已存在,跳过创建 公共 IP 已创建: 20.205.83.47 Bastion 主机部署完成 名称: bastion-demo SKU: Standard 状态: Succeeded
|
通过 Bastion 建立 SSH 远程会话
Standard 和 Developer SKU 的 Azure Bastion 支持原生 SSH 客户端连接,这意味着可以直接在 PowerShell 终端中发起 SSH 会话,无需打开浏览器。Azure CLI 提供了 az network bastion ssh 命令来实现这一功能,以下代码演示了如何封装调用逻辑并管理连接过程。
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
| function Connect-BastionSsh { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ResourceGroup,
[Parameter(Mandatory)] [string]$BastionName,
[Parameter(Mandatory)] [string]$VmName,
[ValidateSet('aad', 'password', 'ssh-key')] [string]$AuthType = 'ssh-key',
[string]$Username = 'azureuser',
[string]$SshKeyPath = "$env:USERPROFILE\.ssh\id_rsa" )
Write-Host "正在通过 Bastion 建立 SSH 连接..." Write-Host " Bastion: $BastionName" Write-Host " 目标 VM: $VmName" Write-Host " 认证方式: $AuthType"
$commonArgs = @( 'network', 'bastion', 'ssh', '--resource-group', $ResourceGroup, '--name', $BastionName, '--target-resource-id', "/subscriptions/$(Get-AzContext).Subscription.Id/resourceGroups/$ResourceGroup/providers/Microsoft.Compute/virtualMachines/$VmName", '--auth-type', $AuthType, '--username', $Username )
if ($AuthType -eq 'ssh-key') { $commonArgs += @('--ssh-key', $SshKeyPath) }
& az @commonArgs }
Connect-BastionSsh ` -ResourceGroup 'rg-bastion-demo' ` -BastionName 'bastion-demo' ` -VmName 'vm-linux-01' ` -AuthType 'ssh-key' ` -Username 'azureuser'
|
这段代码封装了 Connect-BastionSsh 函数,支持三种认证方式:Entra ID(AAD)认证、密码认证和 SSH 密钥认证。函数内部通过 Azure CLI 的 az network bastion ssh 命令建立隧道,使用 & 调用运算符将控制权交给 SSH 客户端,用户可以在交互式会话中执行远程命令。对于 Windows 虚拟机,只需将 ssh 替换为 rdp 即可通过 RDP 协议连接。
执行结果示例:
1 2 3 4 5 6 7 8 9 10
| 正在通过 Bastion 建立 SSH 连接... Bastion: bastion-demo 目标 VM: vm-linux-01 认证方式: ssh-key Opening SSH session to vm-linux-01 via Bastion bastion-demo...
Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-1019-azure x86_64)
Last login: Wed Dec 3 06:22:15 2025 from 10.0.1.4 azureuser@vm-linux-01:~$
|
查看 Bastion 会话日志与连接审计
Azure Bastion 的 Standard 和 Developer SKU 支持将远程会话日志导出到存储账户,这在安全合规场景中非常重要。以下代码演示了如何查询 Bastion 的活跃会话、导出会话日志以及生成连接审计报告。
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
| function Get-BastionSessionReport { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ResourceGroup,
[Parameter(Mandatory)] [string]$BastionName )
$bastion = Get-AzBastion -ResourceGroupName $ResourceGroup -Name $BastionName
$report = [ordered]@{ BastionName = $bastion.Name Location = $bastion.Location Sku = $bastion.Sku.Text Provisioning = $bastion.ProvisioningState EnableTunneling = $bastion.EnableTunneling EnableIpConnect = $bastion.EnableIpConnect EnableShareableLink = $bastion.EnableShareableLink DnsName = $bastion.DnsName }
Write-Host "`n===== Bastion 配置报告 =====" foreach ($key in $report.Keys) { Write-Host ("{0,-25} : {1}" -f $key, $report[$key]) }
$vnetId = $bastion.IpConfigurations.Subnet.Id -replace '/subnets/.*$', '' $vnetName = ($vnetId -split '/')[-1] $vnetRg = (($vnetId -split '/')[4])
$vnet = Get-AzVirtualNetwork -Name $vnetName -ResourceGroupName $vnetRg $bastionSubnet = $vnet.Subnets | Where-Object { $_.Name -eq 'AzureBastionSubnet' }
Write-Host "`n===== 网络配置 =====" Write-Host ("{0,-25} : {1}" -f '关联虚拟网络', $vnetName) Write-Host ("{0,-25} : {1}" -f '子网地址范围', $bastionSubnet.AddressPrefix)
$connectedVms = @() $allSubnets = $vnet.Subnets | Where-Object { $_.Name -ne 'AzureBastionSubnet' }
foreach ($subnet in $allSubnets) { $nicIds = $subnet.IpConfigurations.Id | ForEach-Object { ($_ -split '/ipConfigurations/')[0] } | Select-Object -Unique
foreach ($nicId in $nicIds) { $nicName = ($nicId -split '/')[-1] $nicRg = (($nicId -split '/')[4]) $nic = Get-AzNetworkInterface -Name $nicName -ResourceGroupName $nicRg -ErrorAction SilentlyContinue
if ($nic -and $nic.VirtualMachine) { $vmName = ($nic.VirtualMachine.Id -split '/')[-1] $vmRg = (($nic.VirtualMachine.Id -split '/')[4]) $vm = Get-AzVM -Name $vmName -ResourceGroupName $vmRg -ErrorAction SilentlyContinue
if ($vm) { $osType = if ($vm.StorageProfile.OSDisk.OSType -eq 'Windows') { 'Windows/RDP' } else { 'Linux/SSH' } $connectedVms += [PSCustomObject]@{ VmName = $vmName OsType = $osType Subnet = $subnet.Name PrivateIp = ($nic.IpConfigurations | Where-Object { $_.PrivateIpAddress }).PrivateIpAddress } } } } }
Write-Host "`n===== 可连接的虚拟机 =====" Write-Host ("{0,-20} {1,-15} {2,-20} {3}" -f 'VM 名称', '操作系统/协议', '子网', '私有 IP') Write-Host ('-' * 75)
foreach ($vm in $connectedVms) { Write-Host ("{0,-20} {1,-15} {2,-20} {3}" -f $vm.VmName, $vm.OsType, $vm.Subnet, $vm.PrivateIp) }
return $connectedVms }
Get-BastionSessionReport ` -ResourceGroup 'rg-bastion-demo' ` -BastionName 'bastion-demo'
|
这段代码通过 Get-AzBastion 获取 Bastion 主机的配置详情,然后遍历关联虚拟网络中除 AzureBastionSubnet 以外的所有子网,查找关联的虚拟机并列出其网络信息。通过审计报告可以快速了解哪些虚拟机可以通过 Bastion 访问,以及每台虚拟机的连接协议类型。
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ===== Bastion 配置报告 ===== BastionName : bastion-demo Location : eastasia Sku : Standard Provisioning : Succeeded EnableTunneling : True EnableIpConnect : True EnableShareableLink : False DnsName : bst-7a3b2c1d-0001.bastion.azure.com
===== 网络配置 ===== 关联虚拟网络 : vnet-demo 子网地址范围 : 10.0.1.0/26
===== 可连接的虚拟机 ===== VM 名称 操作系统/协议 子网 私有 IP --------------------------------------------------------------------------- vm-linux-01 Linux/SSH subnet-workload 10.0.0.4 vm-linux-02 Linux/SSH subnet-workload 10.0.0.5 vm-win-sql01 Windows/RDP subnet-database 10.0.2.4
|
批量审计多个 Bastion 实例的配置合规性
在大型企业环境中,可能有多个 Azure 订阅和资源组中部署了 Bastion 实例。安全团队需要定期审计所有 Bastion 实例的配置,确保符合企业安全基线。以下代码演示了如何批量检查所有 Bastion 实例的关键配置项。
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
| function Test-BastionCompliance { [CmdletBinding()] param( [string[]]$ResourceGroups )
$results = @()
if (-not $ResourceGroups) { $allBastions = Get-AzBastion } else { $allBastions = @() foreach ($rg in $ResourceGroups) { $allBastions += Get-AzBastion -ResourceGroupName $rg -ErrorAction SilentlyContinue } }
Write-Host "发现 $($allBastions.Count) 个 Bastion 实例,开始合规性检查...`n"
foreach ($bastion in $allBastions) { $compliance = [ordered]@{ Name = $bastion.Name ResourceGroup = $bastion.ResourceGroupName Location = $bastion.Location Sku = $bastion.Sku.Text }
$skuOk = $bastion.Sku.Text -in @('Standard', 'Developer') $compliance['SKU 检查'] = if ($skuOk) { 'PASS' } else { 'WARN: Basic SKU' }
$tunnelOk = $bastion.EnableTunneling -eq $true $compliance['原生客户端'] = if ($tunnelOk) { 'PASS' } else { 'WARN: 未启用' }
$ipConnectOk = $bastion.EnableIpConnect -eq $true $compliance['IP 连接'] = if ($ipConnectOk) { 'PASS' } else { 'WARN: 未启用' }
$shareOk = $bastion.EnableShareableLink -ne $true $compliance['共享链接'] = if ($shareOk) { 'PASS' } else { 'WARN: 已启用' }
$results += [PSCustomObject]$compliance }
Write-Host "===== Bastion 配置合规性报告 =====" Write-Host "扫描时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" Write-Host "实例总数: $($results.Count)`n"
foreach ($item in $results) { Write-Host "--- $($item.Name) ($($item.ResourceGroup)) ---" Write-Host " 位置: $($item.Location)" Write-Host " SKU: $($item.Sku)" Write-Host " SKU 检查: $($item.'SKU 检查')" Write-Host " 原生客户端: $($item.'原生客户端')" Write-Host " IP 连接: $($item.'IP 连接')" Write-Host " 共享链接: $($item.'共享链接')" Write-Host "" }
$passCount = ($results | Where-Object { $_.'SKU 检查' -eq 'PASS' -and $_.'原生客户端' -eq 'PASS' -and $_.'IP 连接' -eq 'PASS' -and $_.'共享链接' -eq 'PASS' }).Count
$total = $results.Count $complianceRate = if ($total -gt 0) { [math]::Round(($passCount / $total) * 100, 1) } else { 0 }
Write-Host "===== 汇总 =====" Write-Host "完全合规实例: $passCount / $total" Write-Host "合规率: $complianceRate%"
return $results }
Test-BastionCompliance -ResourceGroups @('rg-bastion-demo', 'rg-prod-network')
|
这段代码定义了 Test-BastionCompliance 函数,对每个 Bastion 实例执行四项合规性检查:SKU 级别是否符合要求、是否启用原生客户端隧道、是否启用 IP 直连以及是否禁用了可共享链接。函数输出详细的逐项检查结果和整体合规率统计,方便安全团队快速定位不合规的实例。
执行结果示例:
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
| 发现 2 个 Bastion 实例,开始合规性检查...
===== Bastion 配置合规性报告 ===== 扫描时间: 2025-12-03 10:30:00 实例总数: 2
--- bastion-demo (rg-bastion-demo) --- 位置: eastasia SKU: Standard SKU 检查: PASS 原生客户端: PASS IP 连接: PASS 共享链接: PASS
--- bastion-prod (rg-prod-network) --- 位置: eastasia SKU: Basic SKU 检查: WARN: Basic SKU 原生客户端: WARN: 未启用 IP 连接: WARN: 未启用 共享链接: PASS
===== 汇总 ===== 完全合规实例: 1 / 2 合规率: 50.0%
|
注意事项
- 子网命名不可更改:Azure Bastion 要求子网名称必须严格为
AzureBastionSubnet,且最小地址空间为 /26。部署后不能重命名子网,如需调整必须删除并重新创建 Bastion 资源。
- SKU 功能差异:Basic SKU 仅支持通过 Azure 门户浏览器连接,不支持原生 SSH 客户端、隧道模式和 IP 连接。建议生产环境使用 Standard 或 Developer SKU,以获得完整的 CLI 自动化能力。
- 部署时间较长:Bastion 主机的创建通常需要 5-10 分钟,不要在自动化脚本中假设它是即时可用的。建议在部署后通过轮询
ProvisioningState 确认资源就绪再发起连接。
- SSH 密钥路径兼容性:在 Windows 上默认 SSH 密钥路径为
$env:USERPROFILE\.ssh\id_rsa,在 Linux/macOS 上为 $env:HOME/.ssh/id_rsa。跨平台脚本中应使用 Join-Path 拼接路径,避免硬编码分隔符。
- 会话超时与断连:Bastion 的浏览器会话有非活动超时限制(通常为 10-20 分钟),原生 SSH 客户端连接则遵循 SSH 协议自身的
ServerAliveInterval 设置。建议在 SSH 配置中添加 ServerAliveInterval 60 保持连接活跃。
- 成本控制:Bastion 按小时计费(Standard SKU 约 $0.19/小时,各区域价格不同),无论是否有活跃会话。开发测试环境建议使用 Developer SKU(免费但限制并发连接数),或在不使用时删除 Bastion 资源以节省成本。