Skip to content

CWE-798: Hard-coded Credentials - C#

Overview

Hard-coded credentials in C# source code create severe security vulnerabilities. Never embed passwords, API keys, connection strings, or encryption keys in code or configuration files committed to version control. Use environment variables, User Secrets, Azure Key Vault, or configuration providers.

Primary Defence: Use environment variables, .NET User Secrets (development), Azure Key Vault, or IConfiguration with secure configuration providers to store and retrieve credentials.

Common Vulnerable Patterns

Hard-coded Database Credentials

// VULNERABLE - Credentials in source code
using System.Data.SqlClient;

public class DatabaseConnection
{
    private const string ConnectionString = 
        "Server=myserver;Database=mydb;User Id=admin;Password=P@ssw0rd123;";  // DANGEROUS!

    public SqlConnection GetConnection()
    {
        return new SqlConnection(ConnectionString);
    }
}

Why this is vulnerable: Hard-coded credentials in source code are exposed to anyone with repository access, remain in version control history, and cannot be rotated without code changes, enabling unauthorized database access if the code is leaked.

Hard-coded API Keys

// VULNERABLE - API key in code
public class ApiClient
{
    private const string ApiKey = "sk_live_51H7x8y9z10a11b12c";  // DANGEROUS!
    private const string ApiSecret = "whsec_abcdef123456";  // DANGEROUS!

    public async Task<HttpResponseMessage> MakeRequestAsync()
    {
        using var client = new HttpClient();
        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");

        return await client.GetAsync("https://api.example.com/data");
    }
}

Why this is vulnerable: API keys embedded in code are visible in version control, build artifacts, and decompiled assemblies, allowing anyone with code access to impersonate the application and incur charges or access sensitive data.

Hard-coded Encryption Keys

// VULNERABLE - Encryption key in code
using System.Security.Cryptography;
using System.Text;

public class Encryptor
{
    private const string SecretKey = "MySecretKey12345";  // DANGEROUS!

    public byte[] Encrypt(string data)
    {
        using var aes = Aes.Create();
        aes.Key = Encoding.UTF8.GetBytes(SecretKey.PadRight(32));
        aes.IV = new byte[16];

        using var encryptor = aes.CreateEncryptor();
        return encryptor.TransformFinalBlock(Encoding.UTF8.GetBytes(data), 0, data.Length);
    }
}

Why this is vulnerable: Hard-coded encryption keys in source code expose all encrypted data if the code is compromised, decompiled, or accessed by unauthorized parties. The key cannot be rotated without redeploying the application, and anyone with access to the source code can decrypt all data encrypted with this key, bypassing all encryption protections.

Credentials in app settings.json (Committed to Git)

// VULNERABLE - appsettings.json with real credentials committed
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=myserver;Database=mydb;User Id=admin;Password=P@ssw0rd123;"
  },
  "ApiSettings": {
    "ApiKey": "sk_live_51H7x8y9z10a11b12c",
    "ApiSecret": "whsec_abcdef123456"
  }
}

Why this is vulnerable: Configuration files committed to version control expose credentials permanently in git history, remain accessible even after deletion, and are often deployed to production, creating multiple attack vectors.

Secure Patterns

Environment Variables

// SECURE - Read from environment variables
using System;

public class DatabaseConnection
{
    private readonly string _connectionString;

    public DatabaseConnection()
    {
        var server = Environment.GetEnvironmentVariable("DB_SERVER");
        var database = Environment.GetEnvironmentVariable("DB_NAME");
        var userId = Environment.GetEnvironmentVariable("DB_USER");
        var password = Environment.GetEnvironmentVariable("DB_PASSWORD");

        if (string.IsNullOrEmpty(password))
        {
            throw new InvalidOperationException("Database credentials not configured");
        }

        _connectionString = $"Server={server};Database={database};User Id={userId};Password={password};";
    }

    public SqlConnection GetConnection()
    {
        return new SqlConnection(_connectionString);
    }
}

// PowerShell - Set environment variables:
// $env:DB_SERVER="myserver"
// $env:DB_NAME="mydb"
// $env:DB_USER="admin"
// $env:DB_PASSWORD="SecurePassword123"

