Claude
Skills
Sign in
Back

dotnet-secrets-management

Included with Lifetime
$97 forever

Handling secrets or sensitive configuration. User secrets, environment variables, rotation.

General

What this skill does


# dotnet-secrets-management

Cloud-agnostic secrets management for .NET applications. Covers the full lifecycle: user secrets for local development, environment variables for production, IConfiguration binding patterns, secret rotation, and managed identity as a production best practice. Includes anti-patterns to avoid (secrets in source, appsettings.json, hardcoded connection strings).

**Out of scope:** Cloud-provider-specific vault services (Azure Key Vault, AWS Secrets Manager, GCP Secret Manager) -- those are covered by cloud-specific epics. Authentication/authorization implementation (OAuth, Identity) -- see [skill:dotnet-api-security] and [skill:dotnet-blazor-auth]. Cryptographic algorithm selection -- see [skill:dotnet-cryptography]. General Options pattern and configuration sources -- see [skill:dotnet-csharp-configuration].

Cross-references: [skill:dotnet-security-owasp] for OWASP A02 (Cryptographic Failures) and deprecated pattern warnings, [skill:dotnet-csharp-configuration] for Options pattern and configuration source precedence.

---

## Secrets Lifecycle

| Environment | Secret Source | Mechanism |
|-------------|-------------|-----------|
| Local dev | User secrets | `dotnet user-secrets` CLI, `secrets.json` outside repo |
| CI/CD | Pipeline variables | Injected as environment variables, never in YAML |
| Staging/Production | Environment variables or vault | OS-level env vars, managed identity, or vault provider |

**Principle:** Secrets must never exist in the source repository or in any file committed to version control. Each environment tier uses the appropriate mechanism for its trust boundary.

---

## User Secrets (Local Development)

User secrets store sensitive configuration outside the project directory in the user profile, preventing accidental commits.

### Setup

```bash
# 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
```

### How It Works

User secrets are stored at:
- **Windows:** `%APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json`
- **macOS/Linux:** `~/.microsoft/usersecrets/<UserSecretsId>/secrets.json`

The `secrets.json` file is plain JSON with the same structure as `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!!"
  }
}
```

### Loading in Code

User secrets are loaded automatically by `WebApplication.CreateBuilder` and `Host.CreateDefaultBuilder` when `DOTNET_ENVIRONMENT` or `ASPNETCORE_ENVIRONMENT` is `Development`:

```csharp
var builder = WebApplication.CreateBuilder(args);

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

For non-web hosts (console apps, worker services):

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

**Gotcha:** User secrets are not encrypted -- they are just stored outside the repo. They are appropriate for development only, never for production.

---

## Environment Variables (Production)

Environment variables are the standard mechanism for injecting secrets into production applications without touching the filesystem.

### Configuration Precedence

In the default ASP.NET Core configuration stack, environment variables override file-based sources (last wins):

1. `appsettings.json`
2. `appsettings.{Environment}.json`
3. User secrets (Development only)
4. **Environment variables** (overrides all above)
5. Command-line arguments

### Mapping Convention

.NET maps environment variables to configuration keys using `__` (double underscore) as the section separator:

```bash
# 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;..."
```

```csharp
// Load prefixed environment variables
builder.Configuration.AddEnvironmentVariables(prefix: "MYAPP_");

// Access the same way as any configuration source:
var smtpKey = builder.Configuration["Smtp:ApiKey"];
```

### Container Environments

```yaml
# 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
# Dockerfile -- do NOT bake secrets into images
# Use environment variables at runtime instead
ENV ASPNETCORE_URLS=http://+:8080
# NEVER: ENV ConnectionStrings__DefaultDb="Server=..."
```

**Gotcha:** Environment variables are visible to all processes under the same user. In multi-tenant container environments, use container-level isolation (Kubernetes secrets, Docker secrets) rather than host-level env vars.

---

## IConfiguration Binding Patterns

Bind secrets to strongly typed options classes for compile-time safety and validation.

```csharp
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
```

```csharp
// 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);
    }
}
```

> Options classes must use `{ get; set; }` (not `{ get; init; }`) because the configuration binder and `PostConfigure` need to mutate properties after construction. Use data annotation attributes (`[Required]`, `[MinLength]`) for validation.

**Gotcha:** `ValidateOnStart()` catches missing secrets at application startup rather than at first use. Always use it for secrets-bearing options to fail fast with a clear error message.

---

## Secret Rotation

Design a

Related in General