Skip to content

Secrets Management in ConnectSoft Microservice Template

Purpose & Overview

Secrets Management is the secure handling of sensitive information such as connection strings, API keys, passwords, tokens, and certificates. In the ConnectSoft Microservice Template, secrets management follows security-first principles, ensuring that sensitive data is never stored in source control, properly encrypted, and accessed only by authorized services using secure patterns.

Secrets management provides:

  • Zero Secrets in Source Control: Secrets never committed to Git repositories
  • Centralized Management: Azure Key Vault for production secrets
  • Environment-Specific Secrets: Different secrets for development, staging, and production
  • Secure Injection: Managed Identity authentication for Key Vault access
  • Automatic Rotation: Key Vault handles secret rotation and versioning
  • Audit Logging: Track secret access and changes
  • Local Development Support: User Secrets and environment variables for local development

Secrets Management Philosophy

Secrets are the crown jewels of any application. The template follows a zero-trust approach where secrets are never stored in source control, never logged, and accessed only through secure channels. Production secrets are managed through Azure Key Vault with managed identity authentication, while local development uses User Secrets or environment variables. This ensures that sensitive information is protected throughout the entire application lifecycle.

Architecture Overview

Secrets Management Hierarchy

Production (Azure)
    ├── Azure Key Vault
    │   ├── Connection Strings
    │   ├── API Keys
    │   ├── Passwords
    │   └── Certificates
    ├── Azure App Configuration
    │   └── Key Vault References (@Microsoft.KeyVault)
    └── Managed Identity Authentication
Local Development
    ├── User Secrets (dotnet user-secrets)
    ├── Environment Variables
    └── .env files (optional)
Container/Kubernetes
    ├── Kubernetes Secrets
    ├── Environment Variables (from Secrets)
    └── Volume Mounts (for certificates)

Secrets Flow

Secret Storage
    ├── Azure Key Vault (Production)
    │   ├── Managed Identity Access
    │   ├── RBAC Authorization
    │   └── Audit Logging
    ├── Azure App Configuration
    │   └── Key Vault References
    └── Environment Variables (Local/Dev)
Configuration Provider
    ├── Azure App Configuration Provider
    │   └── Resolves Key Vault References
    ├── Environment Variables Provider
    └── User Secrets Provider (Local)
Application Configuration (IConfiguration)
    └── Options Pattern (Strongly-Typed Access)

Secrets Storage Options

Azure Key Vault (Production)

Purpose: Centralized, secure secret storage for production environments.

Features: - Encryption: Hardware Security Modules (HSM) or software encryption - Access Control: Role-Based Access Control (RBAC) - Versioning: Automatic versioning of secrets - Rotation: Built-in secret rotation support - Audit Logging: Complete audit trail of secret access - Soft Delete: Secrets can be recovered after deletion - Managed Identity: No connection strings needed

Key Vault Reference Syntax:

{
  "ConnectionStrings": {
    "DefaultConnection": "@Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/DbConnectionString)"
  }
}

Alternative Syntax:

{
  "ConnectionStrings": {
    "DefaultConnection": "@Microsoft.KeyVault(VaultName=my-vault;SecretName=DbConnectionString)"
  }
}

Azure App Configuration with Key Vault References

Purpose: Centralized configuration management with secure secret references.

Configuration in Azure App Configuration:

Key: ConnectSoft.MicroserviceTemplate:Database:ConnectionString
Value: @Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/DbConnectionString)

Benefits: - Secrets never stored in App Configuration - Automatic secret resolution at runtime - Centralized configuration management - Support for secret rotation

Key Vault Configuration:

configurationBuilder.AddAzureAppConfiguration(options =>
{
    options.Connect(connectionString)
        .Select("ConnectSoft.MicroserviceTemplate:*", LabelFilter.Null)
        .ConfigureKeyVault(kv =>
        {
            // Use Managed Identity
            kv.SetCredential(new DefaultAzureCredential());

            // Or User-Assigned Managed Identity
            // kv.SetCredential(new ManagedIdentityCredential(clientId));
        });
});

User Secrets (Local Development)

Purpose: Secure secret storage for local development without committing secrets to source control.

Initialization:

# Navigate to project directory
cd ConnectSoft.MicroserviceTemplate.Application

# Initialize user secrets
dotnet user-secrets init

Setting Secrets:

# Set connection string
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;Database=MyDb;..."

# Set API key
dotnet user-secrets set "ApiKeys:ExternalApi" "sk-1234567890abcdef"

# Set nested configuration
dotnet user-secrets set "Database:Password" "MyPassword123!"

Listing Secrets:

# List all secrets
dotnet user-secrets list