Why this works: Environment variables keep credentials separate from code. They're set at the OS or deployment platform level (Azure App Service, Docker, Kubernetes), not stored in source control. The validation ensures the application fails fast if credentials are missing, preventing runtime errors with better security. Credentials can be rotated by updating environment variables without code changes or redeployment.

IConfiguration with External Config (.NET Core/5+)

// SECURE - ASP.NET Core configuration
using Microsoft.Extensions.Configuration;

public class ApiClient
{
    private readonly string _apiKey;
    private readonly string _apiSecret;

    public ApiClient(IConfiguration configuration)
    {
        _apiKey = configuration["ApiSettings:ApiKey"] 
            ?? throw new InvalidOperationException("API key not configured");
        _apiSecret = configuration["ApiSettings:ApiSecret"]
            ?? throw new InvalidOperationException("API secret not configured");
    }

    public async Task<HttpResponseMessage> MakeRequestAsync()
    {
        using var client = new HttpClient();
        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");

        return await client.GetAsync("https://api.example.com/data");
    }
}

// appsettings.json (committed - NO secrets):
{
  "ApiSettings": {
    "ApiKey": "",
    "ApiSecret": ""
  }
}

// appsettings.Development.json (NOT committed - add to .gitignore):
{
  "ApiSettings": {
    "ApiKey": "your_dev_key_here",
    "ApiSecret": "your_dev_secret_here"
  }
}

// For production, use environment variables or Azure Key Vault

Why this works: IConfiguration lets you compose settings from safe providers (environment variables, user secrets, Key Vault) while keeping the committed files empty of secrets. The empty values in appsettings.json document required keys without leaking credentials; per-environment files and environment variables override them at runtime, so nothing sensitive hits source control. The constructor throws if a required key is missing, creating a fail-fast startup instead of running with blank credentials. Because configuration binding is centralized, rotating a secret is just updating the external provider (Key Vault/KeyStore/env var) and restarting - no code change or redeploy needed. This pattern scales across environments and supports least-privilege by letting you scope secrets per environment.

User Secrets (Development Only)

// SECURE - User Secrets for development

// 1. Initialize User Secrets (from project directory):
// dotnet user-secrets init

// 2. Set secrets:
// dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;Database=mydb;User Id=admin;Password=DevPassword123;"
// dotnet user-secrets set "ApiSettings:ApiKey" "sk_test_your_dev_key"

// 3. Access in code (same as appsettings.json):
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        var connectionString = Configuration.GetConnectionString("DefaultConnection");
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(connectionString));
    }
}

// Secrets stored in:
// Windows: %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json
// Linux/macOS: ~/.microsoft/usersecrets/<user_secrets_id>/secrets.json

Why this works: User Secrets store development-only credentials outside the repo under the user profile, so nothing enters Git history. The dotnet user-secrets CLI writes encrypted-at-rest files on the developer machine and is ignored by default. Because the same Configuration pipeline reads User Secrets in Development and environment variables/Key Vault in Production, you keep a single code path while swapping providers per environment. The explicit CLI steps also make rotation easy - developers update the secret locally without code changes - and the missing-secret check still happens at startup, preventing silent failures or accidental fallbacks to hard-coded defaults.

Azure Key Vault

// SECURE - Azure Key Vault integration
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.Configuration;

// Program.cs (.NET 6+)
var builder = WebApplication.CreateBuilder(args);

// Add Azure Key Vault
var keyVaultEndpoint = new Uri(Environment.GetEnvironmentVariable("KeyVaultEndpoint")!);
builder.Configuration.AddAzureKeyVault(keyVaultEndpoint, new DefaultAzureCredential());

var app = builder.Build();

// Access secrets like normal configuration:
var apiKey = app.Configuration["ApiKey"];

// Or inject into services:
public class ApiClient
{
    private readonly string _apiKey;

    public ApiClient(IConfiguration configuration)
    {
        _apiKey = configuration["ApiKey"];
    }
}

// NuGet packages:
// <PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
// <PackageReference Include="Azure.Identity" />

