4 min read
Secrets Management Patterns: Best Practices for Azure
Effective secrets management is crucial for security. Let’s explore patterns and best practices for managing secrets in Azure environments.
Secret Categories
| Category | Example | Recommended Approach |
|---|---|---|
| Azure Services | Storage, SQL | Managed Identity |
| External APIs | Third-party APIs | Key Vault |
| Certificates | TLS, Auth | Key Vault or Managed |
| Connection Strings | Legacy systems | Key Vault with rotation |
| Encryption Keys | Data encryption | Key Vault Keys |
Centralized Secret Management
// Central Key Vault setup
resource keyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' = {
name: 'kv-${environment}-secrets'
location: location
properties: {
sku: {
family: 'A'
name: 'premium' // For HSM-backed keys
}
tenantId: subscription().tenantId
enableRbacAuthorization: true
enableSoftDelete: true
softDeleteRetentionInDays: 90
enablePurgeProtection: true
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
virtualNetworkRules: [
{
id: appSubnet.id
}
]
ipRules: [
{
value: allowedIpRange
}
]
}
}
}
// RBAC assignments
resource secretsUserRole 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = {
scope: keyVault
name: guid(keyVault.id, appServiceIdentity, 'secrets-user')
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
principalId: appServiceIdentity
principalType: 'ServicePrincipal'
}
}
Application Configuration Pattern
// Program.cs - ASP.NET Core
using Azure.Identity;
using Azure.Extensions.AspNetCore.Configuration.Secrets;
var builder = WebApplication.CreateBuilder(args);
// Add Key Vault as configuration source
var keyVaultUri = builder.Configuration["KeyVaultUri"];
if (!string.IsNullOrEmpty(keyVaultUri))
{
builder.Configuration.AddAzureKeyVault(
new Uri(keyVaultUri),
new DefaultAzureCredential(),
new AzureKeyVaultConfigurationOptions
{
ReloadInterval = TimeSpan.FromMinutes(5)
});
}
// Use configuration
builder.Services.Configure<ApiOptions>(
builder.Configuration.GetSection("ExternalApi"));
// Key Vault secrets map to configuration
// Secret name: ExternalApi--ApiKey
// Configuration path: ExternalApi:ApiKey
{
"ExternalApi": {
"ApiKey": "from-key-vault",
"BaseUrl": "https://api.external.com"
}
}
Secrets in Kubernetes
# secrets-provider-class.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: azure-keyvault-secrets
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "true"
userAssignedIdentityID: "$IDENTITY_CLIENT_ID"
keyvaultName: "kv-prod-secrets"
objects: |
array:
- |
objectName: database-connection-string
objectType: secret
- |
objectName: api-key
objectType: secret
tenantId: "$TENANT_ID"
secretObjects:
- secretName: app-secrets
type: Opaque
data:
- objectName: database-connection-string
key: DB_CONNECTION
- objectName: api-key
key: API_KEY
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: app
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets"
readOnly: true
env:
- name: DB_CONNECTION
valueFrom:
secretKeyRef:
name: app-secrets
key: DB_CONNECTION
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: azure-keyvault-secrets
Secret Reference Pattern
// Generic secret provider abstraction
public interface ISecretProvider
{
Task<string> GetSecretAsync(string secretName);
Task<T> GetSecretAsync<T>(string secretName);
}
public class KeyVaultSecretProvider : ISecretProvider
{
private readonly SecretClient _client;
private readonly IMemoryCache _cache;
public KeyVaultSecretProvider(string vaultUri, IMemoryCache cache)
{
_client = new SecretClient(new Uri(vaultUri), new DefaultAzureCredential());
_cache = cache;
}
public async Task<string> GetSecretAsync(string secretName)
{
return await _cache.GetOrCreateAsync($"secret:{secretName}", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
var secret = await _client.GetSecretAsync(secretName);
return secret.Value.Value;
});
}
public async Task<T> GetSecretAsync<T>(string secretName)
{
var secretValue = await GetSecretAsync(secretName);
return JsonSerializer.Deserialize<T>(secretValue);
}
}
// Usage with dependency injection
services.AddSingleton<ISecretProvider>(sp =>
new KeyVaultSecretProvider(
configuration["KeyVaultUri"],
sp.GetRequiredService<IMemoryCache>()));
DevOps Pipeline Secrets
# azure-pipelines.yml
variables:
- group: Production-Secrets # Variable group linked to Key Vault
stages:
- stage: Deploy
jobs:
- deployment: Deploy
environment: Production
strategy:
runOnce:
deploy:
steps:
- task: AzureKeyVault@2
inputs:
azureSubscription: 'Azure-Connection'
KeyVaultName: 'kv-prod-secrets'
SecretsFilter: '*'
RunAsPreJob: true
- task: AzureCLI@2
inputs:
azureSubscription: 'Azure-Connection'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
# Secrets are available as environment variables
az webapp config appsettings set \
--name $(WebAppName) \
--resource-group $(ResourceGroup) \
--settings "ApiKey=$(ApiKey)"
Audit and Compliance
// Comprehensive secret access audit
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.KEYVAULT"
| where Category == "AuditEvent"
| project
TimeGenerated,
OperationName,
ResultType,
CallerIPAddress,
Resource = id_s,
Identity = identity_claim_upn_s,
ClientInfo = client_info_s
| where OperationName in ("SecretGet", "SecretSet", "SecretDelete", "SecretList")
| summarize
AccessCount = count(),
UniqueIdentities = dcount(Identity)
by Resource, OperationName, bin(TimeGenerated, 1h)
Effective secrets management combines centralization, automation, and proper access controls to maintain security while enabling developer productivity.