ASP.NET Core – User Secrets & Azure Key Vault

前言

以前就写过很多篇了

ASP.NET Core – Configuration & Options

Asp.net core 学习笔记 ( Azure key-vault )

Asp.net core 学习笔记 Secret 和 Data Protect Azure key-vault & Storage Account 第 2 篇

Azure 入门系列 (第四篇 Key Vault)

这篇作为最新最完整的版本呗.

 

参考

Docs – Safe storage of app secrets in development in ASP.NET Core

Docs –  Azure Key Vault configuration provider in ASP.NET Core

 

What & Why User Secrets?

一个项目里, 许多地方都会用到密码. 比如链接 SQL Server, SMTP, 还有各种 third party app client id & secret.

密码属于敏感信息, 只能个人或公司团队知道, 不可以外泄到其它地方.

早年, 大家都在本机上做开发, 公司团队则有自己的域网和服务器. 所以密码外泄是很难发生的.

可如今, 已经很少公司自己搞 version control 这些了. 大家都用 Github / Azure DevOps 等平台的 cloud service.

于是, 大家就开始关注如何确保密码不外泄了.

User Secrets 就是用来解决这些问题的. User Secrets 会确保密码只会保存在本机上, 不会被发布到 Github / Azure DevOps 上去.

只有每个开发人员的机子上才保存了密码. 这样就确保了密码不外泄. 

而它在做到这些事情的同时并不会增加开发者的负担. 这点, ASP.NET Core 还是封装的很不错的.

 

User Secrets Get Started (WebApp)

创建项目

dotnet new webapp -o UserSecretsWebApp

添加账户密码到 appsettings.json

{
  "Account": {
    "Username": "Derrick",
    "Password": "secret"
  }
}

在 program.cs 读取  Account.Password

var builder = WebApplication.CreateBuilder(args);
var password = builder.Configuration.GetValue<string>("Account:Password");
Console.WriteLine(password); // secret

目前拿到的密码就是 appsettings.json 定义的 "secret", 现在我们加入 User Secrets

dotnet user-secrets init
dotnet user-secrets set "Account:Password" "my real password"

第一句是初始化, 第二句是添加 key value.

注意, key path 的分隔符是分号 : 而不是点 . 哦.

再次运行

dotnet run

password 从原本的 "secret" 换成了 "my real password"

几个重点

1.ASP.NET Core 的 User Secrets 通常是搭配 appsettings.json 和 Configuration 一起使用的.

2.常见的操作有: 初始化, 添加/更新, 列出, 删除

dotnet user-secrets init
dotnet user-secrets set "Account:Password" "my real password"
dotnet user-secrets list
dotnet user-secrets remove "Account:Password"

3. 所有 secret 会被保存到一个 json file, Windows 下的路径是

%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json

4. 通过 Visual Studio 可以直接打开 json file 修改

 

User Secrets Get Started (Console)

var builder = WebApplication.CreateBuilder(args);

CreateBuilder 封装了太多细节. 我们来一个比较底层的接口, 看看它是如果工作的.

创建 Console 和 add package

dotnet new console -o UserSecretsConsole
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.FileExtensions
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
dotnet add package Microsoft.Extensions.Configuration.Binder

program.cs

using System.Reflection;
using Microsoft.Extensions.Configuration;

// 创建 config builder
// 链接 appsettings.json
var configurationBuilder = new ConfigurationManager()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"appsettings.Development.json", optional: true, reloadOnChange: true);

if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
{
    // 链接 User Secrets
    configurationBuilder = configurationBuilder.AddUserSecrets(Assembly.GetExecutingAssembly());
}

// 这里相等于 WebApp 的 builder.Configuration 了
var configuration = configurationBuilder.Build();
var password = configuration.GetValue<string>("Account:Password"); // my real password
Console.WriteLine(password);

AddUserSecrets 就是它偷龙转风的地方了.

查看源码 dotnet/runtime 里的 UserSecretsConfigurationExtensions.cs

会发现它并没有什么神奇的地方, 它也是通过 .AddJsonFile 把 secrets.json 覆盖到 configuration 里而已.

  

