ASP.NET Core 中的 Azure Key Vault 配置提供程序

本文介绍如何使用 Azure Key Vault 配置提供程序从 Azure Key Vault 机密加载应用配置值。 Azure Key Vault 是一项基于云的服务,可帮助保护应用和服务使用的加密密钥和机密。 将 Azure Key Vault 与 ASP.NET Core 应用结合使用的常见场景包括:

  • 控制对敏感配置数据的访问。
  • 满足 FIPS 140-2 第 2 级验证的硬件安全模块 (HSM) 在存储配置数据时的要求。

为以下包添加包引用:

示例应用

示例应用可采用两种模式运行,这由 Program.cs 上的 #define 预处理器指令决定:

  • Certificate:演示如何使用 Azure Key Vault 客户端 ID 和 X.509 证书来访问存储在 Azure Key Vault 中的机密。 可以在任何位置运行此示例,无论部署到 Azure App Service 还是部署到任何可为 ASP.NET Core 应用提供服务的主机。
  • Managed:演示如何使用 Azure 资源的托管标识。 托管标识使用 Azure Active Directory (AD) 身份验证对应用进行有关 Azure Key Vault 的身份验证,且不会在应用的代码或配置中存储凭据。 必须将示例的 Managed 版本部署到 Azure。 按照使用 Azure 资源的托管标识部分中的指导进行操作。

有关使用预处理器指令 (#define) 配置示例应用的详细信息,请参阅 ASP.NET Core 概述

查看或下载示例代码如何下载

开发环境中的机密存储

使用机密管理器在本地设置机密。 在开发环境中的本地计算机上运行示例应用时,会从本地用户机密存储区中加载机密。

机密管理器需要应用的项目文件中的 <UserSecretsId> 属性。 将属性值 ({GUID}) 设置为任何唯一 GUID:

<PropertyGroup>
  <UserSecretsId>{GUID}</UserSecretsId>
</PropertyGroup>

机密以名称/值对的形式创建。 在 ASP.NET Core 配置密钥名称中,层级值(配置节)将 :(冒号)作为分隔符。

机密管理器在项目的内容根中打开的命令行界面中使用,其中,{SECRET NAME} 为名称,{SECRET VALUE} 为值:

dotnet user-secrets set "{SECRET NAME}" "{SECRET VALUE}"

从项目的内容根的命令行界面中执行以下命令,从而为示例应用设置机密:

dotnet user-secrets set "SecretName" "secret_value_1_dev"
dotnet user-secrets set "Section:SecretName" "secret_value_2_dev"

如果这些机密存储在 Azure Key Vault 中(请参见生产环境中使用 Azure Key Vault 存储机密部分),则 _dev 后缀将更改为 _prod。 后缀在应用的输出中提供可视化提示,指示配置值的源。

在生产环境中使用 Azure Key Vault 存储机密

完成以下步骤以创建 Azure Key Vault,并在其中存储示例应用的机密。 有关详细信息,请参阅快速入门:使用 Azure CLI 在 Azure Key Vault 中设置和检索机密

  1. Azure 门户中使用以下方法之一打开 Azure Cloud Shell:

    • 选择代码块右上角的“试用”。 在文本框中使用搜索字符串“Azure CLI”。
    • 在浏览器中使用“启动 Cloud Shell”按钮打开 Cloud Shell。
    • 选择 Azure 门户右上角菜单中的“Cloud Shell”按钮。

    有关详细信息,请参阅 Azure CLIAzure Cloud Shell 概述

  2. 如果尚未进行身份验证,请使用 az login 命令登录。

  3. 使用以下命令创建资源组,其中 {RESOURCE GROUP NAME} 是新资源组的名称,{LOCATION} 是 Azure 区域:

    az group create --name "{RESOURCE GROUP NAME}" --location {LOCATION}
    
  4. 使用以下命令在资源组中创建密钥保管库,其中 {KEY VAULT NAME} 是新保管库的名称,{LOCATION} 是 Azure 区域:

    az keyvault create --name {KEY VAULT NAME} --resource-group "{RESOURCE GROUP NAME}" --location {LOCATION}
    
  5. 在保管库中,以名称/值对的形式创建机密。

    Azure Key Vault 机密名称仅限字母数字字符和破折号。 层级值(配置节)使用 --(双破折号)作为分隔符,因为密钥保管库机密名称中不允许使用冒号。 在 ASP.NET Core 配置的子项中使用冒号分隔节。 将机密加载到应用的配置中时,将双破折号替换为冒号。

    以下机密用于示例应用。 这些值包括 _prod 后缀,使它们与从机密管理器中的开发环境中加载的 _dev 后缀值区分开来。 将 {KEY VAULT NAME} 替换为你在上一步中创建的密钥保管库的名称:

    az keyvault secret set --vault-name {KEY VAULT NAME} --name "SecretName" --value "secret_value_1_prod"
    az keyvault secret set --vault-name {KEY VAULT NAME} --name "Section--SecretName" --value "secret_value_2_prod"
    

将应用程序 ID 和 X.509 证书用于非 Azure 托管的应用

配置 Azure AD、Azure 密钥保管库和应用,以便在 Azure 外托管应用时使用 Azure AD 应用程序 ID 和 X.509 证书对保管库进行身份验证。 有关详细信息,请参阅关于密钥、机密和证书

注意

尽管在 Azure 中托管的应用支持使用应用程序 ID 和 X.509 证书,但不建议使用。 在 Azure 中托管应用时,请改用 Azure 资源的托管标识。 托管标识不需要在应用或开发环境中存储证书。

Program.cs 上的 #define 预处理器指令设置为 Certificate 时,示例应用使用应用程序 ID 和 X.509 证书。

  1. 创建 PKCS#12 存档 (.pfx) 证书。 用于创建证书的选项包括 Windows 上的 New-SelfSignedCertificateOpenSSL
  2. 将证书安装到当前用户的个人证书存储中。 “将密钥标记为可导出”是可选的。 记下证书指纹,本过程稍后将用到。
  3. 将 PKCS#12 存档 (.pfx) 证书导出为 DER 编码的证书 (.cer)。
  4. 向 Azure AD(应用注册)注册应用。
  5. 将 DER 编码的证书 (.cer) 上传到 Azure AD
    1. 在 Azure AD 中选择应用。
    2. 导航到“证书与机密”。
    3. 选择“上传证书”,以上传包含公钥的证书。 支持 .cer、.pem 或 .crt 类型的证书。
  6. 在应用的 appsettings.json 文件中存储密钥保管库名称、应用程序 ID 和证书指纹。
  7. 在 Azure 门户中,导航到“密钥保管库”
  8. 在生产环境中使用 Azure Key Vault 存储机密部分,选择你创建的密钥保管库。
  9. 选择“访问策略”。
  10. 选择“添加访问策略”。
  11. 打开“机密权限”,并为应用提供 Get 和 List 权限。
  12. 选择“选择主体”,并按名称选择注册的应用。 选择“选择”按钮 。
  13. 选择“确定”。
  14. 选择“保存”。
  15. 部署应用。

Certificate 示例应用从 IConfigurationRoot 中获取其配置值,其名称与机密名称相同:

  • 非层级值:通过 config["SecretName"] 获取 SecretName 值。
  • 层级值(节):使用 :(冒号)表示法或使用 GetSection 方法。 使用以下任一方法获取配置值:
    • config["Section:SecretName"]
    • config.GetSection("Section")["SecretName"]

X.509 证书由 OS 托管。 应用使用 appsettings.json 文件提供的值调用 AddAzureKeyVault


using System.Security.Cryptography.X509Certificates;
using Azure.Identity;

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsProduction())
{
    using var x509Store = new X509Store(StoreLocation.CurrentUser);

    x509Store.Open(OpenFlags.ReadOnly);

    var x509Certificate = x509Store.Certificates
        .Find(
            X509FindType.FindByThumbprint,
            builder.Configuration["AzureADCertThumbprint"],
            validOnly: false)
        .OfType<X509Certificate2>()
        .Single();

    builder.Configuration.AddAzureKeyVault(
        new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
        new ClientCertificateCredential(
            builder.Configuration["AzureADDirectoryId"],
            builder.Configuration["AzureADApplicationId"],
            x509Certificate));
}

