Skip to content

CWE-798: Use of Hard-coded Credentials - C

Overview

Hard-coded credentials in C# source code are readable by anyone with repository access, and they survive in version control history and in compiled assemblies. 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 .NET User Secrets for local development only, Azure Key Vault or another managed secret store for production, and IConfiguration with secure providers to retrieve credentials at runtime. Environment variables are useful for deployment-time injection but should not be treated as a complete secret-management system - see CWE-526.

Rotate first, then refactor. Any credential that has been committed is compromised, whether or not the repository is public: it is in git log, in every clone, and in every build artefact produced since. Deleting the literal from HEAD changes none of that. Revoke the value at the system that issued it before or alongside the code change, and treat the refactor as what stops it happening again rather than as the fix.

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: Anyone who can read the source or decompile the assembly recovers the key, and with it can decrypt every value the application has ever encrypted. The key cannot be rotated without redeploying the application.

Credentials in appsettings.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 as well.

Default Administrator Account

// VULNERABLE - Built-in administrator the product ships with
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class LoginController : ControllerBase
{
    private const string AdminUser = "admin";
    private const string AdminPassword = "admin123";  // DANGEROUS!

    // A licence gate that is really a second hard-coded credential.
    private const string LicenceKeyHash =
        "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";  // DANGEROUS!

    private readonly IUserService _users;

    public LoginController(IUserService users) => _users = users;

    [HttpPost]
    public IActionResult Login([FromForm] string username, [FromForm] string password)
    {
        // The built-in account is checked before the real user store
        if (username == AdminUser && password == AdminPassword)
        {
            return Ok(IssueAdminSession());
        }

        return _users.Validate(username, password) ? Ok() : Unauthorized();
    }

    [HttpPost("activate")]
    public IActionResult Activate([FromForm] string licenceKey)
    {
        var digest = Convert.ToHexString(
            SHA256.HashData(Encoding.UTF8.GetBytes(licenceKey))).ToLowerInvariant();

        return digest == LicenceKeyHash ? Ok() : Unauthorized();  // DANGEROUS!
    }
}

Why this is vulnerable: this is the inbound half of CWE-798 - a credential the product accepts rather than one it sends - and a secret store does not fix it. admin123 is the same on every installation, so one copy of the product yields the administrator password for every customer running it; attackers try admin/admin, admin/admin123 and root/toor in the first minutes of a scan, and an account intended only for initial setup is routinely still live in production. The licence check is the same weakness wearing a hash: a fixed digest is a fixed credential, published in every binary, and == on it also compares byte by byte with an early exit, so the comparison leaks how much of the value was right (see CWE-208). Moving either literal into Azure Key Vault changes nothing - the credential still authenticates on every deployment.

Secure Patterns

Credentials the Product Accepts: Authenticate, Do Not Compare

Take this one first. It is the fix for Default Administrator Account above, and none of the secret-management patterns below address it: the built-in administrator is not a secret the application needs to hold, it is an authenticator the application should never have accepted. Moving admin123 into Key Vault leaves the same credential working on every installation.

// SECURE - no credential is identical across installations
using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

public sealed class AdminUser
{
    public string Username { get; init; } = string.Empty;
    public string PasswordHash { get; init; } = string.Empty;
}

// Per-installation store. Nothing in here ships with the product.
public interface IAdminStore
{
    Task<int> CountAsync();
    Task<AdminUser?> FindAsync(string username);
    Task UpdateHashAsync(string username, string passwordHash);
}

[ApiController]
[Route("api/[controller]")]
public class LoginController : ControllerBase
{
    // A hash of a value nobody knows, verified against when the account does
    // not exist, so the unknown-user path costs about what a real check costs.
    private static readonly string DummyHash =
        new PasswordHasher<AdminUser>().HashPassword(new AdminUser(), Guid.NewGuid().ToString());

    private readonly IAdminStore _store;
    private readonly IPasswordHasher<AdminUser> _hasher;

    public LoginController(IAdminStore store, IPasswordHasher<AdminUser> hasher)
    {
        _store = store;
        _hasher = hasher;
    }

