Manage Secrets and Config in .NET Apps

Khimananda Oli 7 min read Programming and Languages
Manage Secrets and Config in .NET Apps

By Khimananda Oli | Last reviewed: August 2026

Hardcoded connection strings and API keys remain the most common security vulnerability I encounter during infrastructure audits, even in mature enterprise codebases. To properly manage secrets and config in .NET apps, you must decouple sensitive values from source control while maintaining a seamless developer experience across local, staging, and production environments. This guide provides the exact configuration hierarchy and tooling required to secure your application without sacrificing deployment velocity.

appsettings.jsonBase DefaultsUser SecretsLocal Dev OnlyEnvironment VarsContainer / VMAzure Key VaultProduction SourceConfiguration Override Flow (Low → High Priority)Higher priority providers overwrite identical keys from lower priority sources
Figure 1: Configuration provider hierarchy when you manage secrets and config in .NET apps, showing how production sources override base defaults.

How do you manage secrets and config in .NET apps locally without Git?

The Microsoft.Extensions.Configuration.UserSecrets package is your first line of defense during development. It stores sensitive data outside the project tree in a JSON file located at %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json on Windows or ~/.microsoft/usersecrets/<user_secrets_id>/secrets.json on Linux/macOS. Because this path lives outside your repository root, accidental commits are structurally impossible.

Initialize and populate User Secrets

  1. Right-click your project in Visual Studio and select Manage User Secrets, or run the CLI command below to generate a unique UserSecretsId GUID in your .csproj:
dotnet user-secrets init --project src/MyWebApp/MyWebApp.csproj
  1. Set individual secrets using the colon-delimited key format that mirrors your configuration hierarchy:
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;Database=DevDb;Trusted_Connection=true;"
dotnet user-secrets set "PaymentGateway:ApiKey" "sk_test_dev_key_never_in_git"
  1. Verify the stored values without opening the file directly:
dotnet user-secrets list

A common mistake I see in teams new to .NET is treating User Secrets as an encrypted store. They are plaintext JSON files intended solely for local development convenience. Never use them on production servers or CI runners. For deeper context on securing pipeline credentials before they reach your app, review handling secrets in CI/CD pipelines safely.

How does the .NET configuration provider hierarchy work?

Understanding precedence is non-negotiable when you manage secrets and config in .NET apps. The framework builds configuration from multiple sources in a specific order, where later providers override earlier ones for identical keys. This design allows you to keep safe defaults in source control while letting secure external sources win at runtime.

  • appsettings.json — Base configuration committed to Git. Contains non-sensitive defaults like logging levels, feature flags, and public endpoints.
  • appsettings.{Environment}.json — Environment-specific overrides (e.g., appsettings.Production.json). Still committed, still non-sensitive.
  • User Secrets — Local development only. Active when ASPNETCORE_ENVIRONMENT=Development. Overrides both JSON files above.
  • Environment Variables — Container and VM runtime injection. Uses double-underscore (__) or colon delimiter for nested keys. Overrides all file-based sources.
  • Azure Key Vault / External Providers — Highest priority when configured. Fetches secrets at startup or via refresh. Wins over everything else.

This layered approach means your code never needs conditional logic like if (isProduction). You simply request config["ConnectionStrings:DefaultConnection"] and trust the host environment to supply the correct value. If you are deploying to Kubernetes, understanding how this interacts with platform-native secret management is critical; see Kubernetes secrets management done right for container-specific patterns.

Host BuilderRegisters ProvidersIConfiguration RootMerged Key-Value StoreOptions PatternStrongly-Typed BindingService / ControllerConsumes IOptions<T>Runtime Secret Resolution PipelineSecrets resolve once at startup; Options bind validated strongly-typed objects
Figure 2: Runtime resolution pipeline showing how merged configuration flows into strongly-typed options when you manage secrets and config in .NET apps.

How do you integrate Azure Key Vault with .NET configuration?

For production Azure workloads, Azure Key Vault should be your authoritative secret source. The Azure.Extensions.AspNetCore.Configuration.Secrets package adds Key Vault as a configuration provider, making secrets available through the same IConfiguration interface you already use.

Configure Key Vault in Program.cs

using Azure.Identity;
using Azure.Extensions.AspNetCore.Configuration.Secrets;

var builder = WebApplication.CreateBuilder(args);

// Add Azure Key Vault as highest-priority provider
builder.Configuration.AddAzureKeyVault(
    new Uri("https://my-app-vault.vault.azure.net/"),
    new DefaultAzureCredential(),
    options =>
    {
        options.SecretFilter = secret => 
            secret.Properties.Tags.ContainsKey("Environment") &&
            secret.Properties.Tags["Environment"] == builder.Environment.EnvironmentName;
    });

builder.Services.Configure<PaymentSettings>(
    builder.Configuration.GetSection("PaymentGateway"));

Always use DefaultAzureCredential instead of client secrets or connection strings. This enables managed identity authentication in Azure and falls back to developer credentials locally without code changes. Tag-based filtering prevents staging secrets from leaking into production and reduces startup latency by limiting fetched secrets.

Bind secrets to strongly-typed options

Never access configuration via magic strings in business logic. Define a POCO class and bind it:

public class PaymentSettings
{
    public string ApiKey { get; set; } = string.Empty;
    public string WebhookSecret { get; set; } = string.Empty;
    public int TimeoutSeconds { get; set; } = 30;
}

// In service registration
builder.Services.Configure<PaymentSettings>(
    builder.Configuration.GetSection("PaymentGateway"));

// In controller/service constructor
public class CheckoutService(IOptions<PaymentSettings> options)
{
    private readonly PaymentSettings _settings = options.Value;
}