var app = builder.Build();

示例值:

  • 密钥保管库名称:contosovault
  • 应用程序 ID:627e911e-43cc-61d4-992e-12db9c81b413
  • 证书指纹:fe14593dd66b2406c5269d742d04b6e1ab03adb1

appsettings.json

{
  "KeyVaultName": "Key Vault Name",
  "AzureADApplicationId": "Azure AD Application ID",
  "AzureADCertThumbprint": "Azure AD Certificate Thumbprint",
  "AzureADDirectoryId": "Azure AD Directory ID"
}

运行应用时,网页将显示加载的机密值。 在开发环境中,机密值将加载 _dev 后缀。 在生产环境中,此值将加载 _prod 后缀。

将托管标识用于 Azure 资源

部署到 Azure 的应用可以利用Azure 资源的托管标识。 托管标识允许应用使用 Azure AD 身份验证向 Azure Key Vault 进行身份验证,而无需在应用的代码或配置中存储凭据。

Program.cs 上的 #define 预处理器指令设置为 Managed 时,示例应用使用系统分配的托管标识。 若要为 Azure 应用服务应用创建托管标识,请参阅如何将托管标识用于应用服务和 Azure Functions。 创建托管标识后,请记下 Azure 门户中应用服务的 Identity 面板上显示的应用对象 ID。

在应用的 appsettings.json 文件中输入保管库名称。 当设置为 Managed 版本时,示例应用不需要应用程序 ID 和密码(客户端机密),因此你可以忽略这些配置条目。 应用将部署到 Azure,Azure 将对应用进行身份验证,以便仅使用存储在 appsettings.json 文件中的保管库名称访问 Azure Key Vault。

将示例应用部署到 Azure 应用服务。

使用 Azure CLI 和应用的对象 ID,为应用提供 listget 权限以访问保管库:

az keyvault set-policy --name {KEY VAULT NAME} --object-id {OBJECT ID} --secret-permissions get list

使用 Azure CLI、PowerShell 或 Azure 门户重启应用。

示例应用创建 DefaultAzureCredential 类的实例。 凭据尝试从 Azure 资源的环境中获取访问令牌:

