Learnixo
Back to blog
Backend Systemsintermediate

Secret Management in .NET: No More Hardcoded Credentials

Securely manage secrets in .NET apps. Covers user secrets, environment variables, Azure Key Vault, managed identity, secret rotation, .NET Aspire integration, and what never to commit to source control.

LearnixoJune 4, 20266 min read
.NETC#SecurityAzure Key VaultSecretsManaged IdentityDevOps
Share:š•

The Rule: Never Commit Secrets

A secret in source control is a public secret — even if the repo is private. Git history is forever. An accidental commit requires rotating every affected credential.

Secrets that must never be in source control:

  • Database connection strings with passwords
  • API keys (OpenAI, Stripe, SendGrid, etc.)
  • JWT signing keys
  • OAuth client secrets
  • Storage account keys
  • SMTP passwords

Development: User Secrets

.NET User Secrets stores sensitive values in your OS user profile — outside the project directory, never committed.

Bash
# Enable user secrets for a project
dotnet user-secrets init

# Set a secret
dotnet user-secrets set "ConnectionStrings:Default" "Server=localhost;Database=Dev;..."
dotnet user-secrets set "Auth:ClientSecret" "my-dev-secret"
dotnet user-secrets set "OpenAI:ApiKey" "sk-..."

# List all secrets
dotnet user-secrets list

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

Secrets are stored at:

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

Access them via IConfiguration like any other setting:

C#
// In Program.cs — automatically loaded in Development environment
builder.Configuration.AddUserSecrets<Program>(); // included by default in ASP.NET Core

// In code
var apiKey = builder.Configuration["OpenAI:ApiKey"];
var connStr = builder.Configuration.GetConnectionString("Default");

Environment Variables

For staging, CI/CD, and simple container deployments:

C#
// Environment variables override appsettings.json
// CONNECTIONSTRINGS__DEFAULT → ConnectionStrings:Default
// OPENAI__APIKEY → OpenAI:ApiKey (double underscore = colon separator)

// In GitHub Actions
- name: Deploy
  env:
    ConnectionStrings__Default: ${{ secrets.DB_CONNECTION_STRING }}
    OpenAI__ApiKey: ${{ secrets.OPENAI_API_KEY }}
    Auth__ClientSecret: ${{ secrets.AUTH_CLIENT_SECRET }}
C#
// appsettings.json — safe to commit (no secrets)
{
  "ConnectionStrings": {
    "Default": ""  // overridden by environment variable or Key Vault
  },
  "OpenAI": {
    "ApiKey": "",
    "Model": "gpt-4o"  // non-secret config stays here
  }
}

Azure Key Vault

For production: all secrets live in Key Vault, fetched at startup.

Bash
# Create Key Vault
az keyvault create \
  --name orderflow-kv \
  --resource-group orderflow-rg \
  --location uksouth

# Add secrets
az keyvault secret set --vault-name orderflow-kv \
  --name "ConnectionStrings--Default" \
  --value "Server=prod.db;..."

az keyvault secret set --vault-name orderflow-kv \
  --name "Auth--ClientSecret" \
  --value "prod-secret-here"
C#
// Program.cs — fetch from Key Vault at startup
var keyVaultUri = new Uri(builder.Configuration["KeyVault:Uri"]!);

builder.Configuration.AddAzureKeyVault(
    keyVaultUri,
    new DefaultAzureCredential());   // uses managed identity in Azure, dev identity locally

DefaultAzureCredential checks these credential sources in order:

  1. Environment variables (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET)
  2. Workload identity (Kubernetes)
  3. Managed identity (Azure VMs, App Service, Container Apps)
  4. Visual Studio / VS Code / Azure CLI credentials (developer machine)

Managed Identity (No Credentials at All)

With managed identity, your app authenticates to Azure services without any credential in config.

Bash
# Enable system-assigned managed identity on Container Apps
az containerapp identity assign \
  --name orders-api \
  --resource-group orderflow-rg \
  --system-assigned

# Get the principal ID
az containerapp show --name orders-api \
  --query identity.principalId -o tsv

# Grant Key Vault access
az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee <principal-id> \
  --scope /subscriptions/.../providers/Microsoft.KeyVault/vaults/orderflow-kv