# List specific secret
dotnet user-secrets list "ConnectionStrings"

Removing Secrets:

# Remove specific secret
dotnet user-secrets remove "ConnectionStrings:DefaultConnection"

# Clear all secrets
dotnet user-secrets clear

Location: User Secrets are stored in: - Windows: %APPDATA%\Microsoft\UserSecrets\{UserSecretsId}\secrets.json - macOS/Linux: ~/.microsoft/usersecrets/{UserSecretsId}/secrets.json

Enable in Application:

// Program.cs (typically only in Development)
if (builder.Environment.IsDevelopment())
{
    builder.Configuration.AddUserSecrets<Program>();
}

Environment Variables

Purpose: Inject secrets at runtime without modifying code or configuration files.

Format:

# Connection strings
export ConnectionStrings__DefaultConnection="Server=localhost;Database=MyDb;..."

# Nested configuration
export Database__Password="MyPassword123!"

# API keys
export ApiKeys__ExternalApi="sk-1234567890abcdef"

Priority: Environment variables override configuration files (highest priority in configuration hierarchy).

Use Cases: - Container deployments (Docker, Kubernetes) - CI/CD pipelines - Local development overrides - Production secret injection

Kubernetes Secrets

Purpose: Secure secret storage for Kubernetes deployments.

Creating Secrets:

# From literal values
kubectl create secret generic microservice-secrets \
  --from-literal=ConnectionStrings__DefaultConnection="Server=...;Database=...;..." \
  --from-literal=ApiKeys__ExternalApi="sk-1234567890abcdef"

# From file
kubectl create secret generic microservice-secrets \
  --from-file=connection-string.txt

YAML Manifest:

apiVersion: v1
kind: Secret
metadata:
  name: microservice-secrets
  namespace: default
type: Opaque
stringData:
  ConnectionStrings__DefaultConnection: "Server=...;Database=...;..."
  ApiKeys__ExternalApi: "sk-1234567890abcdef"

Using Secrets in Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: microservice
spec:
  template:
    spec:
      containers:
      - name: microservice
        image: microservice:latest
        env:
        - name: ConnectionStrings__DefaultConnection
          valueFrom:
            secretKeyRef:
              name: microservice-secrets
              key: ConnectionStrings__DefaultConnection
        - name: ApiKeys__ExternalApi
          valueFrom:
            secretKeyRef:
              name: microservice-secrets
              key: ApiKeys__ExternalApi

Note: Kubernetes secrets are base64-encoded, not encrypted. For production, use External Secrets Operator with Azure Key Vault.

Authentication Methods

Purpose: Secure, credential-free authentication to Azure Key Vault.

System-Assigned Managed Identity:

configurationBuilder.AddAzureAppConfiguration(options =>
{
    options.Connect(new Uri("https://{store-name}.azconfig.io"))
        .ConfigureKeyVault(kv =>
        {
            kv.SetCredential(new DefaultAzureCredential());
        });
});

User-Assigned Managed Identity:

configurationBuilder.AddAzureAppConfiguration(options =>
{
    options.Connect(new Uri("https://{store-name}.azconfig.io"))
        .ConfigureKeyVault(kv =>
        {
            kv.SetCredential(new ManagedIdentityCredential(clientId));
        });
});

Benefits: - No connection strings or credentials needed - Automatic credential rotation - Better security posture - Azure AD integration

Connection String Authentication

Purpose: Alternative authentication method (less secure, not recommended for production).

Connection String Format:

Endpoint=https://{store-name}.azconfig.io;Id={id};Secret={secret}

Usage:

configurationBuilder.AddAzureAppConfiguration(options =>
{
    string connectionString = configuration.GetConnectionString("AzureAppConfiguration");
    options.Connect(connectionString);
});

Security Considerations: - Connection string must be stored securely - Use Key Vault reference for connection string itself - Rotate secrets regularly - Consider using Managed Identity instead

Configuration Patterns

Connection Strings

Storage: Connection strings are stored in ConnectionStrings section.

Example:

{
  "ConnectionStrings": {
    "DefaultConnection": "@Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/DbConnectionString)",
    "AzureAppConfiguration": "@Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/AppConfigConnectionString)"
  }
}

Access:

var connectionString = configuration.GetConnectionString("DefaultConnection");

API Keys

Storage: API keys stored in configuration sections.

Example:

{
  "ApiKeys": {
    "ExternalApi": "@Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/ExternalApiKey)"
  }
}

Options Class:

public sealed class ApiKeysOptions
{
    public const string ApiKeysSectionName = "ApiKeys";

    [Required]
    required public string ExternalApi { get; set; }
}

