dotnet-secrets-management について
.NET アプリケーション開発者向けの、クラウドに依存しないシークレット管理ガイドです。ローカル開発、運用環境の設定、ローテーション、マネージド ID に加え、ソース管理やログでの不適切な扱いを避ける方法も説明します。
主な機能
- 01ローカル開発での User Secrets
- 02運用環境での環境変数
- 03シークレットの強く型付けされたオプションへのバインド
- 04シークレットローテーションのパターン
- 05運用環境でのマネージド ID
- 06シークレット管理のアンチパターン解説
活用シーン
- 01ローカル開発用の機密値を設定
- 02運用環境でアプリのシークレットを注入
- 03停止時間を抑えるシークレットローテーションの設定
- 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 の既定の構成スタックでは、環境変数はファイルベースのソースより優先されます(後から読み込まれたものが優先されます)。
appsettings.jsonappsettings.{Environment}.json- ユーザーシークレット(Development のみ)
- 環境変数(上記すべてを上書き)
- コマンドライン引数
マッピング規則
.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
};
}
}
ローテーションのチェックリスト
- 新しいシークレットを古いシークレットと併せてデプロイします(デュアルキー期間)
- 古いシークレットと新しいシークレットの両方を受け入れるよう、アプリケーションを更新します
- デプロイをローリングし、すべてのインスタンスで新しいシークレットを署名/暗号化に使用します
- すべてのクライアントのローテーションが完了したら、古いシークレットを削除します
- すべてのローテーションイベントを監査ログに記録します
マネージド 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);
エージェントに関する注意点
- ハードコードされたシークレットを含むコードを生成しないでください -- シークレットの解決には、常に
IConfigurationまたはIOptions<T>を使用します。例でもプレースホルダー値を使用してください。 - 実際のシークレットを
appsettings.jsonに記述しないでください -- このファイルはソース管理にコミットされます。開発ではユーザーシークレット、本番環境では環境変数を使用してください。 - オプションクラスで
{ get; init; }を使用しないでください -- 構成バインダーには変更可能なセッターが必要です。代わりに{ get; set; }とデータ注釈による検証を使用してください。 ValidateOnStart()を省略しないでください -- 省略すると、シークレットの不足が明確な起動時エラーではなく、初回使用時の実行時エラーとして発生します。- シークレットの値をログに記録しないでください -- シークレットが構成されているかどうか(
IsConfigured: true/false)、またはメタデータ(接続文字列のサーバー名)を記録し、値は決して記録しないでください。 - ローテーションされるシークレットに
IOptions<T>を使用しないでください -- 再起動せずにローテーションできるよう、実行時に再読み込み可能なシークレットにはIOptionsMonitor<T>を使用してください。 - シークレットを Docker イメージに埋め込まないでください -- コンテナーの実行時に、環境変数またはマウントされたシークレットを使用してください。
前提条件
- .NET 8.0+(LTS の基準バージョン)
Microsoft.Extensions.Configuration.UserSecrets(ASP.NET Core SDK に含まれています。コンソールアプリでは手動で追加してください)ValidateDataAnnotations()用のMicrosoft.Extensions.Options.DataAnnotations