using Azure.Identity;

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsProduction())
{
    builder.Configuration.AddAzureKeyVault(
        new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
        new DefaultAzureCredential());
}

密钥保管库名称示例值:contosovault

appsettings.json

{
  "KeyVaultName": "Key Vault Name"
}

对于使用用户分配的托管标识的应用,使用以下方法之一配置托管标识的客户端 ID:

  1. 设置 AZURE_CLIENT_ID 环境变量。

  2. 调用 AddAzureKeyVault 时设置 DefaultAzureCredentialOptions.ManagedIdentityClientId 属性:

    builder.Configuration.AddAzureKeyVault(
        new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
        new DefaultAzureCredential(new DefaultAzureCredentialOptions
        {
            ManagedIdentityClientId = builder.Configuration["AzureADManagedIdentityClientId"]
        }));
    

运行应用时,网页将显示加载的机密值。 开发环境中的机密值具有 _dev 后缀,因为它们由机密管理器提供。 生产环境中的机密值将加载 _prod 后缀,因为它们由 Azure Key Vault 提供。

如果收到 Access denied 错误,请确认该应用是否注册到 Azure AD,以及是否提供对保管库的访问权限。 确认是否已在 Azure 中重新启用该服务。

有关将提供程序与托管标识和 Azure Pipelines 一起使用的信息,请参阅使用托管服务标识创建与 VM 的 Azure 资源管理器服务连接

配置选项

AddAzureKeyVault 支持使用 AzureKeyVaultConfigurationOptions 对象:

// using Azure.Extensions.AspNetCore.Configuration.Secrets;

builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
    new DefaultAzureCredential(),
    new AzureKeyVaultConfigurationOptions
    {
        // ...
    });

AzureKeyVaultConfigurationOptions 对象包含以下属性:

属性 说明
Manager 用于控制机密加载的 KeyVaultSecretManager 实例。
ReloadInterval TimeSpan 在轮询保管库以做出更改的两次尝试之间等待。 默认值为 null(不重新加载配置)。

使用密钥名称前缀

AddAzureKeyVault 提供重载,该重载接受 KeyVaultSecretManager 的实现,使你可以控制密钥保管库机密到配置密钥的转换方式。 例如,你可以通过实现接口来根据你在应用启动时提供的前缀值加载机密值。 例如,这种方法可让你根据应用版本加载机密。

警告

请勿对密钥保管库机密使用前缀来执行以下操作:

  • 将多个应用的机密放入同一保管库中。
  • 将环境机密(例如开发机密与生产机密)放入同一保管库中。

各个应用和开发/生产环境应使用不同的密钥保管库来隔离应用环境,以实现最高的安全性级别。

在下面的示例中,在密钥保管库中(在开发环境使用机密管理器)为 5000-AppSecret 创建机密(密钥保管库中的机密名称不允许使用句点)。 此机密代表应用版本为 5.0.0.0 的应用机密。 对于应用的另一版本 5.1.0.0,机密将添加到 5100-AppSecret 的保管库(使用机密管理器)。 每个应用版本都会将其版本对应的机密值作为 AppSecret 加载到其配置中,并在加载机密时删除版本。

使用自定义 KeyVaultSecretManager 实现调用 AddAzureKeyVault

// using Azure.Extensions.AspNetCore.Configuration.Secrets;

builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
    new DefaultAzureCredential(),
    new SamplePrefixKeyVaultSecretManager("5000"));

该实现将对机密的版本前缀做出反应,以将适当的机密加载到配置中:

  • 当机密名称以此前缀开头时,Load 将加载机密。 不加载其他机密。
  • GetKey
    • 从机密名称中删除此前缀。
    • 将任何名称中的双破折号替换为 KeyDelimiter,它在配置中用作分隔符(通常为冒号)。 Azure Key Vault 不允许在机密名称中使用冒号。
public class SamplePrefixKeyVaultSecretManager : KeyVaultSecretManager
{
    private readonly string _prefix;

    public SamplePrefixKeyVaultSecretManager(string prefix)
        => _prefix = $"{prefix}-";

    public override bool Load(SecretProperties properties)
        => properties.Name.StartsWith(_prefix);

    public override string GetKey(KeyVaultSecret secret)
        => secret.Name[_prefix.Length..].Replace("--", ConfigurationPath.KeyDelimiter);
}

Load 方法由提供程序算法调用,该算法循环访问保管库机密以查找版本前缀对应的机密。 如果发现版本前缀为 Load,则该算法将使用 GetKey 方法来返回密钥名称的配置名称。 这将从机密的名称中删除此版本前缀。 将返回机密名称的其余部分,以便加载到应用的配置名称/值对中。

