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 篇
这篇作为最新最完整的版本呗.
参考
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