使用应用服务和 Azure Functions 的 Key Vault 引用

本主题介绍在不需进行任何代码更改的情况下,如何使用应用服务或 Azure Functions 应用程序的 Azure Key Vault 中的机密。 Azure Key Vault 是一项服务,可以提供集中式机密管理,并且可以完全控制访问策略和审核历史记录。

授予应用对 Key Vault 的访问权限

若要从 Key Vault 读取机密,需创建一个保管库并授予应用访问该保管库的权限。

  1. 按照 Key Vault 快速入门中的说明创建一个密钥保管库。

  2. 为应用程序创建一个托管标识

    默认情况下,Key Vault 引用将使用应用的系统分配的标识,但你可以指定用户分配的标识

  3. 在 Key Vault 中为此前创建的应用程序标识创建一项访问策略。 在此策略上启用“获取”机密权限。 请勿配置“授权的应用程序”或 applicationId 设置,因为这与托管标识不兼容。

访问网络受限保管库

如果你的保管库配置有网络限制,则你还需要确保该应用程序具有网络访问权。

  1. 请确保该应用程序已配置有出站网络功能,正如应用服务网络功能Azure Functions 网络选项所述。

    试图使用专用终结点的 Linux 应用程序还要求显式将该应用配置为通过虚拟网络路由所有流量。 即将到来的更新中将删除此要求。 若要这样设置,请使用以下 CLI 命令:

    az webapp config set --subscription <sub> -g <rg> -n <appname> --generic-configurations '{"vnetRouteAllEnabled": true}'
    
  2. 请确保该保管库的配置将你的应用访问该保管库时所用的网络或子网纳入考量。

注意

Windows 容器当前不支持通过 VNet 集成的 Key Vault 引用。

使用用户分配的标识访问保管库

系统分配的标识尚不可用时,某些应用需要在创建时引用机密。 在这些情况下,可以提前创建用户分配的标识并为其提供对保管库的访问权限。

被授予对用户分配的标识的权限后,请执行以下步骤:

  1. 将标识分配到应用程序(如果尚未分配)。

  2. 通过将 keyVaultReferenceIdentity 属性设置为用户分配的标识的资源 ID,配置应用以将此标识用于 Key Vault 引用操作。

    userAssignedIdentityResourceId=$(az identity show -g MyResourceGroupName -n MyUserAssignedIdentityName --query id -o tsv)
    appResourceId=$(az webapp show -g MyResourceGroupName -n MyAppName --query id -o tsv)
    az rest --method PATCH --uri "${appResourceId}?api-version=2021-01-01" --body "{'properties':{'keyVaultReferenceIdentity':'${userAssignedIdentityResourceId}'}}"
    

此配置将应用于应用的所有引用。

引用语法

Key Vault 引用采用 @Microsoft.KeyVault({referenceString}) 格式,其中 {referenceString} 将替换为下述选项之一:

引用字符串 说明
SecretUri=secretUri SecretUri 应该是 Key Vault 中机密的完整数据平面 URI,可以选择包括版本,例如 https://myvault.vault.azure.cn/secrets/mysecret/https://myvault.vault.azure.cn/secrets/mysecret/ec96f02080254f109c51a1f14cdb1931
VaultName=vaultName;SecretName=secretName;SecretVersion=secretVersion VaultName 是必需的,并且应该是 Key Vault 资源的名称。 SecretName 是必需的,并且应该是目标机密的名称。 SecretVersion 是可选的,但如果存在,则指示要使用的机密的版本。

例如,完整的引用将如下所示:

@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.cn/secrets/mysecret/)

也可使用以下命令:

@Microsoft.KeyVault(VaultName=myvault;SecretName=mysecret)

旋转

如果引用中未指定版本,则应用将会使用 Key Vault 中存在的最新版本。 在有较新的版本可用时(例如通过轮换事件),应用将会在 24 小时内自动更新并开始使用最新版本。 延迟是因为应用服务会缓存 Key Vault 引用的值,并且每 24 小时会重新提取一次。 任何导致站点重启的应用配置更改都会导致所有引用的机密立即被重新提取。

Key Vault 中的源应用程序设置

Key Vault 引用可以用作应用程序设置的值,以便将机密保存在 Key Vault 而不是站点配置中。可以对应用程序设置进行安全的静态加密,但如果需要机密管理功能,则应将它们置于 Key Vault 中。

若要将 Key Vault 引用用于应用设置,请将引用设为设置的值。 应用可以通过密钥正常引用机密。 不需更改代码。

提示

应该将大多数使用 Key Vault 引用的应用程序设置标记为槽设置,因为你应该为每个环境设置单独的保管库。

Azure 文件存储装载注意事项

应用可使用 WEBSITE_CONTENTAZUREFILECONNECTIONSTRING 应用程序设置将 Azure 文件存储装载为文件系统。 此设置有额外的验证检查,旨在确保该应用可以正常启动。 平台依赖于在 Azure 文件存储中拥有内容共享,而且除非通过 WEBSITE_CONTENTSHARE 设置指定名称,否则其会采用默认名称。 对于任何修改这些设置的请求,平台会尝试验证此内容共享是否存在,如果不存在,则会尝试创建共享。 如果找不到或无法创建该内容共享,系统会阻止该请求。