Why this works: Azure Key Vault centralizes secret management with enterprise-grade security. DefaultAzureCredential uses Managed Identity in production (no credentials in code), Visual Studio/Azure CLI credentials locally. Secrets are encrypted at rest and in transit, with full audit logging. Key Vault integration makes secrets accessible through standard IConfiguration, enabling seamless code migration. Supports secret versioning and automatic rotation.

AWS Secrets Manager

// SECURE - AWS Secrets Manager
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using System.Text.Json;

public class SecretsService
{
    private readonly IAmazonSecretsManager _secretsManager;

    public SecretsService()
    {
        _secretsManager = new AmazonSecretsManagerClient();
    }

    public async Task<DatabaseCredentials> GetDatabaseCredentialsAsync()
    {
        var request = new GetSecretValueRequest
        {
            SecretId = "prod/database/credentials"
        };

        var response = await _secretsManager.GetSecretValueAsync(request);

        return JsonSerializer.Deserialize<DatabaseCredentials>(response.SecretString)
            ?? throw new InvalidOperationException("Failed to deserialize credentials");
    }
}

public class DatabaseCredentials
{
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
    public string Host { get; set; } = string.Empty;
    public int Port { get; set; }
}

// NuGet package:
// <PackageReference Include="AWSSDK.SecretsManager" />

// AWS credentials from environment or EC2 instance role

Why this works: AWS Secrets Manager keeps credentials out of code and source control, encrypts them with KMS, and lets you enforce access through IAM policies. The SDK automatically sources auth from the environment (profiles, EC2/ECS/Lambda roles), so the app never embeds AWS keys. Fetching secrets at runtime means you rotate centrally in Secrets Manager and get the new version without redeploying code. Versioning and CloudTrail auditing provide traceability and rollback during rotations. Deserializing into a typed object ensures you only consume the expected fields, reducing the chance of using malformed or tampered secret data.

Framework-Specific Guidance

ASP.NET Core

// SECURE - Full ASP.NET Core configuration setup

// Program.cs (.NET 6+)
var builder = WebApplication.CreateBuilder(args);

// Configuration sources (in order of precedence):
// 1. appsettings.json
// 2. appsettings.{Environment}.json
// 3. User Secrets (Development only)
// 4. Environment variables
// 5. Command-line arguments
// 6. Azure Key Vault (if configured)

// Add Azure Key Vault in production
if (builder.Environment.IsProduction())
{
    var keyVaultEndpoint = builder.Configuration["KeyVaultEndpoint"];
    if (!string.IsNullOrEmpty(keyVaultEndpoint))
    {
        builder.Configuration.AddAzureKeyVault(
            new Uri(keyVaultEndpoint),
            new DefaultAzureCredential());
    }
}

// Register services with configuration
builder.Services.Configure<ApiSettings>(
    builder.Configuration.GetSection("ApiSettings"));

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

// ApiSettings class
public class ApiSettings
{
    public string ApiKey { get; set; } = string.Empty;
    public string ApiSecret { get; set; } = string.Empty;
}

// Inject settings into controller
[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
    private readonly ApiSettings _apiSettings;

    public DataController(IOptions<ApiSettings> apiSettings)
    {
        _apiSettings = apiSettings.Value;
    }

    [HttpGet]
    public async Task<IActionResult> GetData()
    {
        // Use _apiSettings.ApiKey
        return Ok();
    }
}

Why this works: ASP.NET Core's configuration pipeline composes multiple sources in priority order, letting runtime secrets override committed defaults without touching code. User Secrets work in Development, environment variables in containers, and Key Vault in production - all feeding the same IConfiguration abstraction. Options pattern (IOptions<T>) injects strongly-typed settings into services, catching misconfigurations at startup via validation attributes. The environment check (IsProduction()) ensures Key Vault is only wired in prod, avoiding local dev complexity. This layered approach keeps secrets external, supports per-environment values, and enables zero-downtime rotation by updating the provider and restarting.

.NET Framework (Legacy)

// SECURE - .NET Framework with ConfigurationManager
using System.Configuration;

public class DatabaseConnection
{
    private readonly string _connectionString;

    public DatabaseConnection()
    {
        // Read from app.config or web.config
        _connectionString = ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString;

        if (string.IsNullOrEmpty(_connectionString))
        {
            throw new InvalidOperationException("Connection string not configured");
        }
    }
}