Access:

public class ExternalApiClient
{
    private readonly ApiKeysOptions apiKeys;

    public ExternalApiClient(IOptions<ApiKeysOptions> apiKeys)
    {
        this.apiKeys = apiKeys.Value;
    }

    public async Task CallApiAsync()
    {
        var apiKey = this.apiKeys.ExternalApi;
        // Use API key
    }
}

Nested Secrets

Storage: Nested configuration structure.

Example:

{
  "Database": {
    "Server": "db.example.com",
    "Password": "@Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/DbPassword)"
  }
}

Options Class:

public sealed class DatabaseOptions
{
    public const string DatabaseSectionName = "Database";

    [Required]
    required public string Server { get; set; }

    [Required]
    required public string Password { get; set; }
}

Secret Injection Strategies

Strategy 1: Azure App Configuration with Key Vault References

Best For: Production environments with Azure infrastructure.

Configuration:

configurationBuilder.AddAzureAppConfiguration(options =>
{
    options.Connect(new Uri("https://{store-name}.azconfig.io"), new DefaultAzureCredential())
        .Select("ConnectSoft.MicroserviceTemplate:*", LabelFilter.Null)
        .ConfigureKeyVault(kv =>
        {
            kv.SetCredential(new DefaultAzureCredential());
        });
});

Key Vault Reference in App Configuration:

Key: ConnectSoft.MicroserviceTemplate:Database:ConnectionString
Value: @Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/DbConnectionString)

Strategy 2: Direct Key Vault Provider

Best For: Direct Key Vault access without App Configuration.

Configuration:

configurationBuilder.AddAzureKeyVault(
    new Uri("https://{vault-name}.vault.azure.net/"),
    new DefaultAzureCredential());

Access:

var secret = configuration["Database:ConnectionString"];

Strategy 3: Environment Variables (Container/Kubernetes)

Best For: Containerized deployments and CI/CD pipelines.

Configuration:

# Kubernetes Secret
apiVersion: v1
kind: Secret
metadata:
  name: microservice-secrets
stringData:
  ConnectionStrings__DefaultConnection: "Server=...;Database=...;..."

# Deployment
env:
- name: ConnectionStrings__DefaultConnection
  valueFrom:
    secretKeyRef:
      name: microservice-secrets
      key: ConnectionStrings__DefaultConnection

Strategy 4: User Secrets (Local Development)

Best For: Local development without committing secrets.

Configuration:

if (builder.Environment.IsDevelopment())
{
    builder.Configuration.AddUserSecrets<Program>();
}

Setting Secrets:

dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;Database=MyDb;..."

Secret Naming Conventions

Key Vault Secret Naming

Pattern: {Service}-{Environment}-{SecretType} or {Service}-{SecretType}

Examples: - MicroserviceTemplate-DbConnectionString - MicroserviceTemplate-Prod-ExternalApiKey - MicroserviceTemplate-Dev-RedisConnectionString - JwtSigningKey - EncryptionKey

Configuration Key Naming

Pattern: {Service}:{Section}:{Property}

Examples: - ConnectSoft.MicroserviceTemplate:Database:ConnectionString - ConnectSoft.MicroserviceTemplate:ApiKeys:ExternalApi - ConnectSoft.MicroserviceTemplate:Authentication:JwtSecret

Environment Variable Naming

Pattern: {Section}__{SubSection}__{Property} (double underscore for nesting)

Examples: - ConnectionStrings__DefaultConnection - Database__Password - ApiKeys__ExternalApi

Secret Rotation

Azure Key Vault Rotation

Automatic Rotation:

// Key Vault automatically supports secret versions
// Access latest version
var secret = await secretClient.GetSecretAsync("DbConnectionString");

// Access specific version
var secret = await secretClient.GetSecretAsync("DbConnectionString", "v2");

Rotation Strategy:

  1. Create New Secret Version: Add new version in Key Vault
  2. Update Reference: Update App Configuration reference (if needed)
  3. Verify: Test application with new secret
  4. Rollback: Use previous version if issues occur

Application Handling

Graceful Rotation:

public class DatabaseConnectionManager
{
    private readonly IConfiguration configuration;
    private readonly ILogger<DatabaseConnectionManager> logger;

    public async Task<string> GetConnectionStringAsync()
    {
        var connectionString = configuration.GetConnectionString("DefaultConnection");

        // Test connection
        if (!await TestConnectionAsync(connectionString))
        {
            logger.LogWarning("Primary connection failed, trying previous version");
            // Fallback to previous version if supported
        }

        return connectionString;
    }
}

Security Best Practices