User Secrets with Azure Key Vault (Azure-hosted)

What & Why?

上面讲的都是在开发阶段如何保护密码. 那到了 Production 阶段呢?密码和程序是否应该放在一块?

显然, 分开是比较好的, 至少不要让 hacker 一次过拿完所有东西嘛.

User Secrets 把密码存到了 json file, 那在 VM (Virtual Machine Web Server), 我们也可以把密码 (json file 概念) 放到另一台机子上 (不要和程序的 VM 同一台)

这就是 Azure Key Vault 的工作了. Azure Key Vault 是微软的 Cloud Service. 就是提供一个密码保险箱. 它可以管理 Secret, Key, Certificate 等等. 这里我们主要关注 Secret 就好.

Create Azure Key Vault

首先我们需要有 Microsoft Azure 账号, 然后安装 Azure CLI (MSI)

安装好后开启 Windows PowerShell, 输入

az login

它会开启游览器然后登入 Azure 账号。

有时候可能会需要指定 TENANT_ID,像这样

az login --tenant 12312312-1233-1233-1233-123123123123

TENANT_ID 可以去 Azure Portal search "Azure Active Directory" 就会看见了,或者问 ChatGPT。

接着创建 resource group

az group create --name "TestKeyVault-RG" --location "SoutheastAsia"

如果已经有其它 Resource Group 也可以用 existing 的.

接着创建 Key Vault

az keyvault create --name "First-KV" --resource-group "TestKeyVault-RG" --location "Southeast Asia"

然后给予 user 权限

先找出 user Object ID

az ad user list --output table

这句会 list out 全部的 users。UserPrincipalName 是 user 的 email address,抄下来。

然后

az ad user show --id "user email address"

这句会显示 user 的 information,其中的 id 就是我们需要的 Object ID,抄下来。

然后

az keyvault show --name "First-KV"

把 id 抄下来

接着添加权限

az role assignment create --assignee "Object ID" --role "Key Vault Administrator" --scope "key-vault-id"

这样就有添加 secret 的权限了。

然后添加 secret

az keyvault secret set --vault-name "First-KV" --name "Account-Password" --value "my read password"

注: name 是 Account-Password 分隔符是 hyphen -, 因为 Azure 不支持分号 : 所以...什么鬼嘛...超麻烦的...

另外,我们的项目要访问 secret 也需要权限,用回上面的方法。(注:如果你的程序不是 host 在 Azure VM 那要用另外一招, 下面会教)

获取 VM 的 Object ID

az vm show --ids /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName} --query 'identity.principalId'

里面 3 个 parameters 分别是 subscriptionId, resourceGroupName, vmName

获取 subscriptionId 的方式是

az account show --query 'id' -o tsv

如果拿不到就省略掉 --query,直接

az account show

里面的 id 就是 subscriptionId,然后

az role assignment create --assignee "Object ID" --role "Key Vault Administrator" --scope "key-vault-id"

好了,搞定!

想知道更多 Azure CLI command 可以参考 Docs – az keyvault 或者问 ChatGPT 会更快.

Connect with Azure Key Vault

add package

dotnet add package Azure.Identity
dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets

program.cs

var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsProduction())
{
    builder.Configuration.AddAzureKeyVault(
        new Uri("https://first-kv.vault.azure.net/"),
        new DefaultAzureCredential(),
        new HyphenKeyVaultSecretManager()
    );
    var password = builder.Configuration.GetValue<string>("Account:Password"); // my read password
}

通过 AddAzureKeyVault 让 Azure Key Vault 关联进来.

获取 URI 的方式是 (其实也可以按照命名规范自己推断出来)

az keyvault show --name "First-KV" --query "properties.vaultUri"

class HyphenKeyVaultSecretManager 的作用是把 Azure Key Vault 的 hyphen - 换成分号 : (上面有提到...超麻烦的...)

public class HyphenKeyVaultSecretManager : KeyVaultSecretManager
{
    public override string GetKey(KeyVaultSecret secret)
    {
        return secret.Name.Replace("-", ConfigurationPath.KeyDelimiter);
    }
}