// web.config or app.config (NOT committed):
/*
<configuration>
  <connectionStrings>
    <add name="DefaultConnection" 
         connectionString="Server=myserver;Database=mydb;User Id=admin;Password=#{DB_PASSWORD}#;" />
  </connectionStrings>
  <appSettings>
    <add key="ApiKey" value="#{API_KEY}#" />
  </appSettings>
</configuration>
*/

// Use web.config transforms or Azure App Service configuration substitution

Why this works: ConfigurationManager reads from web.config or app.config at runtime, letting you tokenize connection strings with placeholders (#{DB_PASSWORD}#) that deployment pipelines replace per environment. The validation throws early if the connection string is missing or malformed. Web.config transforms (Web.Release.config) swap values during build, while Azure App Service configuration overrides settings without modifying files. Keeping the config file outside source control (or using transforms) prevents credentials from entering Git. This legacy approach predates modern secret stores but still separates code from config, enabling environment-specific deployments.

Entity Framework Core

// SECURE - EF Core with secure connection string
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<User> Users { get; set; }
}

// Startup.cs or Program.cs
public void ConfigureServices(IServiceCollection services)
{
    // Connection string from configuration (environment variable)
    var connectionString = Configuration.GetConnectionString("DefaultConnection");

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(connectionString));
}

// Set via environment variable:
// $env:ConnectionStrings__DefaultConnection="Server=myserver;Database=mydb;Integrated Security=true;"
// Note: Use Integrated Security or Managed Identity in production

Why this works: Entity Framework Core wires DbContext through dependency injection, pulling the connection string from IConfiguration at registration time. The double-underscore syntax (ConnectionStrings__DefaultConnection) maps environment variables to the nested config structure, letting containers and platforms override without code changes. Because the connection string is resolved once at startup (when AddDbContext runs), misconfiguration fails immediately rather than at first query. Integrated Security or Managed Identity removes passwords entirely, using OS-level or Azure AD authentication tokens. This keeps EF agnostic to secret storage - swap providers by changing config sources, not data access code.

Managed Identity (Azure)

// SECURE - Azure SQL with Managed Identity (no passwords!)
using Azure.Identity;
using Microsoft.Data.SqlClient;

public class DatabaseConnection
{
    public async Task<SqlConnection> GetConnectionAsync()
    {
        var connectionString = "Server=myserver.database.windows.net;Database=mydb;";
        var connection = new SqlConnection(connectionString);

        // Get token using Managed Identity
        var credential = new DefaultAzureCredential();
        var token = await credential.GetTokenAsync(
            new Azure.Core.TokenRequestContext(new[] { "https://database.windows.net/.default" }));

        connection.AccessToken = token.Token;

        await connection.OpenAsync();
        return connection;
    }
}

// NuGet package:
// <PackageReference Include="Azure.Identity" />
// <PackageReference Include="Microsoft.Data.SqlClient" />

Why this works: Managed Identity eliminates passwords entirely by using Azure AD authentication. The application's identity is granted database access through Azure RBAC, not username/password. Tokens are short-lived and automatically rotated by Azure. No credentials to store, rotate, or leak. Works seamlessly with Azure SQL, Storage, Key Vault, and other Azure services. Significantly reduces attack surface.

Testing with Test Credentials

// SECURE - Use test credentials for unit tests
using Xunit;
using Testcontainers.MsSql;

public class DatabaseIntegrationTests : IAsyncLifetime
{
    private MsSqlContainer _msSqlContainer = default!;

    public async Task InitializeAsync()
    {
        // Test container provides isolated database with test credentials
        _msSqlContainer = new MsSqlBuilder()
            .WithPassword("Test123!")
            .Build();

        await _msSqlContainer.StartAsync();
    }

    [Fact]
    public async Task TestDatabaseConnection()
    {
        var connectionString = _msSqlContainer.GetConnectionString();

        using var connection = new SqlConnection(connectionString);
        await connection.OpenAsync();

        // Test database operations
        Assert.Equal(System.Data.ConnectionState.Open, connection.State);
    }

    public async Task DisposeAsync()
    {
        await _msSqlContainer.DisposeAsync();
    }
}

// NuGet package:
// <PackageReference Include="Testcontainers.MsSql" />

