Isaac.

Securing Sensitive Data in Configuration

Learn how to protect database passwords, API keys, and other secrets in your application configuration.

By EMEPublished: February 20, 2025
securitysecrets managementconfigurationpasswordsapi keysvaultencryption

A Simple Analogy

Imagine storing important documents in a building:

  • Leaving secrets in code: Like leaving passwords written on sticky notes on your desk where anyone can see them
  • Using .env files locally: Like keeping documents in a locked drawer at home (safe for development)
  • Using secrets management: Like storing documents in a bank vault with restricted access and audit trails (safe for production)

The more sensitive the data, the more secure the storage should be.


What Are Sensitive Data?

Sensitive data is any information that should only be accessible to authorized people and systems:

  • Database passwords
  • API keys and tokens
  • Encryption keys
  • Private credentials (AWS, Azure, Google Cloud)
  • OAuth client secrets
  • Webhook signing keys
  • Payment processor tokens
  • Personal user data (emails, phone numbers)

Why Protect Configuration Secrets?

  • Breach prevention: Leaked secrets = compromised systems and data
  • Compliance: GDPR, HIPAA, PCI-DSS require protecting credentials
  • Trust: Users trust you to protect their data
  • Financial: A leaked secret can cost millions to remediate
  • Multi-environment: Different secrets for dev, staging, production
  • Team security: Developers shouldn't have production credentials

The Problem: Hardcoding Secrets

Why Hardcoding Is Dangerous

// ✗ DANGEROUS - Secret is visible to anyone with access to source code
public class DatabaseConfig
{
    public string ConnectionString = "Server=prod.db.com;User=admin;Password=MySecretPassword123!";
}

// ✗ DANGEROUS - Secret is visible in git history forever
// Even if you delete it, it's still in git commits

The Risks:

  1. Source code exposure: Anyone with repo access sees secrets
  2. Git history: Secrets persist in commit history even after deletion
  3. Accidental commits: Developers accidentally commit secrets
  4. Public repositories: If you open-source, everyone sees secrets
  5. Team turnover: Secrets spread to former employees' devices

Solution 1: Environment Variables (Local Development)

Using .env Files

Create a .env file (never commit to git):

# .env (local development only)
DATABASE_PASSWORD=dev_password
API_KEY=dev_key_12345
STRIPE_SECRET=sk_test_12345
JWT_SECRET=my_dev_jwt_secret

Add to .gitignore:

.env
.env.local
.env.*.local