# Grant SQL Server access (Azure SQL with AAD auth)
az sql server ad-admin set \
  --resource-group orderflow-rg \
  --server-name orderflow-sql \
  --display-name "orders-api" \
  --object-id <principal-id>
C#
// SQL Server connection with managed identity — no password
var conn = new SqlConnection
{
    ConnectionString = "Server=orderflow.database.windows.net;Database=orders;Authentication=Active Directory Managed Identity"
};

// Or with EF Core
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default"),
        sql => sql.UseAzureADAuthentication(new DefaultAzureCredential())));

Secret Rotation

Secrets expire. Rotation must be automated or it won't happen.

Strategy: Dual Secret Approach

Phase 1: Both Old and New secret are active
Phase 2: App is updated to use New secret
Phase 3: Old secret is revoked
C#
// Reload secrets without restarting the app
// Azure Key Vault provider supports periodic reload
builder.Configuration.AddAzureKeyVault(keyVaultUri, new DefaultAzureCredential(),
    new AzureKeyVaultConfigurationOptions
    {
        ReloadInterval = TimeSpan.FromHours(1) // check for updates hourly
    });

.NET Aspire Secret Integration

C#
// AppHost — reference Key Vault secrets
var kv = builder.AddAzureKeyVault("orderflow-kv");

var ordersApi = builder.AddProject<Projects.OrderFlow_Orders>("orders-api")
    .WithReference(kv);  // injects Key Vault URI + grants access via managed identity
C#
// In Orders service — reads from Key Vault automatically
builder.AddAzureKeyVaultSecrets("orderflow-kv");

// Access normally via IConfiguration
var secret = builder.Configuration["Auth:ClientSecret"];

What a .gitignore Must Include

# Never commit these
appsettings.*.json          # if they contain secrets
.env
.env.local
*.pfx
*.p12
*.key
secrets.json
**/UserSecrets/

The only files safe to commit are:

  • appsettings.json — non-secret defaults
  • appsettings.Development.json — pointers to where secrets live (not the secrets themselves)

Security Audit Checklist

☐ No secrets in appsettings.json committed to repo
☐ .gitignore covers .env, *.pfx, secrets.json
☐ User Secrets used for local development
☐ CI/CD uses GitHub Secrets or Azure Key Vault — not hardcoded
☐ Production uses managed identity — no client secrets in env vars
☐ Key Vault access is via RBAC, not access policies
☐ Secret rotation schedule defined and automated
☐ Git history scanned for accidental secret commits (truffleHog, git-secrets)
☐ Alerts configured for Key Vault access anomalies

If You Accidentally Committed a Secret

Bash
# 1. IMMEDIATELY revoke/rotate the secret — assume it's already been found
# 2. Remove from git history
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch path/to/file-with-secret" \
  --prune-empty --tag-name-filter cat -- --all

# 3. Force push all branches
git push origin --force --all

# 4. Notify your security team

Interview Questions

Q: What is managed identity and why is it better than storing credentials in environment variables? Managed identity gives an Azure resource a cryptographic identity issued by Azure AD. The resource authenticates to other Azure services using that identity — no credential is stored, transmitted, or rotated. An attacker who gains access to your running app can't extract a credential to reuse elsewhere. Environment variables with credentials can be read by any process in the container.

Q: What is the difference between User Secrets and environment variables? User Secrets are stored outside the project (in the OS user profile), never committed to source control, and only work in Development. Environment variables work in any environment and are the standard injection mechanism for containers. Use User Secrets during local development; use environment variables or Key Vault in staging and production.

Q: How do you handle secret rotation without app downtime? Use a dual-secret approach: add the new secret to Key Vault alongside the old one, update the app to try the new secret first, then revoke the old one after verifying all traffic uses the new one. With Azure Key Vault's reload interval configured, the app picks up the new secret without restart.

Q: How would you scan a repository for accidentally committed secrets? Tools: truffleHog (scans git history for high-entropy strings and known patterns), git-secrets (pre-commit hook that prevents committing known patterns), gitleaks, or GitHub's own secret scanning. Run retrospectively: truffleHog git file://. --since-commit HEAD~100. Set up pre-commit hooks to prevent future incidents.

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.