.gitignore Best Practices

# Add these patterns to .gitignore

# Configuration files with secrets

appsettings.Development.json
appsettings.Local.json
appsettings.*.json
!appsettings.json

# User Secrets

**/secrets.json

# Environment files

.env
.env.local

# Azure publish settings

*.PublishSettings
*.pubxml
*.azurePubxml

# Private keys

*.pfx
*.p12

Detecting Hard-coded Secrets

Using git-secrets

# Install git-secrets

# https://github.com/awslabs/git-secrets

# Add patterns to detect

git secrets --add 'password\s*=\s*["\'][^"\']+["\']'
git secrets --add 'apikey\s*=\s*["\'][^"\']+["\']'
git secrets --add '(?i)(aws_access_key_id|aws_secret_access_key)'

# Scan repository

git secrets --scan

# Add pre-commit hook

git secrets --install

Using TruffleHog

# Install TruffleHog

pip install truffleHog

# Scan repository

trufflehog --regex --entropy=True https://github.com/yourusername/yourrepo

Verification

To verify credentials are not hardcoded:

  • Search source code: Grep for patterns like password=, apiKey=, secret=, connection strings, and API keys in .cs files
  • Review configuration: Check appsettings.json, web.config, and other config files for hardcoded credentials
  • Check environment usage: Verify the application reads credentials from environment variables or secret managers at runtime
  • Test without secrets: Run the application without setting environment variables - it should fail gracefully with clear error messages, not fall back to hardcoded values
  • Review version control: Check git history for accidentally committed secrets (use tools like git-secrets or truffleHog)
  • Verify .gitignore: Ensure configuration files with secrets (e.g., appsettings.Development.json) are excluded from version control
  • Use static analysis: Run tools like SonarQube or security scanners to detect hardcoded credentials
  • Check build artifacts: Verify deployed packages don't contain hardcoded secrets
// SECURE - Unit tests demonstrating hardcoded credential detection and prevention
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

[TestClass]
public class HardcodedCredentialsTests
{
    [TestMethod]
    public void TestCredentialsNotHardcoded()
    {
        // Verify that credentials are loaded from environment, not hardcoded
        var dbConnection = new DatabaseConnection();

        // Should throw if environment variables not set
        Environment.SetEnvironmentVariable("DB_PASSWORD", null);
        Assert.ThrowsException<InvalidOperationException>(() => new DatabaseConnection());

        // Should work when environment variables are set
        Environment.SetEnvironmentVariable("DB_PASSWORD", "TestPassword123");
        Environment.SetEnvironmentVariable("DB_SERVER", "localhost");
        Environment.SetEnvironmentVariable("DB_NAME", "testdb");
        Environment.SetEnvironmentVariable("DB_USER", "testuser");

        var connection = new DatabaseConnection();
        Assert.IsNotNull(connection);
    }

    [TestMethod]
    public void TestEnvironmentVariablesAreUsed()
    {
        // Test that environment variables are correctly loaded
        var expectedApiKey = "test_api_key_12345";
        Environment.SetEnvironmentVariable("API_KEY", expectedApiKey);

        var apiKey = Environment.GetEnvironmentVariable("API_KEY");
        Assert.AreEqual(expectedApiKey, apiKey);

        // Cleanup
        Environment.SetEnvironmentVariable("API_KEY", null);
    }

    [TestMethod]
    public void TestConfigurationFromIConfiguration()
    {
        // Test that credentials are loaded from IConfiguration
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string>
            {
                { "ApiSettings:ApiKey", "test_key" },
                { "ApiSettings:ApiSecret", "test_secret" }
            })
            .Build();

