dotnet机密管理 dotnet-secrets-management

该技能用于管理.NET应用程序中的机密和敏感配置,涵盖完整生命周期,包括本地开发使用用户机密、生产环境使用环境变量、IConfiguration绑定模式、机密轮换策略以及托管身份作为生产最佳实践。关键词:机密管理、.NET、用户机密、环境变量、IConfiguration、机密轮换、托管身份、.NET安全、配置管理、开发运维。

DevOps 0 次安装 0 次浏览 更新于 3/6/2026

name: dotnet-secrets-management description: “管理机密和敏感配置。用户机密、环境变量、轮换。” user-invocable: false

dotnet机密管理

适用于.NET应用程序的云无关机密管理。涵盖完整生命周期:本地开发使用用户机密,生产环境使用环境变量,IConfiguration绑定模式,机密轮换,以及托管身份作为生产最佳实践。包括应避免的反模式(源代码中的机密、appsettings.json中的机密、硬编码连接字符串)。

范围

  • 本地开发的用户机密
  • 生产环境的环境变量
  • 机密的IConfiguration绑定模式
  • 机密轮换策略
  • 托管身份作为生产最佳实践
  • 应避免的反模式(源代码中的机密、appsettings.json中的机密)

超出范围

  • 云提供商特定的保险库服务(Azure Key Vault、AWS Secrets Manager、GCP Secret Manager)——参见[skill:dotnet-advisor]
  • 身份验证/授权实现(OAuth、Identity)——参见[skill:dotnet-api-security]和[skill:dotnet-blazor-auth]
  • 加密算法选择——参见[skill:dotnet-cryptography]
  • 通用选项模式和配置源——参见[skill:dotnet-csharp-configuration]

交叉引用:[skill:dotnet-security-owasp]用于OWASP A02(加密失败)和已弃用模式警告,[skill:dotnet-csharp-configuration]用于选项模式和配置源优先级。


机密生命周期

环境 机密源 机制
本地开发 用户机密 dotnet user-secrets CLI,仓库外的secrets.json
CI/CD 流水线变量 作为环境变量注入,永远不在YAML中
暂存/生产 环境变量或保险库 操作系统级环境变量、托管身份或保险库提供商

原则: 机密绝不能存在于源代码仓库或任何提交到版本控制的文件中。每个环境层使用适合其信任边界的机制。


用户机密(本地开发)

用户机密将敏感配置存储在项目目录外的用户配置文件中,防止意外提交。

设置

# 为项目初始化用户机密(在csproj中创建UserSecretsId)
dotnet user-secrets init

# 设置单个机密
dotnet user-secrets set "ConnectionStrings:DefaultDb" "Server=localhost;Database=myapp;User=sa;Password=dev123"
dotnet user-secrets set "Smtp:ApiKey" "SG.dev-key-here"
dotnet user-secrets set "Jwt:SigningKey" "dev-signing-key-min-32-chars-long!!"

# 列出当前机密
dotnet user-secrets list

# 移除机密
dotnet user-secrets remove "Smtp:ApiKey"

# 清除所有机密
dotnet user-secrets clear

工作原理

用户机密存储在:

  • Windows: %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json
  • macOS/Linux: ~/.microsoft/usersecrets/<UserSecretsId>/secrets.json

secrets.json文件是纯JSON,结构与appsettings.json相同:

{
  "ConnectionStrings": {
    "DefaultDb": "Server=localhost;Database=myapp;User=sa;Password=dev123"
  },
  "Smtp": {
    "ApiKey": "SG.dev-key-here"
  },
  "Jwt": {
    "SigningKey": "dev-signing-key-min-32-chars-long!!"
  }
}

在代码中加载

DOTNET_ENVIRONMENTASPNETCORE_ENVIRONMENT设置为Development时,WebApplication.CreateBuilderHost.CreateDefaultBuilder自动加载用户机密:

var builder = WebApplication.CreateBuilder(args);

// 用户机密已自动加载。通过IConfiguration访问:
var connectionString = builder.Configuration.GetConnectionString("DefaultDb");

对于非Web主机(控制台应用、工作服务):

var builder = Host.CreateApplicationBuilder(args);
// 在开发环境中自动加载用户机密。
// 用于显式控制:
if (builder.Environment.IsDevelopment())
{
    builder.Configuration.AddUserSecrets<Program>();
}

注意事项: 用户机密未加密——仅存储在仓库外。仅适用于开发,绝不用于生产。


环境变量(生产环境)

环境变量是标准机制,用于在不接触文件系统的情况下将机密注入生产应用程序。

配置优先级

在默认的ASP.NET Core配置栈中,环境变量覆盖基于文件的源(最后胜出):

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. 用户机密(仅开发环境)
  4. 环境变量(覆盖以上所有)
  5. 命令行参数

映射约定

.NET使用__(双下划线)作为节分隔符将环境变量映射到配置键:

# 这些环境变量映射到配置节:
export ConnectionStrings__DefaultDb="Server=prod-db;Database=myapp;..."
export Smtp__ApiKey="SG.production-key"
export Jwt__SigningKey="production-signing-key-256-bits"

# 带前缀(推荐以避免冲突):
export MYAPP_ConnectionStrings__DefaultDb="Server=prod-db;..."
// 加载带前缀的环境变量
builder.Configuration.AddEnvironmentVariables(prefix: "MYAPP_");

// 访问方式与任何配置源相同:
var smtpKey = builder.Configuration["Smtp:ApiKey"];

容器环境

# docker-compose.yml —— 通过环境注入机密
services:
  api:
    image: myapp:latest
    environment:
      - ConnectionStrings__DefaultDb=Server=db;Database=myapp;User=sa;Password=${DB_PASSWORD}
      - Smtp__ApiKey=${SMTP_API_KEY}
    env_file:
      - .env  # 不提交到源代码控制
# Dockerfile —— 不要将机密烘焙到镜像中
# 在运行时使用环境变量代替
ENV ASPNETCORE_URLS=http://+:8080
# 永远不要:ENV ConnectionStrings__DefaultDb="Server=..."

注意事项: 环境变量对同一用户下的所有进程可见。在多租户容器环境中,使用容器级隔离(Kubernetes机密、Docker机密)而非主机级环境变量。


IConfiguration绑定模式

将机密绑定到强类型选项类,以获得编译时安全和验证。

public sealed class JwtOptions
{
    public const string SectionName = "Jwt";

    [Required, MinLength(32)]
    public string SigningKey { get; set; } = "";

    /// <summary>
    /// 轮换窗口期间保留的先前签名密钥。
    /// 设置此项,以便使用旧密钥签名的令牌在过期前保持有效。轮换完成后移除。
    /// </summary>
    public string? PreviousSigningKey { get; set; }

    [Required]
    public string Issuer { get; set; } = "";

    [Required]
    public string Audience { get; set; } = "";

    [Range(1, 1440)]
    public int ExpirationMinutes { get; set; } = 60;
}

// 带验证的注册
builder.Services
    .AddOptions<JwtOptions>()
    .BindConfiguration(JwtOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart(); // 如果机密缺失,快速失败
// 注入和使用
public sealed class TokenService(IOptions<JwtOptions> jwtOptions)
{
    private readonly JwtOptions _jwt = jwtOptions.Value;

    public string GenerateToken(string userId)
    {
        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_jwt.SigningKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _jwt.Issuer,
            audience: _jwt.Audience,
            claims: [new Claim(ClaimTypes.NameIdentifier, userId)],
            expires: DateTime.UtcNow.AddMinutes(_jwt.ExpirationMinutes),
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

选项类必须使用{ get; set; }(而非{ get; init; }),因为配置绑定器和PostConfigure需要在构造后修改属性。使用数据注解属性([Required][MinLength])进行验证。

注意事项: ValidateOnStart()在应用程序启动时而非首次使用时捕获缺失的机密。始终为承载机密的选项使用它,以快速失败并提供清晰错误信息。


机密轮换

设计应用程序以处理机密轮换而无需停机。

轮换友好模式

// 对可能在运行时更改的机密使用IOptionsMonitor<T>
public sealed class EmailService(IOptionsMonitor<SmtpOptions> smtpOptions, ILogger<EmailService> logger)
{
    public async Task SendAsync(string to, string subject, string body)
    {
        // CurrentValue在每次调用时读取最新配置
        var options = smtpOptions.CurrentValue;
        logger.LogDebug("使用SMTP主机 {Host}", options.Host);

        // ... 使用当前选项发送邮件 ...
    }
}

// 通过托管服务审计日志配置更改。
// IHostedService始终由主机激活,因此订阅有保证。
public sealed class SmtpOptionsChangeLogger(
    IOptionsMonitor<SmtpOptions> monitor,
    ILogger<SmtpOptionsChangeLogger> logger) : IHostedService, IDisposable
{
    private IDisposable? _subscription;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _subscription = monitor.OnChange(options =>
        {
            logger.LogInformation("SMTP配置在 {Time} 重新加载", DateTime.UtcNow);
        });
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

    public void Dispose() => _subscription?.Dispose();
}

// 注册:
builder.Services.AddHostedService<SmtpOptionsChangeLogger>();
// 用于零停机轮换的双密钥验证
// 在轮换窗口期间接受旧的和新的签名密钥
public sealed class DualKeyTokenValidator(IOptionsMonitor<JwtOptions> optionsMonitor)
{
    public TokenValidationParameters GetParameters()
    {
        // 每次调用读取CurrentValue,以便无需重启应用程序即可拾取轮换的密钥
        var options = optionsMonitor.CurrentValue;

        var keys = new List<SecurityKey>
        {
            new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.SigningKey))
        };

        if (!string.IsNullOrEmpty(options.PreviousSigningKey))
        {
            keys.Add(new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(options.PreviousSigningKey)));
        }

        return new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = options.Issuer,
            ValidateAudience = true,
            ValidAudience = options.Audience,
            ValidateLifetime = true,
            IssuerSigningKeys = keys
        };
    }
}