对此设置使用密钥保管库引用时,默认情况下该验证检查会失败,因为在处理传入请求时,无法解析密钥本身。 若要避免此问题,可以通过将 WEBSITE_SKIP_CONTENTSHARE_VALIDATION 设置为“1”来跳过验证。 这将跳过所有检查,不会创建内容共享。 您应确保预先创建该内容共享。

注意

如果跳过验证,并且连接字符串或内容共享无效,该应用将无法正常启动,并且只会显示 HTTP 500 错误。

在创建站点的过程中,也可能会因为未传播托管标识权限,或未设置虚拟网络集成,导致内容共享装载尝试失败。 你可将 Azure 文件存储的设置工作推迟到稍后在部署模板中进行,以适应这种情况。 若要了解更多信息,请参阅 Azure 资源管理器部署。 “应用服务”会使用默认文件系统直至 Azure 文件存储设置完毕,并且不会复制文件,因此你需要确保在装载 Azure 文件存储之前的过渡期内不会进行任何部署尝试。

Azure 资源管理器部署

通过 Azure 资源管理器模板自动进行资源部署时,可能需要将依赖项按特定的顺序排列,这样才能使该功能发挥作用。 请注意,需将应用程序设置定义为其自己的资源,而不能使用站点定义中的 siteConfig 属性。 这是因为,站点需先进行定义,这样才能使用它来创建系统分配标识并将该标识用在访问策略中。

函数应用的示例伪模板可能如下所示:

{
    //...
    "resources": [
        {
            "type": "Microsoft.Storage/storageAccounts",
            "name": "[variables('storageAccountName')]",
            //...
        },
        {
            "type": "Microsoft.Insights/components",
            "name": "[variables('appInsightsName')]",
            //...
        },
        {
            "type": "Microsoft.Web/sites",
            "name": "[variables('functionAppName')]",
            "identity": {
                "type": "SystemAssigned"
            },
            //...
            "resources": [
                {
                    "type": "config",
                    "name": "appsettings",
                    //...
                    "dependsOn": [
                        "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]",
                        "[resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))]",
                        "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('storageConnectionStringName'))]",
                        "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('appInsightsKeyName'))]"
                    ],
                    "properties": {
                        "AzureWebJobsStorage": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('storageConnectionStringResourceId')).secretUriWithVersion, ')')]",
                        "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('storageConnectionStringResourceId')).secretUriWithVersion, ')')]",
                        "APPINSIGHTS_INSTRUMENTATIONKEY": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('appInsightsKeyResourceId')).secretUriWithVersion, ')')]",
                        "WEBSITE_ENABLE_SYNC_UPDATE_SITE": "true"
                        //...
                    }
                },
                {
                    "type": "sourcecontrols",
                    "name": "web",
                    //...
                    "dependsOn": [
                        "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]",
                        "[resourceId('Microsoft.Web/sites/config', variables('functionAppName'), 'appsettings')]"
                    ],
                }
            ]
        },
        {
            "type": "Microsoft.KeyVault/vaults",
            "name": "[variables('keyVaultName')]",
            //...
            "dependsOn": [
                "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]"
            ],
            "properties": {
                //...
                "accessPolicies": [
                    {
                        "tenantId": "[reference(resourceId('Microsoft.Web/sites/', variables('functionAppName')), '2020-12-01', 'Full').identity.tenantId]",
                        "objectId": "[reference(resourceId('Microsoft.Web/sites/', variables('functionAppName')), '2020-12-01', 'Full').identity.principalId]",
                        "permissions": {
                            "secrets": [ "get" ]
                        }
                    }
                ]
            },
            "resources": [
                {
                    "type": "secrets",
                    "name": "[variables('storageConnectionStringName')]",
                    //...
                    "dependsOn": [
                        "[resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))]",
                        "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
                    ],
                    "properties": {
                        "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountResourceId'),'2019-09-01').key1)]"
                    }
                },
                {
                    "type": "secrets",
                    "name": "[variables('appInsightsKeyName')]",
                    //...
                    "dependsOn": [
                        "[resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))]",
                        "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]"
                    ],
                    "properties": {
                        "value": "[reference(resourceId('microsoft.insights/components/', variables('appInsightsName')), '2019-09-01').InstrumentationKey]"
                    }
                }
            ]
        }
    ]
}

注意

在此示例中,源代码管理部署取决于应用程序设置。 这通常是不安全的行为,因为应用设置更新是以异步方式表现的。 不过,由于我们已包括 WEBSITE_ENABLE_SYNC_UPDATE_SITE 应用程序设置,因此更新是同步的。 这意味着源代码管理部署只有在应用程序设置已完全更新后才会开始。 有关更多应用设置,请参阅 Azure 应用服务中的环境变量和应用设置

排查 Key Vault 引用问题

如果未正常解析某个引用,将改用引用值。 这意味着,对于应用程序设置,将创建一个环境变量,其值采用 @Microsoft.KeyVault(...) 语法。 这可能导致应用程序引发错误,因为它需要特定结构的机密。

最常见的原因是 Key Vault 访问策略配置不当。 但是,原因也可能是机密不再存在,或者引用本身存在语法错误。

如果语法正确,可以通过在门户中检查当前解析状态来查看其他错误原因。 导航到“应用程序设置”,然后选择有问题的引用对应的“编辑”。 在“设置配置”下面,应会看到状态信息,包括所有错误。 缺少这些信息意味着引用语法无效。

也可以使用某个内置检测程序来获取更多信息。