Do's

  1. Use Managed Identity for Production

    // ✅ GOOD - No credentials needed
    kv.SetCredential(new DefaultAzureCredential());
    

  2. Never Commit Secrets

    // ❌ BAD - Never commit secrets
    {
      "ApiKey": "sk-1234567890abcdef"
    }
    
    // ✅ GOOD - Use Key Vault reference
    {
      "ApiKey": "@Microsoft.KeyVault(SecretUri=...)"
    }
    

  3. Use User Secrets for Local Development

    # ✅ GOOD - Secrets stored locally, not in Git
    dotnet user-secrets set "ConnectionStrings:DefaultConnection" "..."
    

  4. Separate Secrets from Configuration

    // ✅ GOOD - Non-secret config in files, secrets in Key Vault
    {
      "Database": {
        "Server": "db.example.com"
        // Password from Key Vault
      }
    }
    

  5. Use Environment Variables for Containers

    # ✅ GOOD - Secrets injected via environment variables
    env:
    - name: ConnectionStrings__DefaultConnection
      valueFrom:
        secretKeyRef:
          name: microservice-secrets
          key: ConnectionStrings__DefaultConnection
    

  6. Enable Audit Logging

    // ✅ GOOD - Key Vault automatically logs all access
    // Review logs regularly
    

  7. Rotate Secrets Regularly

    # ✅ GOOD - Set expiration dates and rotate secrets
    az keyvault secret set --vault-name my-vault --name ApiKey --value "new-key" --expires 2025-12-31
    

Don'ts

  1. Don't Store Secrets in Source Control

    // ❌ BAD - Never commit secrets
    {
      "ConnectionStrings": {
        "DefaultConnection": "Server=db;Password=Secret123!"
      }
    }
    

  2. Don't Log Secrets

    // ❌ BAD - Secrets in logs
    logger.LogInformation("Using API key: {ApiKey}", apiKey);
    
    // ✅ GOOD - Redact or omit
    logger.LogInformation("Using API key: {ApiKey}", "[REDACTED]");
    

  3. Don't Use Connection Strings for Key Vault Access in Production

    // ❌ BAD - Connection string in production
    options.Connect(connectionString);
    
    // ✅ GOOD - Managed Identity
    options.Connect(new Uri("https://..."), new DefaultAzureCredential());
    

  4. Don't Hardcode Secrets

    // ❌ BAD - Hardcoded secret
    var apiKey = "sk-1234567890abcdef";
    
    // ✅ GOOD - From configuration
    var apiKey = configuration["ApiKeys:ExternalApi"];
    

  5. Don't Share Secrets Between Environments

    // ❌ BAD - Same secret for dev and prod
    {
      "ApiKey": "shared-secret"
    }
    
    // ✅ GOOD - Environment-specific secrets
    // Dev: Key Vault reference to dev secret
    // Prod: Key Vault reference to prod secret
    

  6. Don't Store Secrets in Plain Text

    # ❌ BAD - Plain text in environment variable (visible in process list)
    export API_KEY="sk-1234567890abcdef"
    
    # ✅ GOOD - Use secure secret management
    # User Secrets, Key Vault, or encrypted Kubernetes secrets
    

Common Scenarios

Scenario 1: Database Connection String

Key Vault:

Secret Name: DbConnectionString
Secret Value: Server=db.example.com;Database=MyDb;User Id=appuser;Password=P@ssw0rd!;

Azure App Configuration:

Key: ConnectSoft.MicroserviceTemplate:Database:ConnectionString
Value: @Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/DbConnectionString)

Access:

var connectionString = configuration.GetConnectionString("DefaultConnection");
// Or
var connectionString = configuration["ConnectSoft.MicroserviceTemplate:Database:ConnectionString"];

Scenario 2: External API Key

Key Vault:

Secret Name: ExternalApiKey
Secret Value: sk-1234567890abcdef

Configuration:

{
  "ApiKeys": {
    "ExternalApi": "@Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/ExternalApiKey)"
  }
}

Options Class:

public sealed class ApiKeysOptions
{
    public const string ApiKeysSectionName = "ApiKeys";

    [Required]
    required public string ExternalApi { get; set; }
}

Registration:

services.AddOptions<ApiKeysOptions>()
    .BindConfiguration(ApiKeysOptions.ApiKeysSectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart();

Usage:

public class ExternalApiClient
{
    private readonly ApiKeysOptions apiKeys;

    public ExternalApiClient(IOptions<ApiKeysOptions> apiKeys)
    {
        this.apiKeys = apiKeys.Value;
    }

    public async Task<Response> CallApiAsync()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/data");
        request.Headers.Add("X-API-Key", this.apiKeys.ExternalApi);

        // Make request
    }
}

