ASP.NET Core 密钥云存储推荐
本地文件持久化的局限性
在实际开发 ASP.NET Core 应用的过程中,数据安全一直是一个不能忽略的问题。涉及到用户认证票据、CSRF 令牌等敏感信息时,Data Protection 系统就成为了一个不可缺少的部分。但是数据保护系统底层依赖于密钥存储的位置和安全属性,稍有不慎就会埋下日后的隐患。今天,我想跟大家聊聊为什么我建议大家在生产环境中优先选择把密钥存放在 Azure Blob 存储中,并且用 Azure Managed Identity 来管理访问权限。
在大多数的本地开发环境或者测试服务器中,出于便捷的考虑,ASP.NET Core 默认会把密钥存放到文件系统固定目录下,Windows的%LOCALAPPDATA%\ASP.NET\DataProtection-Keys,MacOS/Linux的$HOME/.aspnet/DataProtection-Keys。看起来没有问题,对调试、开发来说比较方便。但是,当项目进入云端、服务需要弹性扩展,或者出现多实例并发部署的情况时,文件系统密钥存储立刻就暴露了它的不足:密钥不能在实例之间自动同步,应用横向扩展之后实例之间对密钥并不了解;而且应用重启、服务器升级甚至镜像更换都有丢失密钥的风险。最糟糕的是,如果某次部署没有做好密钥迁移,历史加密文件就会出现无法解密的情况,亲身体验过之后真的会让人对文件型存储感到恐惧。
Azure Blob 存储的优势
这个时候,Azure Blob 存储方案就显得比较安全。最大的优点就是可以天然适配云原生场景,不管有多少个 Web 应用实例,上百个服务横跨多少个可用区,只要配置好访问权限,所有的应用实例都可以访问到同一个密钥仓库,保证密钥的统一、持久和安全。特别是开发大团队项目或者采用微服务架构的时候,这样的统一体验是无法替代的。密钥存放在Blob之后,实例间的Cookie票据、CSRF令牌等加密信息可以顺利交互,带来的开发简化和运维安全毫不夸张地说,是结构性的飞跃。
安全优先,推荐 Azure 托管身份
但是把密钥存放在Blob中还不足以称作安全的最佳实践。数据保护密钥本身就是应用的“命门”,因此需要考虑如何使密钥的获取和使用变得最小化、自动化。以前有的同学用存储账号和密钥(连接字符串带密码)去连接存储账户,虽然解决了共享的问题,但是我也在项目中遇到过很多坑。例如密码多处分发、交接流程混乱、安全漏洞等问题,都会使数据防护本来的目的大打折扣。
近几年来,我建议大家多使用Azure的托管标识(Managed Identity)。简单来说就是应用服务有了自己的身份凭证,只用到最小的权限,这样就不会存在存储明文连接字符串、泄露存储凭据的风险了。配置方式也很透明,在Azure Portal中为App Service分配或者新建一个User Assigned Managed Identity,在Blob账户控制面板的访问控制(IAM)中授予该标识Storage Blob Data Contributor的角色,基本上三步就可以完成密钥仓库的配置了。以后应用服务会自动使用系统身份和Azure进行交互,不需要我们手动去维护密钥,也不会把敏感的账户信息交给开发团队,大大降低了人为失误和安全事件发生的机会。
// 在 Azure Portal 为 App Service 分配 User Assigned Managed Identity,并在 Blob 存储账户中赋予 Storage Blob Data Contributor 角色
如果你的项目中还有本地开发调试或者需要开发人员临时访问 Blob 密钥仓库的地方,也可以通过 Azure CLI 登录到开发者 Azure 用户,并给指定开发账号相同的用户数据贡献者权限,做到权限细致划分,不会影响到生产密钥管理。开发代入感和零感知的集成体验,传统连接字符串方案无法比拟的。
// 本地开发情况下,可用 Azure CLI 登录并授权开发账号
az login
代码层实现和替代方案
在实际操作中,该方案的具体体现就是非常直接的。比如在注册服务时通过PersistKeysToAzureBlobStorage提供Blob URI和TokenCredential,系统就会自动用上设定的Managed Identity角色来执行加密、解密操作。所有的服务实例都会被托管身份所覆盖,从而实现了去凭证化的凭权访问,有效地避免了“密码到处都是”“密钥满天飞”的尴尬。
TokenCredential? credential;
if (builder.Environment.IsProduction())
{
credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
// 本地开发和测试
DefaultAzureCredentialOptions options = new()
{
VisualStudioTenantId = "{TENANT ID}",
SharedTokenCacheTenantId = "{TENANT ID}"
};
credential = new DefaultAzureCredential(options);
}
builder.Services.AddDataProtection()
.SetApplicationName("{APPLICATION NAME}")
.PersistKeysToAzureBlobStorage(new Uri("{BLOB URI}"), credential);
当然,你可能会问,如果非得用传统的 SAS 地址或者连接字符串行不可的话?答案是可以的,但是只建议在特殊情况下使用,比如历史遗留系统不能快速切换新的认证模式,或者临时测试用例。对于长期运行的生产环境来说,托管标识的安全性以及管理的便利性并不是SAS和connection string可以比拟的。在进行连接字符串迁移到托管标识的过程中,我深深地体会到风险管理成本的降低以及心态的轻松。唯一要注意的是,托管身份授权流程需要管理员配合操作,开发初期可能有门槛,但从长期的安全收益来看是非常值得的。
// 传统 SAS token 方式,仅建议特殊场景下使用
builder.Services.AddDataProtection()
.SetApplicationName("{APPLICATION NAME}")
.PersistKeysToAzureBlobStorage(new Uri("{BLOB URI WITH SAS}"));
权限分配细则
最后一个容易被忽略但非常重要的点就是:并不是所有的身份都需要拥有很大的权限。不管是用哪种云端密钥存储方式,都要确保只给应用服务所需要的最小角色,比如Blob Data Contributor,千万不要直接给Owner或者Storage Account Contributor这样的高权限角色,否则一旦某个服务被攻破就会造成不可挽回的损失。
总结与建议
“ 因此,我始终建议在生产环境中将 ASP.NET Core 密钥持久化选项固定在 Azure Blob 存储,并且配合托管身份。它解决了多实例统一访问、安全隔离、权限细粒度的问题,而且也提高了管理上的安心感、安全性。正如我在多个项目和团队落地过程中所体会到的那样,把密钥的存储和权限管理标准化、云原生化,就是目前 ASP.NET Core 应用从高效走向安全的必经之路。如果当前项目还处于文件或者传统的数据库阶段的话,不妨用一两个星期的时间来进行迁移尝试,安全收益一定远远超过预期。