至此, 只要我们的程序是 host 在 Azure VM (和 Key Vault 相同 resource group) 那么它们就可以链接到了.

注: 或许你会发现本地测试也可以跑起来, 那是因为 Azure CLI login 了, 同时我们用相同 account create 了 Key Vault.

一旦 az logout 本地就无法访问了.

 

User Secrets with Azure Key Vault (non-Azure-hosted)

如果我们的程序不是运行在 Azure VM / App 的话, 过程会繁琐一些.

参考: Docs – Use Application ID and X.509 certificate for non-Azure-hosted apps

创建 Azure App Registration (AKA Client App)

首先我们需要搞一个 Client App, 它们的关系是 : 我们的程序 connect to Client App 然后由  Client App 去访问 Azure Key Vault.

az ad app create --display-name AzureKeyVault-AR

Add access policy for Client App

像上面我们授权给 VM 那样, Azure Key Vault 也需要授权给 Client App

az keyvault set-policy --name "First-KV" --secret-permissions get list --object-id "9e60491d-5161-43e2-b409-a580124944b8"

注意, 这里的 object-id 其实是放 application id 的值.

通过 az ad app 可以查看 Client App 的 application id 和 object id

az ad app list --display-name "AzureKeyVault-AR" --query "[].{id:id, appId:appId}" --output tsv

id 是 object id, appId 是 application id (AKA client id), 而 set-policy 的参数虽然叫 object-id 但正确的输入是放 application id 值哦, 它乱来, 我们可不要被误导哦.

Add certificate to Client App

早年 Azure 只是提供一个 Client Secret (AKA password), 但后来因为不够安全所以改成用 certificate 做非对称加密和签名.

具体怎样创建 certificate 可以看之前我写的 ASP.NET Core – Work with X509

创建 .pfx 和 .cer

using var algorithm = RSA.Create(keySizeInBits: 2048);
var subject = new X500DistinguishedName($"CN=AzureKeyVault"); // 名字顺便, 因为是 self-signed
var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
var certificate = request.CreateSelfSigned(
    notBefore: DateTimeOffset.UtcNow,
    notAfter: DateTimeOffset.UtcNow.AddYears(2)
);
var pfxBytes = certificate.Export(X509ContentType.Pfx, "my password");
var certBytes = certificate.Export(X509ContentType.Cert);
File.WriteAllBytes(Directory.GetCurrentDirectory() + "\\AzureKeyVault.pfx", pfxBytes);
File.WriteAllBytes(Directory.GetCurrentDirectory() + "\\AzureKeyVault.cer", certBytes);

.pfx 是给程序用的 .cer 是给 Azure Client App 用的.

upload .cer to Client App

az ad app credential upload --id "9e60491d-5161-43e2-b409-a580124944b8" --cert "@C:\keatkeat\my-projects\asp.net core\7.0\Secret\UserSecretsWebApp\AzureKeyVault.cer"

参数 --id 同样是指 application id 而不是 object id 哦.

Connect to Client App and Key Vault

var builder = WebApplication.CreateBuilder(args);

// read certificate from file
var rowData = File.ReadAllBytes(Directory.GetCurrentDirectory() + "\\AzureKeyVault.pfx");
var certificate = new X509Certificate2(rowData, "my password");

// connect to Client App and Key Vault
builder.Configuration.AddAzureKeyVault(
    new Uri("https://stgtestkeyvault-kv.vault.azure.net/"),
    new ClientCertificateCredential(
        tenantId: "30609328-e0b1-8cb0-974d-117ee17e02b5",
        clientId: "9e60491d-5161-43e2-b409-a580124944b8",
        certificate
    ),
    new HyphenKeyVaultSecretManager()
);
var password = builder.Configuration.GetValue<string>("Account:Password"); // my read password
Console.WriteLine(password); // my read password

1. 其实 certificate 应该要放在 store 里面的, 但因为 IIS read store 经常有权限问题, 这里只是为了演示而已, 所以我直接 read from file 就好了.

2. 获取 tenantId

az account show --query tenantId -o tsv

 

posted @ 2023-04-29 21:47  兴杰  阅读(141)  评论(0编辑  收藏  举报