实现此方法时:

  1. 将在应用的项目文件中指定应用版本。 在以下示例中,应用的版本设置为 5.0.0.0

    <PropertyGroup>
      <Version>5.0.0.0</Version>
    </PropertyGroup>
    
  2. 确认 <UserSecretsId> 属性存在于应用的项目文件中,文件中的 {GUID} 是用户提供的 GUID:

    <PropertyGroup>
      <UserSecretsId>{GUID}</UserSecretsId>
    </PropertyGroup>
    

    使用机密管理器在本地保存以下机密:

    dotnet user-secrets set "5000-AppSecret" "5.0.0.0_secret_value_dev"
    dotnet user-secrets set "5100-AppSecret" "5.1.0.0_secret_value_dev"
    
  3. 使用以下 Azure CLI 命令在 Azure Key Vault 中保存机密:

    az keyvault secret set --vault-name {KEY VAULT NAME} --name "5000-AppSecret" --value "5.0.0.0_secret_value_prod"
    az keyvault secret set --vault-name {KEY VAULT NAME} --name "5100-AppSecret" --value "5.1.0.0_secret_value_prod"
    
  4. 应用运行时,将加载密钥保管库机密。 5000-AppSecret 的字符串机密与应用的项目文件中指定的应用版本 (5.0.0.0) 相匹配。

  5. 将从密钥名称中删除版本 5000(带有破折号)。 在整个应用中,通过密钥 AppSecret 读取配置会加载机密值。

  6. 如果在项目文件中将应用版本更改为 5.1.0.0,并再次运行应用,则开发环境中返回的机密值为 5.1.0.0_secret_value_dev,而生产环境中对应的值为 5.1.0.0_secret_value_prod

注意

你还可将自己的 SecretClient 实现用于 AddAzureKeyVault。 自定义客户端允许跨应用共享客户端的单个实例。

将数组绑定至类

提供程序可将配置值读取到数组中,然后绑定到 POCO 数组。

从允许密钥包含冒号 (:) 分隔符的配置源中进行读取时,将使用数字键段来区分组成数组的密钥 (:0:, :1:, … :{n}:)。 有关详细信息,请参阅配置:将数组绑定到类

Azure Key Vault 密钥不能使用冒号作为分隔符。 本文中所述的方法使用双破折号 (--) 作为层级值(节)的分隔符。 数组键以双破折号和数字键段的形式存储在 Azure Key Vault 中 (--0--, --1--, … --{n}--)。

查看 JSON 文件提供的以下 Serilog 日志记录提供程序配置。 在 WriteTo 数组中定义了两个反映各自 Serilog 接收器的对象文字,这些接收器描述了日志记录输出的目标:

"Serilog": {
  "WriteTo": [
    {
      "Name": "AzureTableStorage",
      "Args": {
        "storageTableName": "logs",
        "connectionString": "DefaultEnd...ountKey=Eby8...GMGw=="
      }
    },
    {
      "Name": "AzureDocumentDB",
      "Args": {
        "endpointUrl": "https://contoso.documents.azure.com:443",
        "authorizationKey": "Eby8...GMGw=="
      }
    }
  ]
}

前面的 JSON 文件中所示的配置将使用双破折号 (--) 表示法和数值段存储在 Azure Key Vault 中:

密钥
Serilog--WriteTo--0--Name AzureTableStorage
Serilog--WriteTo--0--Args--storageTableName logs
Serilog--WriteTo--0--Args--connectionString DefaultEnd...ountKey=Eby8...GMGw==
Serilog--WriteTo--1--Name AzureDocumentDB
Serilog--WriteTo--1--Args--endpointUrl https://contoso.documents.azure.com:443
Serilog--WriteTo--1--Args--authorizationKey Eby8...GMGw==

重载机密

默认情况下,配置提供程序在应用的生存期内缓存机密。 应用会忽略随后在保管库中禁用或更新的机密。

若要重新加载机密,请调用 IConfigurationRoot.Reload

config.Reload();

若要按指定的时间间隔定期重新加载机密,请设置 AzureKeyVaultConfigurationOptions.ReloadInterval 属性。 有关详细信息,请参阅配置选项

已禁用和过期的机密

默认情况下,过期的机密包含在配置提供程序中。 若要在应用配置中排除这些机密的值,请更新过期的机密,或使用自定义配置提供程序提供配置:

class SampleKeyVaultSecretManager : KeyVaultSecretManager
{
  public override bool Load(SecretProperties properties) =>
    properties.ExpiresOn.HasValue &&
    properties.ExpiresOn.Value > DateTimeOffset.Now;
}

将此自定义 KeyVaultSecretManager 传递给 AddAzureKeyVault

// using Azure.Extensions.AspNetCore.Configuration.Secrets;

builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
    new DefaultAzureCredential(),
    new SampleKeyVaultSecretManager());

已禁用的机密不能从密钥保管库中检索,也永远不会被包括在内。

疑难解答

当应用无法使用提供程序加载配置时,会将错误消息写入 ASP.NET Core 日志记录基础结构。 以下条件将阻止加载配置:

  • 未正确配置 Azure AD 中的应用或证书。
  • Azure 密钥保管库中不存在保管库。
  • 应用无权访问保管库。
  • 访问策略不包括 GetList 权限。
  • 在保管库中,配置数据(名称-值对)名称错误、缺失或已禁用。
  • 应用的密钥保管库名称错误 (KeyVaultName)、Azure AD 应用程序 ID 错误 (AzureADApplicationId)、Azure AD 证书指纹错误 (AzureADCertThumbprint) 或 Azure AD Directory ID 错误 (AzureADDirectoryId)。
  • 为应用添加密钥保管库访问策略时,策略虽已创建,但在“访问策略”UI 中没有选择“保存”按钮

其他资源

