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

PracticeDescription
Use RBACSwitch from access policies to RBAC for enterprise scale
Private endpointsDisable public access, use private endpoints
Enable auditingSend logs to Log Analytics for analysis
Soft-deleteAlways enable with 90-day retention
Purge protectionEnable to prevent accidental deletion
Rotation automationAutomate secret rotation where possible
Naming conventionsConsistent 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


Azure Integration Hub - Architect Level Security Architecture & Zero Trust