        var apiClient = new ApiClient(configuration);
        Assert.IsNotNull(apiClient);
    }

    [TestMethod]
    public void TestMissingCredentialsThrowException()
    {
        // Verify application fails fast when credentials are missing
        Environment.SetEnvironmentVariable("DB_PASSWORD", null);
        Environment.SetEnvironmentVariable("DB_SERVER", null);
        Environment.SetEnvironmentVariable("DB_NAME", null);
        Environment.SetEnvironmentVariable("DB_USER", null);

        Assert.ThrowsException<InvalidOperationException>(() => 
            new DatabaseConnection(),
            "Should throw when credentials not configured"
        );
    }

    [TestMethod]
    public void TestSourceCodeForHardcodedSecrets()
    {
        // Static analysis test - scan source files for hardcoded credentials
        var projectRoot = Path.GetFullPath(Path.Combine(
            AppDomain.CurrentDomain.BaseDirectory, "..", "..", ".."));

        var csFiles = Directory.GetFiles(projectRoot, "*.cs", SearchOption.AllDirectories)
            .Where(f => !f.Contains("\\bin\\") && !f.Contains("\\obj\\"))
            .ToList();

        var suspiciousPatterns = new[]
        {
            new Regex(@"password\s*=\s*[""'][^""']{6,}[""']", RegexOptions.IgnoreCase),
            new Regex(@"api[_-]?key\s*=\s*[""'][^""']{10,}[""']", RegexOptions.IgnoreCase),
            new Regex(@"secret\s*=\s*[""'][^""']{10,}[""']", RegexOptions.IgnoreCase),
            new Regex(@"token\s*=\s*[""'][^""']{10,}[""']", RegexOptions.IgnoreCase),
            new Regex(@"connectionstring\s*=\s*[""'][^""']*password[^""']*[""']", RegexOptions.IgnoreCase)
        };

        var findings = new List<string>();

        foreach (var file in csFiles)
        {
            var content = File.ReadAllText(file);
            var lines = content.Split('\n');

            for (int i = 0; i < lines.Length; i++)
            {
                var line = lines[i];

                // Skip comments and test files
                if (line.TrimStart().StartsWith("//") || 
                    file.Contains("Test") && line.Contains("test_"))
                    continue;

                foreach (var pattern in suspiciousPatterns)
                {
                    if (pattern.IsMatch(line))
                    {
                        findings.Add($"{file}:{i + 1}: {line.Trim()}");
                    }
                }
            }
        }

        if (findings.Any())
        {
            var message = "Potential hardcoded credentials found:\n" + 
                         string.Join("\n", findings);
            Assert.Fail(message);
        }
    }

    [TestMethod]
    public void TestCredentialRotation()
    {
        // Test that application can handle credential rotation
        var initialPassword = "InitialPassword123";
        var rotatedPassword = "RotatedPassword456";

        Environment.SetEnvironmentVariable("DB_PASSWORD", initialPassword);
        Environment.SetEnvironmentVariable("DB_SERVER", "localhost");
        Environment.SetEnvironmentVariable("DB_NAME", "testdb");
        Environment.SetEnvironmentVariable("DB_USER", "testuser");

        var connection1 = new DatabaseConnection();
        Assert.IsNotNull(connection1);

        // Simulate credential rotation
        Environment.SetEnvironmentVariable("DB_PASSWORD", rotatedPassword);

        var connection2 = new DatabaseConnection();
        Assert.IsNotNull(connection2);

        // Verify new instance picks up rotated credentials
        Assert.AreNotEqual(initialPassword, rotatedPassword);
    }

    [TestMethod]
    public void TestUserSecretsInDevelopment()
    {
        // Test that User Secrets are accessible in development
        // This test would run in development environment only
        var configuration = new ConfigurationBuilder()
            .AddUserSecrets<HardcodedCredentialsTests>()
            .Build();

        // User Secrets should be configured via:
        // dotnet user-secrets set "TestSecret" "TestValue"
        var testSecret = configuration["TestSecret"];

        // In CI/CD, this would be null (expected)
        // In local dev with secrets configured, it would have a value
        Assert.IsTrue(testSecret == null || testSecret.Length > 0);
    }

    [TestMethod]
    public void TestAzureKeyVaultIntegration()
    {
        // Test Azure Key Vault integration (requires Azure resources in real environment)
        // This is a mock test showing the pattern

        var keyVaultUrl = Environment.GetEnvironmentVariable("KeyVaultEndpoint");

        if (string.IsNullOrEmpty(keyVaultUrl))
        {
            Assert.Inconclusive("KeyVaultEndpoint not configured, skipping integration test");
            return;
        }

        // In real test, would connect to Key Vault and retrieve secret
        // var secretsManager = new AzureKeyVaultSecretsManager(keyVaultUrl);
        // var secret = await secretsManager.GetSecretAsync("DatabasePassword");
        // Assert.IsNotNull(secret);
    }

    [TestMethod]
    public void TestConfigurationFilesNotCommitted()
    {
        // Verify that sensitive config files are in .gitignore
        var gitignorePath = Path.GetFullPath(Path.Combine(
            AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", ".gitignore"));

        if (!File.Exists(gitignorePath))
        {
            Assert.Inconclusive(".gitignore not found");
            return;
        }

        var gitignoreContent = File.ReadAllText(gitignorePath);

        var requiredPatterns = new[]
        {
            "appsettings.Development.json",
            "appsettings.Local.json",
            "secrets.json",
            ".env"
        };

        foreach (var pattern in requiredPatterns)
        {
            Assert.IsTrue(
                gitignoreContent.Contains(pattern) || gitignoreContent.Contains(pattern.Replace(".json", "*.json")),
                $"Sensitive file pattern '{pattern}' should be in .gitignore"
            );
        }
    }

    [TestMethod]
    public void TestNoCredentialsInLogs()
    {
        // Test that credentials are not logged
        var testPassword = "TestPassword123";
        var testApiKey = "sk_test_12345";

        // Simulate logging
        var logOutput = new System.Text.StringBuilder();
        var logger = new StringWriter(logOutput);

        // This should NOT log the actual password
        logger.WriteLine($"Connecting to database...");

        var logContent = logOutput.ToString();

        Assert.IsFalse(logContent.Contains(testPassword), 
            "Password should not appear in logs");
        Assert.IsFalse(logContent.Contains(testApiKey), 
            "API key should not appear in logs");
    }
}

Cloud-Native Secrets Management (Production-Grade)

For production .NET applications, use dedicated secrets management services:

Azure Key Vault

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using System;
using System.Threading.Tasks;

/// <summary>
/// Manages secrets from Azure Key Vault.
/// 
/// Prerequisites:
/// - Install NuGet packages: Azure.Identity, Azure.Security.KeyVault.Secrets
/// - Azure resource (App Service, VM, AKS) must have Managed Identity enabled
/// - Managed Identity must have "Get" permission on Key Vault secrets
/// </summary>
public class AzureKeyVaultSecretsManager
{
    private readonly SecretClient _client;

    public AzureKeyVaultSecretsManager(string keyVaultUrl)
    {
        // DefaultAzureCredential uses Managed Identity in production
        // Uses Visual Studio/Azure CLI/Environment variables for local development
        // NO credentials in code!
        var credential = new DefaultAzureCredential();
        _client = new SecretClient(new Uri(keyVaultUrl), credential);
    }

    public async Task<string> GetSecretAsync(string secretName)
    {
        try
        {
            KeyVaultSecret secret = await _client.GetSecretAsync(secretName);
            return secret.Value;
        }
        catch (Azure.RequestFailedException ex) when (ex.Status == 404)
        {
            throw new Exception($"Secret '{secretName}' not found in Key Vault");
        }
        catch (Azure.RequestFailedException ex) when (ex.Status == 403)
        {
            throw new Exception($"Access denied to secret '{secretName}'. Check Managed Identity permissions.");
        }
    }

    public async Task<Dictionary<string, string>> GetMultipleSecretsAsync(params string[] secretNames)
    {
        var secrets = new Dictionary<string, string>();
        foreach (var name in secretNames)
        {
            secrets[name] = await GetSecretAsync(name);
        }
        return secrets;
    }
}

// ASP.NET Core Startup configuration
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register as singleton for caching
        services.AddSingleton(sp =>
        {
            var keyVaultUrl = "https://myapp-keyvault.vault.azure.net/";
            return new AzureKeyVaultSecretsManager(keyVaultUrl);
        });

        services.AddControllers();
    }
}

