mcpField
D
dotnet-secrets-management

dotnet-secrets-management

github.com/wshaddixセキュリティ8683 回インストール35 週間前に更新
リポジトリ

dotnet-secrets-management について

.NET アプリケーション開発者向けの、クラウドに依存しないシークレット管理ガイドです。ローカル開発、運用環境の設定、ローテーション、マネージド ID に加え、ソース管理やログでの不適切な扱いを避ける方法も説明します。

主な機能

  1. 01ローカル開発での User Secrets
  2. 02運用環境での環境変数
  3. 03シークレットの強く型付けされたオプションへのバインド
  4. 04シークレットローテーションのパターン
  5. 05運用環境でのマネージド ID
  6. 06シークレット管理のアンチパターン解説

活用シーン

  1. 01ローカル開発用の機密値を設定
  2. 02運用環境でアプリのシークレットを注入
  3. 03停止時間を抑えるシークレットローテーションの設定
  4. 04コードやログにおけるシークレット露出リスクの確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-secrets-management

インストール後、Claude Code で/dotnet-secrets-managementを入力して読み込まれたか確認します。

詳細

dotnet-secrets-management

.NET アプリケーション向けの、クラウドに依存しないシークレット管理です。ローカル開発用のユーザーシークレット、本番環境用の環境変数、IConfiguration のバインドパターン、シークレットのローテーション、本番環境のベストプラクティスとしてのマネージド ID など、ライフサイクル全体を扱います。ソースコード内のシークレット、appsettings.json、ハードコードされた接続文字列など、避けるべきアンチパターンも含まれます。

対象外: クラウドプロバイダー固有のボールトサービス(Azure Key Vault、AWS Secrets Manager、GCP Secret Manager)は、クラウド固有のエピックで扱います。認証/認可の実装(OAuth、Identity)については、[skill:dotnet-api-security] と [skill:dotnet-blazor-auth] を参照してください。暗号アルゴリズムの選定については、[skill:dotnet-cryptography] を参照してください。一般的な Options パターンと構成ソースについては、[skill:dotnet-csharp-configuration] を参照してください。

相互参照:OWASP A02(暗号化の失敗)と非推奨パターンの警告については [skill:dotnet-security-owasp]、Options パターンと構成ソースの優先順位については [skill:dotnet-csharp-configuration] を参照してください。


シークレットのライフサイクル

環境シークレットソース仕組み
ローカル開発ユーザーシークレットdotnet user-secrets CLI、リポジトリ外の secrets.json
CI/CDパイプライン変数環境変数として注入し、YAML には決して記述しない
ステージング/本番環境環境変数またはボールトOS レベルの環境変数、マネージド ID、またはボールトプロバイダー

原則: シークレットをソースリポジトリや、バージョン管理にコミットされるファイルに保存してはなりません。各環境層では、それぞれの信頼境界に適した仕組みを使用します。


ユーザーシークレット(ローカル開発)

ユーザーシークレットは、機密性の高い構成をプロジェクトディレクトリの外にあるユーザープロファイルへ保存し、誤ってコミットするのを防ぎます。

セットアップ
# Initialize user secrets for a project (creates UserSecretsId in csproj)
dotnet user-secrets init

# Set individual secrets
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!!"

# List current secrets
dotnet user-secrets list

# Remove a secret
dotnet user-secrets remove "Smtp:ApiKey"

# Clear all secrets
dotnet user-secrets clear
仕組み

ユーザーシークレットは次の場所に保存されます。

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

secrets.json ファイルは、appsettings.json と同じ構造のプレーンな 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_ENVIRONMENT または ASPNETCORE_ENVIRONMENT が Development の場合、WebApplication.CreateBuilder と Host.CreateDefaultBuilder によってユーザーシークレットが自動的に読み込まれます。

var builder = WebApplication.CreateBuilder(args);

// User secrets are already loaded. Access them via IConfiguration:
var connectionString = builder.Configuration.GetConnectionString("DefaultDb");

