Secrets Governance with Key Vault
Enterprise Secret Management on Azure
Introduction
Azure Key Vault is the cornerstone of security for integration workloads, providing a central repository for secrets, certificates, and keys. But simply creating a Key Vault isn't enough—you need governance around who can access what, how secrets are rotated, audit trails for compliance, and integration with your deployment pipelines. This guide covers enterprise Key Vault governance for Azure integration scenarios.
This comprehensive guide covers:
- Vault architecture — Organizing vaults for enterprise scale
- Access control — Using RBAC for fine-grained permissions
- Secret lifecycle — Rotation strategies and automation
- Network security — Private endpoints and firewall rules
- Audit and compliance — Logging and monitoring
Vault Architecture
Enterprise Vault Structure
┌─────────────────────────────────────────────────────────────────────┐
│ KEY VAULT ORGANIZATION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ SUBSCRIPTION LEVEL │
│ └──rg-security (Security Resources) │
│ │ │
│ ├──kv-platform (Shared Services Vault) │
│ │ ├── Cross-account connection strings │
│ │ ├── Platform-level certificates │
│ │ └── Shared encryption keys │
│ │ │
│ └──rg-integration (Integration Workloads) │
│ │ │
│ ├──kv-integration-prod (Production) │
│ │ ├── Service Bus connection strings │
│ │ ├── Function app keys │
│ │ └── External API keys │
│ │ │
│ ├──kv-integration-staging (Staging) │
│ │ (Same structure, lower environment) │
│ │ │
│ └──kv-integration-dev (Development) │
│ (Same structure, dev-only secrets) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Vault Configuration
# Create vault with soft-delete and purge protection
az keyvault create \
--name kv-integration-prod \
--resource-group rg-integration \
--location eastus \
--enable-soft-delete true \
--soft-delete-retention-days 90 \
--enable-purge-protection true \
--sku premium \
--enable-rbac-authorization true
# Configure network access
az keyvault update \
--name kv-integration-prod \
--public-network-access Disabled \
--bypass None
# Add private endpoint
az network private-endpoint create \
--name kv-private-endpoint \
--resource-group rg-integration \
--vnet-name vnet-integration \
--subnet private-endpoints \
--connection-name kv-connection \
--private-link-resource-type Microsoft.KeyVault/vaults \
--target-resource /subscriptions/xxx/resourceGroups/rg-integration/providers/Microsoft.KeyVault/vaults/kv-integration-prod
Access Control
RBAC Permission Model
{
"rbacRoles": {
"KeyVaultAdmin": {
"description": "Full access to manage vault and secrets",
"permissions": {
"keys": ["all"],
"secrets": ["all"],
"certificates": ["all"]
},
"assignableScopes": [
"/subscriptions/xxx/resourceGroups/rg-integration"
]
},
"KeyVaultSecretsUser": {
"description": "Can read and use secrets",
"permissions": {
"keys": ["get", "list", "unwrapKey", "verify"],
"secrets": ["get", "list"],
"certificates": ["get", "list"]
}
},
"KeyVaultSecretsOfficer": {
"description": "Can manage secrets, but not vault settings",
"permissions": {
"keys": ["get", "list"],
"secrets": ["all"],
"certificates": ["get", "list", "import", "update"]
}
}
}
}
Role Assignments
# Grant admin access to security team
az role assignment create \
--assignee security-team-group-id \
--role "Key Vault Administrator" \
--scope /subscriptions/xxx/resourceGroups/rg-integration/providers/Microsoft.KeyVault/vaults/kv-integration-prod
# Grant app access via managed identity
az role assignment create \
--assignee <function-app-mi-principal-id> \
--role "Key Vault Secrets User" \
--scope /subscriptions/xxx/resourceGroups/rg-integration/providers/Microsoft.KeyVault/vaults/kv-integration-prod
# Grant CI/CD pipeline access
az role assignment create \
--assignee deployment-service-principal-id \
--role "Key Vault Secrets Officer" \
--scope /subscriptions/xxx/resourceGroups/rg-integration/providers/Microsoft.KeyVault/vaults/kv-integration-prod
Access Policies (Non-RBAC)
{
"accessPolicies": [
{
"tenantId": "xxx",
"objectId": "function-app-object-id",
"permissions": {
"secrets": ["get", "list"],
"certificates": ["get", "list"]
}
},
{
"tenantId": "xxx",
"objectId": "devops-service-principal-id",
"permissions": {
"secrets": ["all"],
"keys": ["all"],
"certificates": ["all"]
}
}
]
}
Secret Lifecycle Management
Secret Rotation Strategies
public class SecretRotationService
{
private readonly KeyVaultClient _keyVaultClient;
private readonly string _vaultName;
public async Task RotateSecretAsync(string secretName, TimeSpan validity)
{
// Get current secret version
var currentSecret = await _keyVaultClient
.GetSecretAsync(_vaultName, secretName);
// Create new version
var newSecret = new SecretProperties(secretName)
{
Enabled = true,
Expires = DateTime.UtcNow.Add(validity),
Tags = new Dictionary<string, string>
{
{ "rotation-required", "true" },
{ "previous-version", currentSecret.Properties.Version },
{ "rotated-by", Environment.MachineName }
}
};
// Generate new secret value
var newValue = GenerateNewSecretValue();
// Set new secret
await _keyVaultClient.SetSecretAsync(_vaultName, secretName, newValue, newSecret);
// Notify consumers (via Event Grid)
await PublishRotationEventAsync(secretName);
}
public async Task<List<SecretSummary>> GetExpiringSecretsAsync(int daysThreshold)
{
var secrets = await _keyVaultClient.GetSecretsAsync(_vaultName);
var expiring = new List<SecretSummary>();
foreach (var secret in secrets)
{
var versions = await _keyVaultClient
.GetSecretVersionsAsync(_vaultName, secret.Name);
var latest = versions.FirstOrDefault();
if (latest.Expires.HasValue &&
latest.Expires.Value < DateTime.UtcNow.AddDays(daysThreshold))
{
expiring.Add(new SecretSummary
{
Name = secret.Name,
ExpiresOn = latest.Expires.Value,
DaysUntilExpiry = (latest.Expires.Value - DateTime.UtcNow).Days
});
}
}
return expiring;
}
}
Automatic Rotation Configuration
{
"rotationConfig": {
"enabled": true,
"schedule": "0 0 * * 0", // Weekly on Sunday
"secrets": [
{
"name": "ServiceBusConnection",
"rotationPeriodDays": 90,
"notifyBeforeDays": 30,
"autoRotate": true,
"rotateTo": "IntegrationFunctions"
},
{
"name": "ExternalApiKey",
"rotationPeriodDays": 30,
"notifyBeforeDays": 7,
"autoRotate": false,
"manualApprovalRequired": true
},
{
"name": "EncryptionKey",
"rotationPeriodDays": 365,
"notifyBeforeDays": 60,
"autoRotate": true,
"rotateTo": "KeyVault"
}
],
"notifications": {
"email": "security-team@company.com",
"slack": "#security-alerts",
"teams": "Security Channel"
}
}
}
Network Security
Private Endpoint Configuration
{
"networkSecurity": {
"publicAccess": "Disabled",
"privateEndpoints": [
{
"name": "kv-pe-integration",
"subnetId": "/subscriptions/xxx/resourceGroups/rg-vnet/providers/Microsoft.Network/virtualNetworks/vnet-integration/subnets/private",
"connectionMethod": "PrivateLink",
"approvalRequired": false
}
],
"ipRules": [
{
"action": "Allow",
"value": "10.0.0.0/24" # CI/CD agents
}
],
"trustedServices": [
"AzureAppService",
"AzureFunctionApp",
"AzureLogicApps"
]
}
}
Firewall Rules
# Allow specific IPs
az keyvault update \
--name kv-integration-prod \
--ip "203.0.113.0/24" \
--ip "198.51.100.0/24"
# Allow Azure services (trusted)
az keyvault update \
--name kv-integration-prod \
--allow-bypass "AzureServices"
# Verify network rules
az keyvault show \
--name kv-integration-prod \
--query networkAcls
Audit and Compliance
Diagnostic Settings
# Enable logging to Log Analytics
az monitor diagnostic-settings create \
--name kv-audit \
--resource /subscriptions/xxx/resourceGroups/rg-integration/providers/Microsoft.KeyVault/vaults/kv-integration-prod \
--workspace /subscriptions/xxx/resourceGroups/rg-logging/providers/Microsoft.OperationalInsights/workspaces/log-analytics-workspace \
--logs '[
{"category": "AuditEvent", "enabled": true},
{"category": "Security", "enabled": true},
{"category": "Request", "enabled": true}
]' \
--metrics '[
{"category": "AllMetrics", "enabled": true}
]'
Audit Queries
// Recent secret access
AzureDiagnostics
| where ResourceType == "VAULTS"
| where OperationName contains "Secret"
| project TimeGenerated, OperationName, Identity, SecretName
| order by TimeGenerated desc
// All write operations (secret creation/update)
AzureDiagnostics
| where ResourceType == "VAULTS"
| where OperationName contains "write"
| project TimeGenerated, OperationName, Identity, ResultType, SecretName
// Failed access attempts
AzureDiagnostics
| where ResourceType == "VAULTS"
| where ResultType == "Failure"
| project TimeGenerated, OperationName, Identity, ErrorCode
// Secret access by application
AzureDiagnostics
| where ResourceType == "VAULTS"
| where OperationName contains "get"
| extend AppId = parse_json(Identity)[0].get("appId")
| summarize count() by AppId, bin(TimeGenerated, 1h)
Alert Rules
{
"alerts": [
{
"name": "SecretAccessOutsideBusinessHours",
"condition": "OperationName contains 'get' and TimeGenerated < 06:00 or TimeGenerated > 20:00",
"severity": "Warning",
"description": "Secret accessed outside business hours"
},
{
"name": "BulkSecretDownload",
"condition": "count() > 10 per minute",
"severity": "Critical",
"description": "Bulk download of secrets detected"
},
{
"name": "SecretDeleted",
"condition": "OperationName contains 'purge' or 'delete'",
"severity": "Critical",
"description": "Secret deletion detected"
}
]
}
Best Practices
Implementation Checklist
| Practice | Description |
|---|---|
| Use RBAC | Switch from access policies to RBAC for enterprise scale |
| Private endpoints | Disable public access, use private endpoints |
| Enable auditing | Send logs to Log Analytics for analysis |
| Soft-delete | Always enable with 90-day retention |
| Purge protection | Enable to prevent accidental deletion |
| Rotation automation | Automate secret rotation where possible |
| Naming conventions | Consistent secret naming across environments |
Secret Naming
┌─────────────────────────────────────────────────────────────────────┐
│ SECRET NAMING CONVENTIONS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Pattern: {environment}-{service}-{purpose} │
│ │
│ Examples: │
│ ├── prod-servicebus-connectionstring │
│ ├── staging-functions-storagekey │
│ ├── dev-externalapi-apikey │
│ ├── prod-cosmosdb-masterkey │
│ └── shared-encryption-key │
│ │
│ Avoid: │
│ ✗ Secret, Password, Key (generic names) │
│ ✗ Production, Prod (in the name, use env prefix) │
│ ✗ Special characters that break tooling │
│ │
└─────────────────────────────────────────────────────────────────────┘
Related Topics
- Zero Trust Networking — Network security
- Entra ID at Scale — Identity management
- Compliance as Code — Policy enforcement
Azure Integration Hub - Architect Level Security Architecture & Zero Trust