// Usage in controller
public class DatabaseService
{
    private readonly AzureKeyVaultSecretsManager _secretsManager;

    public DatabaseService(AzureKeyVaultSecretsManager secretsManager)
    {
        _secretsManager = secretsManager;
    }

    public async Task<SqlConnection> GetDatabaseConnectionAsync()
    {
        var connectionString = await _secretsManager.GetSecretAsync("DatabaseConnectionString");
        return new SqlConnection(connectionString);
    }
}

// Usage with multiple secrets
public class ExternalApiService
{
    public async Task InitializeAsync(AzureKeyVaultSecretsManager secretsManager)
    {
        var secrets = await secretsManager.GetMultipleSecretsAsync(
            "StripeApiKey",
            "SendGridApiKey",
            "TwilioApiKey"
        );

        _stripeClient = new StripeClient(secrets["StripeApiKey"]);
        _sendGridClient = new SendGridClient(secrets["SendGridApiKey"]);
        _twilioClient = new TwilioRestClient(secrets["TwilioApiKey"]);
    }
}

ASP.NET Core Configuration Integration:

// Program.cs (.NET 6+) or Startup.cs
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureKeyVault;

var builder = WebApplication.CreateBuilder(args);

// Automatically load secrets from Key Vault into IConfiguration
var keyVaultUrl = builder.Configuration["KeyVaultUrl"];
if (!string.IsNullOrEmpty(keyVaultUrl))
{
    builder.Configuration.AddAzureKeyVault(
        new Uri(keyVaultUrl),
        new DefaultAzureCredential()
    );
}

