适用于 PowerShell 7.0 及以上版本
在云环境中,服务之间的认证一直是个令人头疼的问题。传统做法是创建服务主体(Service Principal),然后把客户端 ID 和密钥硬编码在配置文件或环境变量里。这些密码一旦泄露,攻击者就能以该身份访问你的 Azure 资源。更糟糕的是,密码有有效期,过期了服务就会中断,而定期轮换密码本身就是一项繁琐的运维负担。
Azure 托管标识(Managed Identity)从根本上解决了这个问题。它为 Azure 资源自动提供一个由 Azure AD(现称 Microsoft Entra ID)管理的标识,应用程序无需管理任何凭据就能获取 OAuth 2.0 令牌来访问支持 Azure AD 认证的服务。系统分配的托管标识与资源生命周期绑定,资源删除时标识自动回收;用户分配的托管标识则可以共享给多个资源使用。
本文将演示如何通过 PowerShell 完成托管标识的全生命周期管理:从启用标识、分配权限,到实际访问 Key Vault、Storage 和 SQL Database,最后展示虚拟机和容器中的令牌获取方式。让你从”到处藏密码”的窘境中彻底解脱出来。
托管标识配置与权限分配 配置托管标识是”无密码”运维的第一步。下面的脚本展示了如何为虚拟机启用系统分配标识和用户分配标识,并通过 RBAC 角色分配授予相应的 Azure 资源访问权限。系统分配标识与资源一一绑定,适合单一资源的场景;用户分配标识可以复用,适合多个资源需要共享同一权限集的场景。
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 Connect-AzAccount -Subscription 'production-subscription' $resourceGroup = 'rg-managed-identity-demo' $location = 'eastasia' $vmName = 'vm-demo-01' $vm = Get-AzVM -ResourceGroupName $resourceGroup -Name $vmName Update-AzVM -ResourceGroupName $resourceGroup -VM $vm ` -IdentityType SystemAssigned Write-Host '系统分配标识已启用' -ForegroundColor Green$vm = Get-AzVM -ResourceGroupName $resourceGroup -Name $vmName $principalId = $vm .Identity.PrincipalIdWrite-Host "主体 ID(PrincipalId): $principalId " $identityName = 'id-shared-demo' $userIdentity = New-AzUserAssignedIdentity ` -ResourceGroupName $resourceGroup ` -Name $identityName ` -Location $location Write-Host "用户分配标识已创建: $ ($userIdentity .Name)" $vm = Get-AzVM -ResourceGroupName $resourceGroup -Name $vmName Update-AzVM -ResourceGroupName $resourceGroup -VM $vm ` -IdentityType UserAssigned ` -IdentityId $userIdentity .Id $keyVault = Get-AzKeyVault -ResourceGroupName $resourceGroup -VaultName 'kv-demo-01' New-AzRoleAssignment ` -ObjectId $principalId ` -RoleDefinitionName 'Key Vault Secrets User' ` -Scope $keyVault .ResourceId $storageAccount = Get-AzStorageAccount -ResourceGroupName $resourceGroup ` -Name 'stademo01' New-AzRoleAssignment ` -ObjectId $userIdentity .PrincipalId ` -RoleDefinitionName 'Storage Blob Data Reader' ` -Scope $storageAccount .Id Write-Host 'RBAC 角色分配完成' -ForegroundColor Green
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 系统分配标识已启用 主体 ID(PrincipalId): a1b2c3d4-e5f6-7890 -abcd-ef1234567890 用户分配标识已创建: id-shared-demo RoleAssignmentName : ra-keyvault-secrets-user RoleAssignmentId : /subscriptions/ .../providers/ Microsoft.Authorization/roleAssignments/ ... Scope : /subscriptions/ .../resourceGroups/ rg-managed-identity-demo/providers/ Microsoft.KeyVault/vaults/ kv-demo-01 DisplayName : vm-demo-01 RoleDefinitionName : Key Vault Secrets User RoleAssignmentName : ra-storage-blob-reader RoleAssignmentId : /subscriptions/ .../providers/ Microsoft.Authorization/roleAssignments/ ... Scope : /subscriptions/ .../resourceGroups/ rg-managed-identity-demo/providers/ Microsoft.Storage/storageAccounts/ stademo01 DisplayName : id-shared-demo RoleDefinitionName : Storage Blob Data Reader RBAC 角色分配完成
无密码访问 Azure 服务 配置好标识和权限后,最令人兴奋的时刻来了——代码中不再需要任何密码。下面的脚本演示如何使用托管标识令牌直接访问 Key Vault 读取密钥、访问 Storage Blob 下载文件,以及连接 Azure SQL 数据库。整个过程完全无凭据,令牌的获取、缓存和刷新由 Azure SDK 自动处理。
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 $vaultName = 'kv-demo-01' Connect-AzAccount -Identity $secret = Get-AzKeyVaultSecret -VaultName $vaultName -Name 'DatabaseConnectionString' $secretValue = $secret .SecretValue | ConvertFrom-SecureString -AsPlainText Write-Host "从 Key Vault 读取到数据库连接字符串(长度: $ ($secretValue .Length) 字符)" $secrets = Get-AzKeyVaultSecret -VaultName $vaultName | Select-Object Name, ContentType, Updated $secrets | Format-Table -AutoSize $storageAccountName = 'stademo01' $containerName = 'reports' $identity = Get-AzUserAssignedIdentity ` -ResourceGroupName 'rg-managed-identity-demo' ` -Name 'id-shared-demo' Connect-AzAccount -Identity -AccountId $identity .ClientId$ctx = New-AzStorageContext -StorageAccountName $storageAccountName -UseConnectedAccount $blobs = Get-AzStorageBlob -Container $containerName -Context $ctx | Select-Object Name, Length, LastModified $blobs | Format-Table -AutoSize $latestBlob = Get-AzStorageBlob -Container $containerName -Context $ctx | Sort-Object LastModified -Descending | Select-Object -First 1 $downloadPath = Join-Path $env:TEMP $latestBlob .Name$latestBlob | Get-AzStorageBlobContent -Destination $downloadPath -Context $ctx -Force Write-Host "已下载: $downloadPath ($ ($latestBlob .Length) 字节)" $serverName = 'sql-demo-01' $databaseName = 'appdb' $token = (Get-AzAccessToken -ResourceUrl 'https://database.windows.net/' ).Token$connectionString = "Server=tcp:$serverName .database.windows.net,1433;" + "Database=$databaseName ;" + "Authentication=Active Directory Default;" $connection = New-Object System.Data.SqlClient.SqlConnection $connectionString $connection .AccessToken = $token $connection .Open()$command = $connection .CreateCommand()$command .CommandText = 'SELECT TOP 5 name, create_date FROM sys.tables' $reader = $command .ExecuteReader()while ($reader .Read()) { Write-Host "$ ($reader ['name']) $ ($reader ['create_date'])" } $connection .Close()Write-Host 'SQL 查询完成(无密码连接)' -ForegroundColor Green
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 从 Key Vault 读取到数据库连接字符串(长度: 156 字符) Name ContentType Updated ---- ----------- ------- DatabaseConnectionString 2026/3/1 上午 10:30:00 StorageAccountKey 2026/2/28 下午 3:15:00 ThirdPartyApiKey 2026/2/25 上午 9:00:00 Name Length LastModified ---- ------ ------------ report-202601 .pdf 245678 2026/3/1 上午 8:00:00 report-202602 .pdf 312456 2026/3/1 上午 8:05:00 data-export.csv 89123 2026/2/28 下午 4:30:00 已下载: /tmp/report-202602 .pdf (312456 字节) Users 2026-01 -15 10:00:00 Orders 2026-01 -15 10:01:00 Products 2026-01 -15 10:02:00 AuditLog 2026-02 -01 09:00:00 Sessions 2026-02 -10 14:30:00 SQL 查询完成(无密码连接)
从虚拟机和容器中使用托管标识 在生产环境中,托管标识最常见的使用场景是从虚拟机或容器内部获取令牌。Azure 虚拟机通过 IMDS(Instance Metadata Service)端点提供本地令牌服务,而 Azure 容器实例(ACI)和 AKS Pod 也支持类似的机制。下面的脚本可以在虚拟机或容器内运行,自动获取访问令牌并执行运维操作。
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 function Get-ManagedIdentityToken { param ( [Parameter (Mandatory )] [string ]$Resource , [string ]$UserAssignedClientId ) $imdsUrl = 'http://169.254.169.254/metadata/identity/oauth2/token' $params = @ { Uri = $imdsUrl Method = 'GET' Headers = @ { Metadata = 'true' } Body = @ { 'api-version' = '2018-02-01' ; resource = $Resource } UseBasicParsing = $true } if ($UserAssignedClientId ) { $params .Body['client_id' ] = $UserAssignedClientId } try { $response = Invoke-RestMethod @params Write-Host "令牌获取成功,有效期至: $ ($response .expires_on)" -ForegroundColor Green return $response .access_token } catch { Write-Error "令牌获取失败: $ ($_ .Exception.Message)" return $null } } $kvToken = Get-ManagedIdentityToken -Resource 'https://vault.azure.net' $vaultName = 'kv-demo-01' $secretName = 'DatabaseConnectionString' $uri = "https://$vaultName .vault.azure.net/secrets/$secretName " + '?api-version=7.4' $headers = @ { Authorization = "Bearer $kvToken " ContentType = 'application/json' } $result = Invoke-RestMethod -Uri $uri -Headers $headers -Method GetWrite-Host "通过 REST API 读取密钥成功: $ ($result .id)" function Get-ContainerManagedToken { param ( [Parameter (Mandatory )] [string ]$Resource ) $msiEndpoint = $env:MSI_ENDPOINT $msiSecret = $env:MSI_SECRET if ($msiEndpoint -and $msiSecret ) { $params = @ { Uri = "$msiEndpoint `?resource=$Resource &api-version=2017-09-01" Method = 'GET' Headers = @ { Secret = $msiSecret } UseBasicParsing = $true } $response = Invoke-RestMethod @params return $response .access_token } return Get-ManagedIdentityToken -Resource $Resource } function Backup-KeyVaultSecrets { param ( [string ]$VaultName , [string ]$BackupPath = '/tmp/kv-backup' ) $token = Get-ManagedIdentityToken -Resource 'https://vault.azure.net' New-Item -ItemType Directory -Path $BackupPath -Force | Out-Null $listUri = "https://$VaultName .vault.azure.net/secrets" + '?api-version=7.4' $headers = @ { Authorization = "Bearer $token " } $secrets = (Invoke-RestMethod -Uri $listUri -Headers $headers ).value $backupSummary = foreach ($secret in $secrets ) { $secretName = $secret .id.Split('/' )[-1 ] $detail = Invoke-RestMethod -Uri $secret .id`?api-version =7.4 ` -Headers $headers $backup = [PSCustomObject ]@ { Name = $secretName ContentType = $detail .contentType BackupTime = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' Enabled = $detail .attributes.enabled Expires = $detail .attributes.exp } $backup | ConvertTo-Json | Out-File "$BackupPath /$secretName .json" $backup } $backupSummary | Format-Table -AutoSize Write-Host "备份完成,共 $ ($backupSummary .Count) 个密钥" -ForegroundColor Green } Backup-KeyVaultSecrets -VaultName 'kv-demo-01'
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 令牌获取成功,有效期至: 1740916800 通过 REST API 读取密钥成功: https://kv-demo-01 .vault.azure.net/secrets/DatabaseConnectionString/abc123def456 Name ContentType BackupTime Enabled Expires ---- ----------- ---------- ------- ------- DatabaseConnectionString 2026-03 -02 08:30:00 True StorageAccountKey 2026-03 -02 08:30:00 True ThirdPartyApiKey text/plain 2026-03 -02 08:30:00 True 1743571200 CertificateThumbprint 2026-03 -02 08:30:00 True 备份完成,共 4 个密钥
注意事项
RBAC 传播延迟 :新创建的角色分配可能需要 5 到 10 分钟才能生效。如果脚本执行后立即访问资源时报权限不足的错误,请等待一段时间后重试,也可以在脚本中加入轮询等待逻辑。
系统标识 vs 用户标识的选择 :系统分配标识与资源生命周期绑定,资源删除时标识自动清除,适合一对一场景。用户分配标识独立于资源存在,适合多个资源需要共享权限的场景,但需要手动管理生命周期。
IMDS 端点仅限虚拟机内部 :IMDS 端点 169.254.169.254 是链路本地地址,只能在 Azure 虚拟机内部访问。不要在本地开发环境或非 Azure 环境中尝试调用这个端点,开发调试时请使用 Connect-AzAccount -Identity 配合 Azure Cloud Shell。
令牌缓存与刷新 :Azure SDK 和 Az 模块会自动缓存令牌并在过期前刷新。如果你直接调用 REST API 获取令牌,需要自行处理令牌的缓存和刷新逻辑,令牌通常有效期为 1 小时。
最小权限原则 :为托管标识分配 RBAC 角色时,务必遵循最小权限原则,只授予实际需要的角色。避免使用 Owner 或 Contributor 等高权限角色,优先选择如 Key Vault Secrets User、Storage Blob Data Reader 等精确的内置角色。
本地开发调试技巧 :在本地开发时可以使用 Connect-AzAccount 交互式登录,然后通过 -DefaultProfile 参数切换不同的认证上下文,模拟托管标识的行为。部署到 Azure 后,将认证方式切换为 Connect-AzAccount -Identity 即可无缝迁移到托管标识,代码逻辑无需大幅修改。