    [HttpPost]
    public async Task<IActionResult> LoginAsync([FromForm] string username, [FromForm] string password)
    {
        // First-run enrolment gate: refuse to serve until an administrator has
        // been enrolled. A fresh install has no working credential at all,
        // rather than a well-known one.
        if (await _store.CountAsync() == 0)
        {
            return StatusCode(StatusCodes.Status503ServiceUnavailable, "Setup incomplete");
        }

        var user = await _store.FindAsync(username);
        if (user is null)
        {
            _hasher.VerifyHashedPassword(new AdminUser(), DummyHash, password);
            return Unauthorized();
        }

        var result = _hasher.VerifyHashedPassword(user, user.PasswordHash, password);
        if (result == PasswordVerificationResult.Failed)
        {
            return Unauthorized();
        }

        if (result == PasswordVerificationResult.SuccessRehashNeeded)
        {
            // Stored hash used weaker parameters than the current default.
            await _store.UpdateHashAsync(user.Username, _hasher.HashPassword(user, password));
        }

        // Issue the session here.
        return Ok();
    }
}

public static class DeploymentToken
{
    // For a fixed token the product genuinely must accept - one generated at
    // install time and stored per deployment, never compiled in.
    public static bool Matches(string presented, string expected)
    {
        Span<byte> a = stackalloc byte[32];
        Span<byte> b = stackalloc byte[32];
        SHA256.HashData(Encoding.UTF8.GetBytes(presented), a);
        SHA256.HashData(Encoding.UTF8.GetBytes(expected), b);

        return CryptographicOperations.FixedTimeEquals(a, b);
    }
}

Why this works: there is no credential literal left for the code to compare against, so there is nothing an attacker can read out of the assembly and use against every other deployment. PasswordHasher<TUser> - the hasher ASP.NET Core Identity registers by default - verifies a per-user hash from the installation's own store instead of testing equality against a constant, and it salts: hashing the same password twice returns two different strings, so one installation's stolen database does not yield credentials for another. The encoded hash carries its own format version, which is why VerifyHashedPassword has three outcomes rather than two; SuccessRehashNeeded is what it returns for a password that is correct but stored under older parameters, and re-hashing on that branch upgrades accounts as people sign in. Verifying the unknown user against DummyHash keeps the two failure paths similar in cost, which is CWE-208 territory.

CountAsync is what stops the default coming back. A build that refuses to serve until enrolment finishes cannot ship with a working admin/admin123, whereas a default that only logs a warning stays in place - put the same check in middleware so every route is gated, not just login. For a token the product really must accept, CryptographicOperations.FixedTimeEquals is the comparison that does not exit early on the first differing byte. It returns false immediately when the two spans differ in length, so hashing both sides to 32 bytes first keeps the length of the presented token out of the result as well. Store passwords this way in general - see CWE-916.

Environment Variables

// SECURE - Read from environment variables
using System;
using Microsoft.Data.SqlClient;

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 source code and can be supplied by the OS or deployment platform. The validation ensures the application fails fast if credentials are missing. For production, inject these values from a secret store or platform secret mechanism and protect them from process dumps, debug endpoints, platform metadata, and logging. Rotation usually requires updating the provider and refreshing configuration or restarting the application.

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 means updating the external provider (Key Vault, user secrets, or an environment variable) and restarting - no code change or redeploy. Scoping the provider per environment also keeps each environment's credentials to least privilege.

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 Secret Manager tool does not encrypt the stored values and should not be used as a trusted production store. 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 local updates easy, and the missing-secret check still happens at startup.

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 managed access control and audit logging. DefaultAzureCredential uses Managed Identity in production and developer credentials locally, so application credentials are not embedded in code. Secrets are encrypted at rest and in transit. Key Vault integration surfaces secrets through the standard IConfiguration API, so code that already reads configuration does not change. Secret versioning supports staged rotation when the application and operations process handle refresh correctly.

AWS Secrets Manager

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

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);

        // SecretString is a JSON document, and its keys are lower-case.
        // System.Text.Json matches property names case-sensitively by default,
        // so without JsonPropertyName every field would silently be "".
        var creds = JsonSerializer.Deserialize<DatabaseCredentials>(response.SecretString)
            ?? throw new InvalidOperationException("Secret is not a JSON object");

        if (string.IsNullOrEmpty(creds.Username) || string.IsNullOrEmpty(creds.Password))
        {
            throw new InvalidOperationException(
                "Secret 'prod/database/credentials' has no username/password - check the key names");
        }

        return creds;
    }
}