Web 以外のホスト(コンソールアプリ、ワーカーサービス)の場合:

var builder = Host.CreateApplicationBuilder(args);
// User secrets are loaded automatically in Development environment.
// For explicit control:
if (builder.Environment.IsDevelopment())
{
    builder.Configuration.AddUserSecrets<Program>();
}

注意点: ユーザーシークレットは暗号化されません。リポジトリの外に保存されるだけです。開発環境でのみ使用し、本番環境では決して使用しないでください。


環境変数(本番環境)

環境変数は、ファイルシステムに触れることなく本番アプリケーションにシークレットを渡すための標準的な仕組みです。

構成の優先順位

ASP.NET Core の既定の構成スタックでは、環境変数はファイルベースのソースより優先されます(後から読み込まれたものが優先されます)。

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. ユーザーシークレット(Development のみ)
  4. 環境変数(上記すべてを上書き)
  5. コマンドライン引数
マッピング規則

.NET では、環境変数の __(アンダースコア 2 つ)をセクション区切り文字として構成キーにマッピングします。

# These environment variables map to configuration sections:
export ConnectionStrings__DefaultDb="Server=prod-db;Database=myapp;..."
export Smtp__ApiKey="SG.production-key"
export Jwt__SigningKey="production-signing-key-256-bits"

# With a prefix (recommended to avoid collisions):
export MYAPP_ConnectionStrings__DefaultDb="Server=prod-db;..."
// Load prefixed environment variables
builder.Configuration.AddEnvironmentVariables(prefix: "MYAPP_");

// Access the same way as any configuration source:
var smtpKey = builder.Configuration["Smtp:ApiKey"];
コンテナー環境
# docker-compose.yml -- inject secrets via environment
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  # NOT committed to source control
# Dockerfile -- do NOT bake secrets into images
# Use environment variables at runtime instead
ENV ASPNETCORE_URLS=http://+:8080
# NEVER: ENV ConnectionStrings__DefaultDb="Server=..."

注意点: 環境変数は、同じユーザーで実行されるすべてのプロセスから参照できます。マルチテナントのコンテナー環境では、ホストレベルの環境変数ではなく、コンテナーレベルで分離する仕組み(Kubernetes secrets、Docker secrets)を使用してください。


IConfiguration のバインディングパターン

コンパイル時の安全性と検証のため、シークレットを型安全なオプションクラスにバインドします。

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

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

    /// <summary>
    /// Previous signing key retained during rotation window.
    /// Set this when rotating keys so tokens signed with the old key
    /// remain valid until they expire. Remove after rotation completes.
    /// </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;
}

// Registration with validation
builder.Services
    .AddOptions<JwtOptions>()
    .BindConfiguration(JwtOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart(); // Fail fast if secrets are missing
// Inject and use
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; init; } ではなく { get; set; } を使用してください。構成バインダーと PostConfigure が、構築後にプロパティを変更する必要があるためです。検証にはデータ注釈属性([Required]、[MinLength])を使用してください。

注意点: ValidateOnStart() を使用すると、シークレットが初回使用時ではなくアプリケーションの起動時に不足していることを検出できます。シークレットを含むオプションでは、明確なエラーメッセージを表示して迅速に失敗できるよう、必ず使用してください。


シークレットのローテーション

ダウンタイムなしでシークレットのローテーションに対応できるよう、アプリケーションを設計します。

ローテーションに対応しやすいパターン
// Use IOptionsMonitor<T> for secrets that may change at runtime
public sealed class EmailService(IOptionsMonitor<SmtpOptions> smtpOptions, ILogger<EmailService> logger)
{
    public async Task SendAsync(string to, string subject, string body)
    {
        // CurrentValue reads the latest configuration on every call
        var options = smtpOptions.CurrentValue;
        logger.LogDebug("Using SMTP host {Host}", options.Host);

        // ... send email using current options ...
    }
}