本文介绍如何使用 Azure Key Vault 配置提供程序从 Azure Key Vault 机密加载应用配置值。 Azure Key Vault 是一项基于云的服务,可帮助保护应用和服务使用的加密密钥和机密。 将 Azure Key Vault 与 ASP.NET Core 应用结合使用的常见场景包括:

  • 控制对敏感配置数据的访问。
  • 满足 FIPS 140-2 第 2 级验证的硬件安全模块 (HSM) 在存储配置数据时的要求。

为以下包添加包引用:

示例应用

示例应用可采用两种模式运行,这由 Program.cs 上的 #define 预处理器指令决定:

  • Certificate:演示如何使用 Azure Key Vault 客户端 ID 和 X.509 证书来访问存储在 Azure Key Vault 中的机密。 可以在任何位置运行此示例,无论部署到 Azure App Service 还是部署到任何可为 ASP.NET Core 应用提供服务的主机。
  • Managed:演示如何使用 Azure 资源的托管标识。 托管标识使用 Azure Active Directory (AD) 身份验证对应用进行有关 Azure Key Vault 的身份验证,且不会在应用的代码或配置中存储凭据。 使用托管标识进行身份验证时,不需要 Azure AD 应用程序 ID 和密码(客户端密钥)。 必须将示例的 Managed 版本部署到 Azure。 按照使用 Azure 资源的托管标识部分中的指导进行操作。

有关使用预处理器指令 (#define) 配置示例应用的详细信息,请参阅 ASP.NET Core 概述

查看或下载示例代码如何下载

开发环境中的机密存储

使用机密管理器在本地设置机密。 在开发环境中的本地计算机上运行示例应用时,会从本地用户机密存储区中加载机密。

机密管理器需要应用的项目文件中的 <UserSecretsId> 属性。 将属性值 ({GUID}) 设置为任何唯一 GUID:

<PropertyGroup>
  <UserSecretsId>{GUID}</UserSecretsId>
</PropertyGroup>

机密以名称/值对的形式创建。 在 ASP.NET Core 配置密钥名称中,层级值(配置节)将 :(冒号)作为分隔符。

机密管理器在项目的内容根中打开的命令行界面中使用,其中,{SECRET NAME} 为名称,{SECRET VALUE} 为值:

dotnet user-secrets set "{SECRET NAME}" "{SECRET VALUE}"

从项目的内容根的命令行界面中执行以下命令,从而为示例应用设置机密:

dotnet user-secrets set "SecretName" "secret_value_1_dev"
dotnet user-secrets set "Section:SecretName" "secret_value_2_dev"

如果这些机密存储在 Azure Key Vault 中(请参见生产环境中使用 Azure Key Vault 存储机密部分),则 _dev 后缀将更改为 _prod。 后缀在应用的输出中提供可视化提示,指示配置值的源。

在生产环境中使用 Azure Key Vault 存储机密

完成以下步骤以创建 Azure Key Vault,并在其中存储示例应用的机密。 有关详细信息,请参阅快速入门:使用 Azure CLI 在 Azure Key Vault 中设置和检索机密

  1. Azure 门户中使用以下方法之一打开 Azure Cloud Shell:

    • 选择代码块右上角的“试用”。 在文本框中使用搜索字符串“Azure CLI”。
    • 在浏览器中使用“启动 Cloud Shell”按钮打开 Cloud Shell。
    • 选择 Azure 门户右上角菜单中的“Cloud Shell”按钮。

    有关详细信息,请参阅 Azure CLIAzure Cloud Shell 概述

  2. 如果尚未进行身份验证,请使用 az login 命令登录。

  3. 使用以下命令创建资源组,其中 {RESOURCE GROUP NAME} 是新资源组的名称,{LOCATION} 是 Azure 区域:

    az group create --name "{RESOURCE GROUP NAME}" --location {LOCATION}
    
  4. 使用以下命令在资源组中创建密钥保管库,其中 {KEY VAULT NAME} 是新保管库的名称,{LOCATION} 是 Azure 区域:

    az keyvault create --name {KEY VAULT NAME} --resource-group "{RESOURCE GROUP NAME}" --location {LOCATION}
    
  5. 在保管库中,以名称/值对的形式创建机密。

    Azure Key Vault 机密名称仅限字母数字字符和破折号。 层级值(配置节)使用 --(双破折号)作为分隔符,因为密钥保管库机密名称中不允许使用冒号。 在 ASP.NET Core 配置的子项中使用冒号分隔节。 将机密加载到应用的配置中时,将双破折号替换为冒号。

    以下机密用于示例应用。 这些值包括 _prod 后缀,使它们与从机密管理器中的开发环境中加载的 _dev 后缀值区分开来。 将 {KEY VAULT NAME} 替换为你在上一步中创建的密钥保管库的名称:

    az keyvault secret set --vault-name {KEY VAULT NAME} --name "SecretName" --value "secret_value_1_prod"
    az keyvault secret set --vault-name {KEY VAULT NAME} --name "Section--SecretName" --value "secret_value_2_prod"
    

将应用程序 ID 和 X.509 证书用于非 Azure 托管的应用

配置 Azure AD、Azure 密钥保管库和应用,以便在 Azure 外托管应用时使用 Azure AD 应用程序 ID 和 X.509 证书对保管库进行身份验证。 有关详细信息,请参阅关于密钥、机密和证书

注意

尽管在 Azure 中托管的应用支持使用应用程序 ID 和 X.509 证书,但不建议使用。 在 Azure 中托管应用时,请改用 Azure 资源的托管标识。 托管标识不需要在应用或开发环境中存储证书。

Program.cs 上的 #define 预处理器指令设置为 Certificate 时,示例应用使用应用程序 ID 和 X.509 证书。

  1. 创建 PKCS#12 存档 (.pfx) 证书。 用于创建证书的选项包括 Windows 上的 New-SelfSignedCertificateOpenSSL
  2. 将证书安装到当前用户的个人证书存储中。 “将密钥标记为可导出”是可选的。 记下证书指纹,本过程稍后将用到。
  3. 将 PKCS#12 存档 (.pfx) 证书导出为 DER 编码的证书 (.cer)。
  4. 向 Azure AD(应用注册)注册应用。
  5. 将 DER 编码的证书 (.cer) 上传到 Azure AD
    1. 在 Azure AD 中选择应用。
    2. 导航到“证书与机密”。
    3. 选择“上传证书”,以上传包含公钥的证书。 支持 .cer、.pem 或 .crt 类型的证书。
  6. 在应用的 appsettings.json 文件中存储密钥保管库名称、应用程序 ID 和证书指纹。
  7. 在 Azure 门户中,导航到“密钥保管库”
  8. 在生产环境中使用 Azure Key Vault 存储机密部分,选择你创建的密钥保管库。
  9. 选择“访问策略”。
  10. 选择“添加访问策略”。
  11. 打开“机密权限”,并为应用提供 Get 和 List 权限。
  12. 选择“选择主体”,并按名称选择注册的应用。 选择“选择”按钮 。
  13. 选择“确定”。
  14. 选择“保存”。
  15. 部署应用。

Certificate 示例应用从 IConfigurationRoot 中获取其配置值,其名称与机密名称相同:

  • 非层级值:通过 config["SecretName"] 获取 SecretName 值。
  • 层级值(节):使用 :(冒号)表示法或使用 GetSection 方法。 使用以下任一方法获取配置值:
    • config["Section:SecretName"]
    • config.GetSection("Section")["SecretName"]

X.509 证书由 OS 托管。 应用使用 appsettings.json 文件提供的值调用 AddAzureKeyVault

// using System.Linq;
// using System.Security.Cryptography.X509Certificates;
// using Azure.Extensions.AspNetCore.Configuration.Secrets;
// using Azure.Identity;

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            if (context.HostingEnvironment.IsProduction())
            {
                var builtConfig = config.Build();

                using var store = new X509Store(StoreLocation.CurrentUser);
                store.Open(OpenFlags.ReadOnly);
                var certs = store.Certificates.Find(
                    X509FindType.FindByThumbprint,
                    builtConfig["AzureADCertThumbprint"], false);

                config.AddAzureKeyVault(new Uri($"https://{builtConfig["KeyVaultName"]}.vault.azure.net/"),
                                        new ClientCertificateCredential(builtConfig["AzureADDirectoryId"], builtConfig["AzureADApplicationId"], certs.OfType<X509Certificate2>().Single()),
                                        new KeyVaultSecretManager());

                store.Close();
            }
        })
        .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());