// Now secrets are available via IConfiguration
builder.Services.AddDbContext<AppDbContext>(options =>
{
    // "DatabaseConnectionString" automatically retrieved from Key Vault
    options.UseSqlServer(builder.Configuration["DatabaseConnectionString"]);
});

var app = builder.Build();

Caching for Performance:

using Microsoft.Extensions.Caching.Memory;

public class CachedSecretsManager
{
    private readonly AzureKeyVaultSecretsManager _keyVault;
    private readonly IMemoryCache _cache;
    private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(30);

    public CachedSecretsManager(AzureKeyVaultSecretsManager keyVault, IMemoryCache cache)
    {
        _keyVault = keyVault;
        _cache = cache;
    }

    public async Task<string> GetSecretAsync(string secretName)
    {
        return await _cache.GetOrCreateAsync(
            $"secret:{secretName}",
            async entry =>
            {
                entry.AbsoluteExpirationRelativeToNow = _cacheDuration;
                return await _keyVault.GetSecretAsync(secretName);
            }
        );
    }
}

Best Practices for Azure Key Vault

Use Managed Identity (No Credentials in Code)

  • Enable System-Assigned or User-Assigned Managed Identity on App Service/VM/AKS
  • Grant Key Vault access policy: az keyvault set-policy --name MyKeyVault --object-id <identity-id> --secret-permissions get list
  • DefaultAzureCredential handles authentication automatically

Separate Secrets by Environment

   MyApp-Dev-KeyVault
   MyApp-Staging-KeyVault  
   MyApp-Prod-KeyVault
  • Never share Key Vaults across environments
  • Use different Managed Identities per environment

Enable Soft Delete and Purge Protection

az keyvault create --name MyKeyVault \

     --resource-group MyResourceGroup \
     --enable-soft-delete true \
     --enable-purge-protection true
  • Prevents accidental deletion
  • Retains deleted secrets for 90 days

Monitor Secret Access

  • Enable Azure Monitor diagnostics for Key Vault
  • Alert on unusual SecretGet operations
  • Track failed authentication attempts

Implement Secret Rotation

   // Use Key Vault secret versions for rotation
var secretWithVersion = await _client.GetSecretAsync("DatabasePassword", "version-guid");

   // Or always use latest version (default)
   var latestSecret = await _client.GetSecretAsync("DatabasePassword");
  • Rotate secrets every 30-90 days
  • Use versioning to support gradual rollout

Local Development Configuration

   // appsettings.Development.json
{
     "KeyVaultUrl": "",  // Empty - use User Secrets instead
  "DatabaseConnectionString": "Server=localhost;..."  // Local connection
   }

   // appsettings.Production.json
   {
     "KeyVaultUrl": "https://myapp-prod.vault.azure.net/"
     // DatabaseConnectionString loaded from Key Vault
   }
  • Use User Secrets (dotnet user-secrets) for local development
  • Never commit appsettings.Production.json with actual secrets

Additional Resources