.env.example (Document What's Needed)

# .env.example (commit this to show what variables are needed)
DATABASE_PASSWORD=
API_KEY=
STRIPE_SECRET=
JWT_SECRET=

Read Environment Variables

C# / ASP.NET Core:

var builder = WebApplication.CreateBuilder(args);

// IConfiguration automatically reads environment variables
var databasePassword = builder.Configuration["DatabasePassword"];
var apiKey = builder.Configuration["ApiKey"];

// Or from environment directly
var stripeSecret = Environment.GetEnvironmentVariable("STRIPE_SECRET");

builder.Services.AddDbContext<AppContext>(options =>
    options.UseSqlServer($"Server=localhost;Password={databasePassword}"));

JavaScript / Node.js:

// Load .env file
require('dotenv').config();

const dbPassword = process.env.DATABASE_PASSWORD;
const apiKey = process.env.API_KEY;

// Use in configuration
const pool = new Pool({
    host: 'localhost',
    password: dbPassword,
});

Python:

import os
from dotenv import load_dotenv

load_dotenv()  # Load from .env

db_password = os.getenv('DATABASE_PASSWORD')
api_key = os.getenv('API_KEY')

Solution 2: Secrets Managers (Production)

For production, use enterprise secrets management solutions.

AWS Secrets Manager

using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;

var client = new AmazonSecretsManagerClient();

var request = new GetSecretValueRequest
{
    SecretId = "prod/database-password"
};

var response = await client.GetSecretValueAsync(request);
var dbPassword = response.SecretString;  // Encrypted secret

// Configure database
builder.Services.AddDbContext<AppContext>(options =>
    options.UseSqlServer($"Server=prod.db.com;Password={dbPassword}");

Azure Key Vault

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

var keyVaultUrl = new Uri("https://mykeyvault.vault.azure.net/");
var client = new SecretClient(keyVaultUrl, new DefaultAzureCredential());

var secret = await client.GetSecretAsync("DatabasePassword");
var dbPassword = secret.Value.Value;

builder.Services.AddDbContext<AppContext>(options =>
    options.UseSqlServer($"Server=prod.db.com;Password={dbPassword}"));

HashiCorp Vault

using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;

var httpClientHandler = new HttpClientHandler();
httpClientHandler.ServerCertificateCustomValidationCallback = 
    (message, cert, chain, errors) => true;

var authMethod = new TokenAuthMethodInfo(vaultToken: "my-token");
var vaultClientSettings = new VaultClientSettings("http://127.0.0.1:8200", authMethod)
{
    Namespace = ""
};

IVaultClient vaultClient = new VaultClient(vaultClientSettings);

var secret = await vaultClient.V1.Secrets.KeyValue.V2
    .ReadSecretAsync(path: "database-password");

var dbPassword = secret.Data.Data["password"].ToString();

Solution 3: Docker & Kubernetes

Docker with Environment Variables

# Dockerfile - don't hardcode secrets!
FROM mcr.microsoft.com/dotnet/sdk:7.0

WORKDIR /app
COPY . .
RUN dotnet build

# Secrets passed at runtime
ENTRYPOINT ["dotnet", "run"]

Run with secrets:

docker run \
  -e DATABASE_PASSWORD="prod_password" \
  -e API_KEY="prod_key" \
  -e JWT_SECRET="prod_jwt_secret" \
  myapp:latest

Kubernetes Secrets

# kubernetes-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  database-password: cHJvZF9wYXNzd29yZA==  # base64 encoded
  api-key: cHJvZF9rZXk=

---
apiVersion: v1
kind: Pod
metadata:
  name: myapp
spec:
  containers:
  - name: myapp
    image: myapp:latest
    env:
    - name: DATABASE_PASSWORD
      valueFrom:
        secretKeyRef:
          name: app-secrets
          key: database-password
    - name: API_KEY
      valueFrom:
        secretKeyRef:
          name: app-secrets
          key: api-key

Best Practices

1. Use Strong, Unique Secrets

✗ Bad: password123, admin, secret
✓ Good: 8f#9$x&2Kp@nL!qR7vM (random, 32+ chars)

2. Rotate Secrets Regularly

- Change database passwords quarterly
- Rotate API keys annually
- Update after team member departures
- Immediately after suspected compromise

3. Principle of Least Privilege

// Only give each service the secrets it needs
// App only needs DATABASE_PASSWORD, not STRIPE_SECRET
var databasePassword = builder.Configuration["DatabasePassword"];
// Stripe service gets only its secret, separately

4. Audit Access Logs

- Who accessed the secret?
- When was it accessed?
- From which system/IP?
- Was it accessed unusually?

5. Never Log Secrets

// ✗ WRONG - Logs contain the secret!
_logger.LogInformation($"Connecting with password: {dbPassword}");

// ✓ CORRECT - Only log that connection was successful
_logger.LogInformation("Database connection established");

6. Use HTTPS for All Communication

// Secrets should only be transmitted over encrypted channels
app.UseHttpsRedirection();
app.UseHsts();

7. Separate Development & Production Secrets

Development:
  .env (local file, never committed)
  
Staging:
  Azure Key Vault / AWS Secrets Manager
  
Production:
  Highly restricted, multi-layer access
  Encryption at rest
  Audit logging

Practical Example: Secure Configuration

Structure

MyApp/
  MyApp.csproj
  Program.cs
  appsettings.json              # Public config
  appsettings.Development.json  # Dev-specific (public)
  appsettings.Production.json   # Prod-specific, reviewed
  .env                          # Local secrets (gitignore)
  .env.example                  # Template (commit)

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  },
  "Database": {
    "Server": "prod.db.example.com",
    "Port": 5432,
    "Name": "myapp"
  },
  "Jwt": {
    "Issuer": "myapp",
    "Audience": "myapp-users"
  }
}

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Load environment-specific config
builder.Configuration
    .AddJsonFile("appsettings.json", optional: false)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
    .AddEnvironmentVariables()
    .Build();

// Inject secrets from various sources
if (builder.Environment.IsProduction())
{
    var keyVaultUrl = new Uri(builder.Configuration["KeyVault:Url"]);
    builder.Configuration.AddAzureKeyVault(
        keyVaultUrl,
        new DefaultAzureCredential());
}

// Configure with secrets
var dbPassword = builder.Configuration["DatabasePassword"];
var jwtSecret = builder.Configuration["JwtSecret"];

builder.Services.AddDbContext<AppContext>(options =>
    options.UseSqlServer(
        $"Server={builder.Configuration["Database:Server"]};Password={dbPassword}"));

builder.Services.Configure<JwtOptions>(options =>
    options.Secret = jwtSecret);

Real-World Scenario: Detecting Leaked Secrets

// GitHub detected a secret in your commit
// Action steps:

1. Immediately rotate the secret
2. Check access logs for unauthorized use
3. Remove it from git history (git filter-branch)
4. Alert team and users if data was compromised
5. Implement secret scanning in CI/CD

Tools for Secret Scanning

Development:
  - git-secrets (local hooks)
  - pre-commit framework
  - IDE plugins (JetBrains, VS Code)

CI/CD:
  - GitHub Secret Scanning
  - GitLab Secret Detection
  - HashiCorp Sentinel
  - Snyk

Related Concepts to Explore

  • Key rotation strategies
  • Encryption at rest vs. in transit
  • TLS/SSL certificates
  • Certificate authorities
  • Credential management
  • Zero-trust security model
  • OAuth and OpenID Connect
  • SAML and enterprise SSO
  • Multi-factor authentication (MFA)
  • Hardware security modules (HSM)
  • Compliance (GDPR, HIPAA, PCI-DSS)
  • Data encryption libraries
  • Secret versioning and rollback

Summary

Securing sensitive data in configuration is non-negotiable. Never hardcode secrets in source code. For development, use .env files; for production, use enterprise secrets managers like AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault. Rotate secrets regularly, audit access, apply least privilege, and never log sensitive data. By following these practices, you protect your users' data and your organization's reputation.