Scenario 3: JWT Signing Key

Key Vault:

Secret Name: JwtSigningKey
Secret Value: (base64-encoded key)

Configuration:

{
  "Authentication": {
    "Jwt": {
      "SigningKey": "@Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/JwtSigningKey)"
    }
  }
}

Access:

var signingKey = configuration["Authentication:Jwt:SigningKey"];

Scenario 4: Local Development Setup

Initialize User Secrets:

cd ConnectSoft.MicroserviceTemplate.Application
dotnet user-secrets init

Set Secrets:

dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;Database=MyDb;User Id=sa;Password=Password@123;"
dotnet user-secrets set "ApiKeys:ExternalApi" "sk-dev-key-1234567890abcdef"

Enable in Application:

// Program.cs
if (builder.Environment.IsDevelopment())
{
    builder.Configuration.AddUserSecrets<Program>();
}

Troubleshooting

Issue: Key Vault Reference Not Resolving

Symptoms: Configuration value shows @Microsoft.KeyVault(...) instead of actual secret.

Solutions: 1. Verify Managed Identity:

# Check if managed identity is assigned
az webapp identity show --name <app-name> --resource-group <rg-name>

  1. Check Key Vault Access Policy:

    # Grant access to managed identity
    az keyvault set-policy --name <vault-name> --object-id <identity-id> --secret-permissions get list
    

  2. Verify Secret URI:

    // Ensure URI is correct
    "@Microsoft.KeyVault(SecretUri=https://{vault}.vault.azure.net/secrets/{secret-name})"
    

  3. Check Key Vault Configuration:

    // Ensure Key Vault is configured
    .ConfigureKeyVault(kv => kv.SetCredential(new DefaultAzureCredential()))
    

Issue: User Secrets Not Loading

Symptoms: User Secrets not available in application.

Solutions: 1. Verify User Secrets ID:

<!-- .csproj file -->
<PropertyGroup>
  <UserSecretsId>your-user-secrets-id</UserSecretsId>
</PropertyGroup>

  1. Check Registration:

    // Ensure User Secrets are added
    if (builder.Environment.IsDevelopment())
    {
        builder.Configuration.AddUserSecrets<Program>();
    }
    

  2. Verify Secret Names:

    # List secrets to verify names
    dotnet user-secrets list
    

Issue: Environment Variables Not Overriding

Symptoms: Environment variables not taking effect.

Solutions: 1. Check Naming Format:

# ✅ GOOD - Double underscore for nesting
export ConnectionStrings__DefaultConnection="..."

# ❌ BAD - Single underscore
export ConnectionStrings_DefaultConnection="..."

  1. Verify Load Order:

    // Environment variables should be added last (highest priority)
    configurationBuilder
        .AddJsonFile("appsettings.json")
        .AddEnvironmentVariables(); // Last = highest priority
    

  2. Check Variable Names:

    # Verify variable names match configuration keys exactly
    echo $ConnectionStrings__DefaultConnection
    

Issue: Secrets Visible in Logs

Symptoms: Secrets appearing in application logs.

Solutions: 1. Use Redaction:

// ✅ GOOD - Redact sensitive data
logger.LogInformation("Using API key: {ApiKey}", "[REDACTED]");

  1. Avoid Logging Secrets:

    // ❌ BAD - Don't log secrets
    logger.LogInformation("API key: {ApiKey}", apiKey);
    

  2. Use Structured Logging Carefully:

    // Be careful with structured logging
    logger.LogInformation("Request: {@Request}", request); // Ensure request doesn't contain secrets
    

Summary

Secrets management in the ConnectSoft Microservice Template provides:

  • Zero Secrets in Source Control: Secrets never committed to Git
  • Centralized Management: Azure Key Vault for production secrets
  • Secure Authentication: Managed Identity for Key Vault access
  • Local Development: User Secrets for secure local development
  • Container Support: Environment variables and Kubernetes secrets
  • Automatic Rotation: Key Vault handles secret rotation
  • Audit Logging: Complete audit trail of secret access
  • Best Practices: Security-first approach to secrets management

By following these patterns, teams can:

  • Protect Sensitive Data: Secrets never exposed in source control or logs
  • Enable Secure Access: Managed Identity eliminates credential management
  • Support Multiple Environments: Different secrets for dev, staging, and production
  • Simplify Development: User Secrets for easy local development
  • Ensure Compliance: Audit logging and access control for regulatory compliance

Secrets management is essential for maintaining security and compliance, ensuring that sensitive information is protected throughout the application lifecycle while enabling secure access for authorized services and developers.