轮换清单

  1. 将新机密与旧机密一起部署(双密钥窗口)
  2. 更新应用程序以接受旧的和新的机密
  3. 滚动部署,使所有实例使用新机密进行签名/加密
  4. 所有客户端轮换后,移除旧机密
  5. 审计记录每个轮换事件

托管身份(生产最佳实践)

托管身份通过使用平台的身份系统对服务进行身份验证,完全消除机密的必要性。

概念: 应用程序无需存储带密码的连接字符串,而是使用其平台分配的身份对数据库/服务进行身份验证。无需管理、轮换或泄露机密。

// 示例:使用DefaultAzureCredential与SQL Server进行无密码连接
// 此模式适用于Azure,AWS和GCP有类似模式
var connectionString = "Server=myserver.database.windows.net;Database=mydb;Authentication=Active Directory Default";
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));
// 连接字符串中无密码——身份从环境中解析

何时使用托管身份:

  • 托管在云平台上的生产和暂存环境
  • 平台支持身份联合的任何服务间通信
  • 数据库连接、消息队列访问、存储访问

何时仍需机密:

  • 仅支持API密钥的第三方API
  • 无身份联合的遗留系统
  • 本地开发(使用用户机密作为备用)

反模式

源代码中的机密

// 永远不要:源代码中的硬编码机密
private const string ApiKey = "sk-live-abc123def456";           // 错误
private const string ConnectionString = "Server=prod;Password=secret"; // 错误

修复: 使用用户机密(开发)或环境变量(生产)。参见以上章节。

appsettings.json中的机密

// 永远不要:appsettings.json中的真实凭据(提交到仓库)
{
  "ConnectionStrings": {
    "DefaultDb": "Server=prod-db;Password=RealPassword123!"
  }
}

修复: appsettings.json应仅包含非敏感默认值。使用明显失败的占位符值:

{
  "ConnectionStrings": {
    "DefaultDb": "Server=localhost;Database=myapp;Integrated Security=true"
  },
  "Smtp": {
    "ApiKey": "REPLACE_VIA_ENV_OR_USER_SECRETS"
  }
}

硬编码连接字符串

// 永远不要:代码中的直接连接字符串
var connection = new SqlConnection("Server=prod-db;Database=myapp;User=sa;Password=P@ssw0rd!");

修复: 始终从IConfiguration解析连接字符串:

// 正确:从配置解析
public sealed class OrderRepository(IConfiguration configuration)
{
    private readonly string _connectionString =
        configuration.GetConnectionString("DefaultDb")
        ?? throw new InvalidOperationException("ConnectionStrings:DefaultDb未配置");
}

// 更好:使用带验证的选项模式
public sealed class OrderRepository(IOptions<DatabaseOptions> options)
{
    private readonly string _connectionString = options.Value.ConnectionString;
}

记录机密

// 永远不要:记录机密值
logger.LogInformation("使用API密钥: {ApiKey}", apiKey);       // 错误
logger.LogDebug("连接字符串: {Conn}", connectionString); // 错误

修复: 记录机密是否已配置,而非其值:

logger.LogInformation("API密钥配置: {IsConfigured}", !string.IsNullOrEmpty(apiKey));
logger.LogInformation("为{Server}配置数据库连接", new SqlConnectionStringBuilder(connectionString).DataSource);

代理注意事项

  1. 不要生成硬编码机密的代码——始终使用IConfigurationIOptions<T>解析机密。即使在示例中,也使用占位符值。
  2. 不要在appsettings.json中放入真实机密——它提交到源代码控制。开发使用用户机密,生产使用环境变量。
  3. 不要在选项类上使用{ get; init; }——配置绑定器需要可变设置器。使用带数据注解验证的{ get; set; }代替。
  4. 不要跳过ValidateOnStart()——没有它,缺失的机密会在首次使用时导致运行时失败,而非清晰的启动错误。
  5. 不要记录机密值——记录机密是否配置(IsConfigured: true/false)或元数据(连接字符串中的服务器名),永远不要记录值。
  6. 不要对轮换的机密使用IOptions<T>——对运行时可重新加载的机密使用IOptionsMonitor<T>,以便轮换无需重启。
  7. 不要将机密烘焙到Docker镜像中——在容器运行时使用环境变量或挂载的机密。

先决条件

  • .NET 8.0+(LTS基线)
  • Microsoft.Extensions.Configuration.UserSecrets包含在ASP.NET Core SDK中;控制台应用需手动添加)
  • Microsoft.Extensions.Options.DataAnnotations用于ValidateDataAnnotations()

参考