public class DatabaseCredentials
{
    [JsonPropertyName("username")] public string Username { get; set; } = string.Empty;
    [JsonPropertyName("password")] public string Password { get; set; } = string.Empty;
    [JsonPropertyName("host")] public string Host { get; set; } = string.Empty;
    [JsonPropertyName("port")] 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.

Two details decide whether this works at all. SecretString is a JSON document, not the secret itself, and the keys an RDS-managed secret uses are username, password, host, port, engine, dbname - all lower-case. JsonSerializer.Deserialize matches property names case-sensitively unless told otherwise, so a DatabaseCredentials with plain Username/Password properties deserializes without error into an object whose every field is "". The null-coalescing throw does not fire, because the object is not null. Either the [JsonPropertyName] attributes above or new JsonSerializerOptions { PropertyNameCaseInsensitive = true } fixes it; the explicit check on the populated values is what turns the remaining failure modes into a startup error instead of a login attempt with an empty password.

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 the order they are added. Each one OVERRIDES the
// ones above it, so the last entry wins - appsettings.json is the weakest
// source, not the strongest.
// 1. appsettings.json
// 2. appsettings.{Environment}.json
// 3. User Secrets (Development only)
// 4. Environment variables
// 5. Command-line arguments
// 6. Azure Key Vault, because it is added below - after CreateBuilder

// 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. Rotating a secret is then a provider update and a restart, not a code change.

.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 explicit check fails startup with a clear message rather than letting an empty connection string through. Prefer deployment-time substitution from a secret store or platform configuration, and do not commit transformed production configs. This legacy approach predates modern secret stores but still separates code from config when operational controls keep the substituted secrets out of source and artifacts.

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 Microsoft Entra 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 Microsoft Entra authentication. The application's identity is granted database access through Azure RBAC, not username/password. Tokens are short-lived and rotated by Azure, so there is no password to store, rotate, or leak. The same DefaultAzureCredential works for Azure SQL, Storage, Key Vault, and other Azure services.

Detecting Hard-coded Secrets

Using git-secrets

# Install git-secrets

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

# Register the built-in AWS patterns, then add your own.

# Patterns are POSIX extended regular expressions - use [[:space:]], not \s.

# Escape the single quote as '' so PowerShell passes one argument, not three.

git secrets --register-aws
git secrets --add 'password[[:space:]]*=[[:space:]]*["'']'
git secrets --add 'apikey[[:space:]]*=[[:space:]]*["'']'

# Scan repository

git secrets --scan

# Add pre-commit hook

git secrets --install

Using TruffleHog

# Install TruffleHog. It is a Go binary - the PyPI "truffleHog" package is

# the abandoned v2 from 2018 and does not accept the arguments below.

docker pull trufflesecurity/trufflehog:latest

# Scan a repository

docker run --rm -it trufflesecurity/trufflehog:latest github --repo 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

Additional Azure Key Vault Practices

The Azure Key Vault pattern shown above (Secure Patterns) covers the core integration. For production deployments, also:

  • Use Managed Identity, not access keys: enable System-Assigned or User-Assigned Managed Identity on the App Service/VM/AKS resource and grant it a Key Vault access policy (az keyvault set-policy --secret-permissions get list); DefaultAzureCredential picks it up automatically.
  • Separate vaults by environment: never share one Key Vault across dev, staging, and production - use different vaults and different Managed Identities per environment.
  • Enable soft delete and purge protection (--enable-soft-delete true --enable-purge-protection true) so secrets aren't lost to accidental deletion.
  • Monitor access: enable Azure Monitor diagnostics for the vault and alert on unusual SecretGet calls or failed authentication attempts.
  • Use secret versioning for rotation: fetch a specific version during staged rollout, or the latest version once rotation is complete, instead of hard-coding a version everywhere.
  • Cache retrieved secrets briefly (for example with IMemoryCache, a 15-30 minute TTL) to reduce Key Vault API calls without holding stale credentials indefinitely.

Common Pitfalls

  • Moving a connection string from a const string in code into appsettings.json and committing that file with the real password still in it - IConfiguration now reads it "from configuration" instead of a literal, but the file is still tracked by git, so nothing about the exposure actually changed.
  • Using dotnet user-secrets correctly for local development but then copying the same secrets.json values into a committed appsettings.Production.json "just to get it deployed" - User Secrets are explicitly documented as development-only and unencrypted; treating the file itself as portable defeats the reason it lives outside the repo.
  • Registering Azure Key Vault with DefaultAzureCredential but leaving a fallback Environment.GetEnvironmentVariable("DB_PASSWORD") in the same constructor for "local dev convenience," where that fallback value is set directly in a committed launchSettings.json or Docker Compose file - the fallback path reintroduces a hard-coded credential in version control even though the primary path is correct.
  • Granting the Managed Identity or service principal get, list, and set/delete permissions on the Key Vault instead of get/list only - an application that can write or delete secrets has a much larger blast radius if compromised than one that can only read the specific secrets it needs.

Additional Resources