示例值:

  • 密钥保管库名称:contosovault
  • 应用程序 ID:627e911e-43cc-61d4-992e-12db9c81b413
  • 证书指纹:fe14593dd66b2406c5269d742d04b6e1ab03adb1

appsettings.json

{
  "KeyVaultName": "Key Vault Name",
  "AzureADApplicationId": "Azure AD Application ID",
  "AzureADCertThumbprint": "Azure AD Certificate Thumbprint",
  "AzureADDirectoryId": "Azure AD Directory ID"
}

运行应用时,网页将显示加载的机密值。 在开发环境中,机密值将加载 _dev 后缀。 在生产环境中,此值将加载 _prod 后缀。

将托管标识用于 Azure 资源

部署到 Azure 的应用可以利用Azure 资源的托管标识。 托管标识允许应用使用 Azure AD 身份验证向 Azure Key Vault 进行身份验证,而无需存储在应用中的凭据(应用程序 ID 和密码/客户端机密)。

Program.cs 上的 #define 预处理器指令设置为 Managed 时,示例应用使用 Azure 资源的托管标识。

在应用的 appsettings.json 文件中输入保管库名称。 当设置为 Managed 版本时,示例应用不需要应用程序 ID 和密码(客户端机密),因此你可以忽略这些配置条目。 应用将部署到 Azure,Azure 将对应用进行身份验证,以便仅使用存储在 appsettings.json 文件中的保管库名称访问 Azure Key Vault。

将示例应用部署到 Azure 应用服务。

创建服务时,会自动向 Azure AD 注册部署到 Azure 应用服务的应用。 从部署中获取对象 ID 以供在以下命令中使用。 对象 ID 显示在 Azure 门户中应用服务的“Identity面板上。

使用 Azure CLI 和应用的对象 ID,为应用提供 listget 权限以访问保管库:

az keyvault set-policy --name {KEY VAULT NAME} --object-id {OBJECT ID} --secret-permissions get list

使用 Azure CLI、PowerShell 或 Azure 门户重启应用。