// Audit-log configuration changes via a hosted service.
// IHostedService is always activated by the host, so the subscription is guaranteed.
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 configuration reloaded at {Time}", DateTime.UtcNow);
        });
        return Task.CompletedTask;
    }

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

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

// Registration:
builder.Services.AddHostedService<SmtpOptionsChangeLogger>();
// Dual-key validation for zero-downtime rotation
// Accept both old and new signing keys during rotation window
public sealed class DualKeyTokenValidator(IOptionsMonitor<JwtOptions> optionsMonitor)
{
    public TokenValidationParameters GetParameters()
    {
        // Read CurrentValue on every call so rotated keys are picked up
        // without restarting the application
        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. すべてのローテーションイベントを監査ログに記録します

マネージド ID(本番環境でのベストプラクティス)

マネージド ID はプラットフォームの ID システムを使用してサービスに認証するため、クラウドでホストされるアプリケーションのシークレットを不要にします。

概念: パスワードを含む接続文字列を保存する代わりに、アプリケーションはプラットフォームから割り当てられた ID を使用してデータベース/サービスに認証します。管理、ローテーション、漏えいの心配があるシークレットはありません。

// Example: passwordless connection to SQL Server using DefaultAzureCredential
// This pattern works across Azure, and similar patterns exist for AWS and GCP
var connectionString = "Server=myserver.database.windows.net;Database=mydb;Authentication=Active Directory Default";
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));
// No password in the connection string -- identity is resolved from the environment

マネージド ID を使用する場合:

  • クラウドプラットフォームでホストされる本番環境およびステージング環境
  • プラットフォームが ID フェデレーションに対応しているサービス間通信
  • データベース接続、メッセージキューへのアクセス、ストレージへのアクセス

それでもシークレットが必要な場合:

  • API キーのみをサポートするサードパーティ API
  • ID フェデレーションに対応していないレガシーシステム
  • ローカル開発(代替手段としてユーザーシークレットを使用)

アンチパターン

ソース管理内のシークレット
// NEVER: hardcoded secrets in source code
private const string ApiKey = "sk-live-abc123def456";           // WRONG
private const string ConnectionString = "Server=prod;Password=secret"; // WRONG

解決策: 開発ではユーザーシークレット、本番環境では環境変数を使用します。上記のセクションを参照してください。

appsettings.json 内のシークレット
// NEVER: real credentials in appsettings.json (committed to repo)
{
  "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"
  }
}
ハードコードされた接続文字列
// NEVER: connection strings directly in code
var connection = new SqlConnection("Server=prod-db;Database=myapp;User=sa;Password=P@ssw0rd!");

解決策: 接続文字列は常に IConfiguration から解決します。

// Correct: resolve from configuration
public sealed class OrderRepository(IConfiguration configuration)
{
    private readonly string _connectionString =
        configuration.GetConnectionString("DefaultDb")
        ?? throw new InvalidOperationException("ConnectionStrings:DefaultDb is not configured");
}

// Better: use Options pattern with validation
public sealed class OrderRepository(IOptions<DatabaseOptions> options)
{
    private readonly string _connectionString = options.Value.ConnectionString;
}
シークレットのログ出力
// NEVER: log secret values
logger.LogInformation("Using API key: {ApiKey}", apiKey);       // WRONG
logger.LogDebug("Connection string: {Conn}", connectionString); // WRONG

解決策: シークレットの値ではなく、読み込まれたことをログに記録します。

logger.LogInformation("API key configured: {IsConfigured}", !string.IsNullOrEmpty(apiKey));
logger.LogInformation("Database connection configured for {Server}", new SqlConnectionStringBuilder(connectionString).DataSource);

エージェントに関する注意点

  1. ハードコードされたシークレットを含むコードを生成しないでください -- シークレットの解決には、常に IConfiguration または IOptions<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 に含まれています。コンソールアプリでは手動で追加してください)
  • ValidateDataAnnotations() 用の Microsoft.Extensions.Options.DataAnnotations

参考資料

コメント