This pattern gives you compile-time safety, validation attributes, and testability. If you need broader observability into how these configurations affect runtime behavior, consider instrumenting your app with OpenTelemetry to trace configuration-dependent failures.

ProviderBest ForSecurity LevelCostRefresh Support
User SecretsLocal developmentLow (plaintext)FreeManual reload
Environment VariablesContainers, VMs, CIMedium (host-level)FreeRestart required
Azure Key VaultAzure production workloadsHigh (HSM-backed)Per-operationPolling or push
AWS Secrets ManagerAWS production workloadsHigh (KMS-encrypted)Per-secret + APILambda rotation
HashiCorp VaultMulti-cloud, on-premHighest (dynamic)Self-hosted/EnterpriseDynamic leases

What are common mistakes when managing .NET configuration?

Even experienced teams fall into predictable traps. Avoid these failures I repeatedly audit:

  • Committing appsettings.Production.json with real values. Environment-specific JSON files should contain only structural placeholders or non-sensitive overrides. Actual production values belong in Key Vault or environment variables.
  • Using IConfiguration directly in services. Always prefer IOptions<T> or IOptionsSnapshot<T>. Direct dictionary access bypasses validation, creates hidden dependencies, and makes unit testing painful.
  • Ignoring secret rotation. Static secrets are liabilities. Use Azure Key Vault's auto-rotation policies or AWS Secrets Manager Lambda rotators. Your app should handle transient auth failures gracefully.
  • Logging configuration values. Structured logging frameworks can accidentally serialize entire configuration sections. Configure redaction for any key containing "password", "secret", "key", or "token". Review structured logging best practices for safe instrumentation patterns.
  • Missing validation at startup. Use ValidateOnStart() with the Options builder to fail fast if required secrets are missing rather than discovering the gap during the first user request.
❌ Insecure PatternHardcoded strings in C# source filesReal credentials in appsettings.Production.jsonClient secrets stored in CI/CD variablesIConfiguration["Key"] scattered in servicesNo startup validation or secret rotation✅ Secure PatternUser Secrets for local dev (outside repo)Azure Key Vault + Managed Identity in prodOIDC federation for CI/CD (no long-lived keys)IOptions<T> with ValidateOnStart() bindingAuto-rotation + redacted structured logs
Figure 3: Side-by-side comparison of insecure versus secure patterns to manage secrets and config in .NET apps.

Secure Configuration Is a Deployment Discipline

When you manage secrets and config in .NET apps correctly, security becomes an architectural property rather than a developer chore. Start today by auditing your repository for hardcoded values, migrating local development to User Secrets, and wiring Azure Key Vault (or your cloud equivalent) as the authoritative production source. Enforce the pattern through CI checks that reject commits containing suspicious patterns and validate configuration completeness at startup. If your team needs hands-on guidance implementing this architecture or preparing for a compliance audit, reach out to discuss your specific environment.

Frequently Asked Questions

Run dotnet user-secrets init in your project directory. This adds a UserSecretsId to your csproj file and creates a local secrets.json store outside source control for development overrides.

On Linux and macOS they reside in ~/.microsoft/usersecrets//secrets.json. Windows stores them under %APPDATA%\Microsoft\UserSecrets\\secrets.json, keeping sensitive data completely separate from your repository and build artifacts.

Yes. The configuration provider hierarchy checks environment variables after secrets.json. Use double underscores as delimiters like ConnectionStrings__Default to override nested keys without modifying any JSON files during deployment.

Add the Azure.Extensions.AspNetCore.Configuration.Secrets NuGet package. Call builder.Configuration.AddAzureKeyVault with your vault URI and DefaultAzureCredential to inject remote secrets directly into the standard IConfiguration pipeline at startup.

appsettings.json is version-controlled and suits non-sensitive defaults. User Secrets exist only locally in an untracked store, preventing accidental credential commits while allowing developers to override production-like values safely during testing.

No. User Secrets are strictly a local development tool. For containerized environments, mount Kubernetes secrets, use Docker environment variables, or integrate cloud secret managers through the standard .NET configuration providers.

Execute dotnet user-secrets list in your terminal. This displays every key-value pair currently stored in the active project’s secret store, helping verify overrides before running the application locally.

Not directly. Each project has a unique UserSecretsId. You can manually copy the secrets.json content between IDs or use a shared configuration source like Azure Key Vault for team-wide secret management.

Run dotnet user-secrets remove to delete a single entry. Use dotnet user-secrets clear to wipe the entire store for the current project when resetting your local development environment.

Standard User Secrets do not reload automatically. Azure Key Vault and environment variable providers support reloadOnChange if configured, but most secret changes in production still require an application restart or graceful recycle.

No. They are stored as plain text JSON. User Secrets prevent accidental git commits, not unauthorized access. Encrypt your disk or use OS-level credential stores if local machine security is a concern.

Pipe JSON into the CLI using cat secrets.json | dotnet user-secrets set. This bulk-imports structured configuration without manual entry, useful when onboarding new developers or synchronizing local environments.

Yes. DefaultAzureCredential automatically uses the App Service or AKS managed identity in Azure environments. This eliminates connection strings and client secrets entirely for secure, passwordless secret retrieval in 2026 deployments.

Verify the UserSecretsId matches your csproj and that ASPNETCORE_ENVIRONMENT is set to Development. Secrets are ignored in Production by default unless you explicitly configure the provider to load them.

Generally no. Even development config files often contain test credentials or internal endpoints. Keep appsettings.Development.json out of source control and rely on User Secrets or environment variables for developer-specific overrides.