示例应用:

  • 创建 DefaultAzureCredential 类的实例。 凭据尝试从 Azure 资源的环境中获取访问令牌。
  • 使用 DefaultAzureCredential 实例创建新的 SecretClient
  • SecretClient 实例与 KeyVaultSecretManager 实例一起使用,后者加载机密值并将密钥名称中的双破折号 (--) 替换为冒号 (:)。
// using Azure.Security.KeyVault.Secrets;
// using Azure.Identity;
// using Azure.Extensions.AspNetCore.Configuration.Secrets;

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            if (context.HostingEnvironment.IsProduction())
            {
                var builtConfig = config.Build();
                var secretClient = new SecretClient(
                    new Uri($"https://{builtConfig["KeyVaultName"]}.vault.azure.net/"),
                    new DefaultAzureCredential());
                config.AddAzureKeyVault(secretClient, new KeyVaultSecretManager());
            }
        })
        .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());

密钥保管库名称示例值:contosovault

appsettings.json

{
  "KeyVaultName": "Key Vault Name"
}

运行应用时,网页将显示加载的机密值。 开发环境中的机密值具有 _dev 后缀,因为它们由机密管理器提供。 生产环境中的机密值将加载 _prod 后缀,因为它们由 Azure Key Vault 提供。

如果收到 Access denied 错误,请确认该应用是否注册到 Azure AD,以及是否提供对保管库的访问权限。 确认是否已在 Azure 中重新启用该服务。

有关将提供程序与托管标识和 Azure Pipelines 一起使用的信息,请参阅使用托管服务标识创建与 VM 的 Azure 资源管理器服务连接

配置选项

AddAzureKeyVault 支持使用 AzureKeyVaultConfigurationOptions 对象:

config.AddAzureKeyVault(
    new SecretClient(
        new Uri("Your Key Vault Endpoint"),
        new DefaultAzureCredential(),
        new AzureKeyVaultConfigurationOptions())
    {
        ...
    });

AzureKeyVaultConfigurationOptions 对象包含以下属性。

属性 说明
Manager 用于控制机密加载的 KeyVaultSecretManager 实例。
ReloadInterval TimeSpan 在轮询保管库以做出更改的两次尝试之间等待。 默认值为 null(不重新加载配置)。

使用密钥名称前缀

AddAzureKeyVault 提供重载,该重载接受 KeyVaultSecretManager 的实现,使你可以控制密钥保管库机密到配置密钥的转换方式。 例如,你可以通过实现接口来根据你在应用启动时提供的前缀值加载机密值。 例如,这种方法可让你根据应用版本加载机密。

警告

请勿对密钥保管库机密使用前缀来执行以下操作:

  • 将多个应用的机密放入同一保管库中。
  • 将环境机密(例如开发机密与生产机密)放入同一保管库中。

各个应用和开发/生产环境应使用不同的密钥保管库来隔离应用环境,以实现最高的安全性级别。

在下面的示例中,在密钥保管库中(在开发环境使用机密管理器)为 5000-AppSecret 创建机密(密钥保管库中的机密名称不允许使用句点)。 此机密代表应用版本为 5.0.0.0 的应用机密。 对于应用的另一版本 5.1.0.0,机密将添加到 5100-AppSecret 的保管库(使用机密管理器)。 每个应用版本都会将其版本对应的机密值作为 AppSecret 加载到其配置中,并在加载机密时删除版本。

使用自定义 KeyVaultSecretManager 实现调用 AddAzureKeyVault

config.AddAzureKeyVault(
    $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/",
    builtConfig["AzureADApplicationId"],
    certs.OfType<X509Certificate2>().Single(),
    new PrefixKeyVaultSecretManager(versionPrefix));

该实现将对机密的版本前缀做出反应,以将适当的机密加载到配置中:

  • 当机密名称以此前缀开头时,Load 将加载机密。 不加载其他机密。
  • GetKey
    • 从机密名称中删除此前缀。
    • 将任何名称中的双破折号替换为 KeyDelimiter,它在配置中用作分隔符(通常为冒号)。 Azure Key Vault 不允许在机密名称中使用冒号。
public class PrefixKeyVaultSecretManager : KeyVaultSecretManager
{
    private readonly string _prefix;

    public PrefixKeyVaultSecretManager(string prefix)
    {
        _prefix = $"{prefix}-";
    }

    public override bool Load(SecretProperties secret)
    {
        return secret.Name.StartsWith(_prefix);
    }

    public override string GetKey(KeyVaultSecret secret)
    {
        return secret.Name
            .Substring(_prefix.Length)
            .Replace("--", ConfigurationPath.KeyDelimiter);
    }
}

Load 方法由提供程序算法调用,该算法循环访问保管库机密以查找版本前缀对应的机密。 如果发现版本前缀为 Load,则该算法将使用 GetKey 方法来返回密钥名称的配置名称。 这将从机密的名称中删除此版本前缀。 将返回机密名称的其余部分,以便加载到应用的配置名称/值对中。

实现此方法时:

  1. 将在应用的项目文件中指定应用版本。 在以下示例中,应用的版本设置为 5.0.0.0

    <PropertyGroup>
      <Version>5.0.0.0</Version>
    </PropertyGroup>
    
  2. 确认 <UserSecretsId> 属性存在于应用的项目文件中,文件中的 {GUID} 是用户提供的 GUID:

    <PropertyGroup>
      <UserSecretsId>{GUID}</UserSecretsId>
    </PropertyGroup>
    

    使用机密管理器在本地保存以下机密:

    dotnet user-secrets set "5000-AppSecret" "5.0.0.0_secret_value_dev"
    dotnet user-secrets set "5100-AppSecret" "5.1.0.0_secret_value_dev"
    
  3. 使用以下 Azure CLI 命令在 Azure Key Vault 中保存机密:

    az keyvault secret set --vault-name {KEY VAULT NAME} --name "5000-AppSecret" --value "5.0.0.0_secret_value_prod"
    az keyvault secret set --vault-name {KEY VAULT NAME} --name "5100-AppSecret" --value "5.1.0.0_secret_value_prod"
    
  4. 应用运行时,将加载密钥保管库机密。 5000-AppSecret 的字符串机密与应用的项目文件中指定的应用版本 (5.0.0.0) 相匹配。

  5. 将从密钥名称中删除版本 5000(带有破折号)。 在整个应用中,通过密钥 AppSecret 读取配置会加载机密值。

  6. 如果在项目文件中将应用版本更改为 5.1.0.0,并再次运行应用,则开发环境中返回的机密值为 5.1.0.0_secret_value_dev,而生产环境中对应的值为 5.1.0.0_secret_value_prod

注意

你还可将自己的 SecretClient 实现用于 AddAzureKeyVault。 自定义客户端允许跨应用共享客户端的单个实例。

将数组绑定至类

提供程序可将配置值读取到数组中,然后绑定到 POCO 数组。

从允许密钥包含冒号 (:) 分隔符的配置源中进行读取时,将使用数字键段来区分组成数组的密钥 (:0:, :1:, … :{n}:)。 有关详细信息,请参阅配置:将数组绑定到类

Azure Key Vault 密钥不能使用冒号作为分隔符。 本文中所述的方法使用双破折号 (--) 作为层级值(节)的分隔符。 数组键以双破折号和数字键段的形式存储在 Azure Key Vault 中 (--0--, --1--, … --{n}--)。

查看 JSON 文件提供的以下 Serilog 日志记录提供程序配置。 在 WriteTo 数组中定义了两个反映各自 Serilog 接收器的对象文字,这些接收器描述了日志记录输出的目标:

"Serilog": {
  "WriteTo": [
    {
      "Name": "AzureTableStorage",
      "Args": {
        "storageTableName": "logs",
        "connectionString": "DefaultEnd...ountKey=Eby8...GMGw=="
      }
    },
    {
      "Name": "AzureDocumentDB",
      "Args": {
        "endpointUrl": "https://contoso.documents.azure.com:443",
        "authorizationKey": "Eby8...GMGw=="
      }
    }
  ]
}

前面的 JSON 文件中所示的配置将使用双破折号 (--) 表示法和数值段存储在 Azure Key Vault 中:

密钥
Serilog--WriteTo--0--Name AzureTableStorage
Serilog--WriteTo--0--Args--storageTableName logs
Serilog--WriteTo--0--Args--connectionString DefaultEnd...ountKey=Eby8...GMGw==
Serilog--WriteTo--1--Name AzureDocumentDB
Serilog--WriteTo--1--Args--endpointUrl https://contoso.documents.azure.com:443
Serilog--WriteTo--1--Args--authorizationKey Eby8...GMGw==

重载机密

在调用 IConfigurationRoot.Reload 之前会缓存机密。 在执行 Reload 之前,应用不会遵守保管库中后续禁用和更新的机密。

Configuration.Reload();

已禁用和过期的机密

默认情况下,过期的机密包含在配置提供程序中。 若要在应用配置中排除这些机密的值,请更新过期的机密,或使用自定义配置提供程序提供配置:

class SampleKeyVaultSecretManager : KeyVaultSecretManager
{
  public override bool Load(SecretProperties properties) =>
    properties.ExpiresOn.HasValue &&
    properties.ExpiresOn.Value > DateTimeOffset.Now;
}

将此自定义 KeyVaultSecretManager 传递给 AddAzureKeyVault

// using Azure.Extensions.AspNetCore.Configuration.Secrets;

config.AddAzureKeyVault(
    new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
    new DefaultAzureCredential(),
    new SampleKeyVaultSecretManager());

已禁用的机密不能从密钥保管库中检索,也永远不会被包括在内。

疑难解答

当应用无法使用提供程序加载配置时,会将错误消息写入 ASP.NET Core 日志记录基础结构。 以下条件将阻止加载配置:

  • 未正确配置 Azure AD 中的应用或证书。
  • Azure 密钥保管库中不存在保管库。
  • 应用无权访问保管库。
  • 访问策略不包括 GetList 权限。
  • 在保管库中,配置数据(名称-值对)名称错误、缺失或已禁用。
  • 应用的密钥保管库名称错误 (KeyVaultName)、Azure AD 应用程序 ID 错误 (AzureADApplicationId)、Azure AD 证书指纹错误 (AzureADCertThumbprint) 或 Azure AD Directory ID 错误 (AzureADDirectoryId)。
  • 为应用添加密钥保管库访问策略时,策略虽已创建,但在“访问策略”UI 中没有选择“保存”按钮

其他资源