Stackvora.tech

Azure Key Vault + Managed Identity: secrets without secrets

Modern application deployments often require access to sensitive information such as database connection strings, API keys, and encryption certificates.…

Modern application deployments often require access to sensitive information such as database connection strings, API keys, and encryption certificates. Securely managing these secrets, especially without embedding them directly into application code or configuration files, is a critical security and operational challenge. Azure Key Vault provides a robust solution for storing and managing secrets, while Azure Managed Identities offer a secure, automated way for Azure services to authenticate to other Azure resources, eliminating the need for developers to manage credentials directly.

This article details the integration of Azure Key Vault with Managed Identities, illustrating how to establish a secure and credential-free secret retrieval mechanism for your Azure applications. We will cover the setup process, granting appropriate permissions using Azure Role-Based Access Control (RBAC), and demonstrate secret access from both Node.js and .NET applications, including considerations for local development and common troubleshooting steps.

Understanding Managed Identities

Managed Identities for Azure resources provide an automatically managed identity in Azure Active Directory (Azure AD) for applications to use when connecting to resources that support Azure AD authentication. This eliminates the need for developers to manage credentials by providing an identity for the Azure service itself.

  • System-assigned managed identity: Tied directly to the lifecycle of a specific Azure resource (e.g., a Virtual Machine, Azure App Service). When the resource is deleted, the identity is automatically deleted.
  • User-assigned managed identity: A standalone Azure resource that can be assigned to multiple Azure resources. Its lifecycle is independent of the resources it's assigned to. This is often preferred for scenarios where a single identity needs to be shared across several applications or resources.

The core benefit is that Azure handles the creation, rotation, and distribution of the identity's credentials, which are never exposed to the developer or application code.

Setting Up Azure Key Vault

First, ensure you have an Azure Key Vault instance. If not, you can create one using the Azure CLI:

KV_RESOURCE_GROUP="rg-prod-kv"
KV_NAME="acme-kv-prod"
LOCATION="eastus" # Choose an appropriate Azure region

az group create --name $KV_RESOURCE_GROUP --location $LOCATION
az keyvault create \
  --name $KV_NAME \
  --resource-group $KV_RESOURCE_GROUP \
  --location $LOCATION \
  --sku Standard \
  --enabled-for-rbac-authorization true

Note the --enabled-for-rbac-authorization true flag. This is crucial for using Azure RBAC to manage permissions, which is the recommended access model since 2023. If your existing Key Vault uses legacy access policies, it's strongly advised to migrate to RBAC for enhanced security and simplified management. Azure provides tools and documentation for this migration.

Once created, you can store your secrets:

az keyvault secret set \
  --vault-name $KV_NAME \
  --name "db-connection-string" \
  --value "Server=tcp:acmedb.database.windows.net,1433;Database=acmedb;Uid=acmeuser;Pwd=SuperStrongPa55word;"

Assigning a Managed Identity to Your Azure Resource

The next step is to assign a managed identity to your Azure compute resource, such as an Azure App Service, Azure Function, or Virtual Machine. This example demonstrates a system-assigned identity for an App Service:

APP_RESOURCE_GROUP="rg-prod-webapp"
APP_NAME="acme-api-prod"

# Create or target your App Service
az webapp create \
  --name $APP_NAME \
  --resource-group $APP_RESOURCE_GROUP \
  --plan "myAppServicePlan" \
  --runtime "NODE:18-LTS" # Example runtime

# Assign a system-assigned managed identity to the App Service
az webapp identity assign \
  --resource-group $APP_RESOURCE_GROUP \
  --name $APP_NAME

For a Virtual Machine, the command would be:

VM_RESOURCE_GROUP="rg-prod-vm"
VM_NAME="acme-vm-prod"

# Assign a system-assigned managed identity to the VM
az vm identity assign \
  --resource-group $VM_RESOURCE_GROUP \
  --name $VM_NAME

After assigning, it might take a minute or two for the identity to propagate through Azure AD. You can retrieve its Principal ID, which is needed for RBAC assignments:

PRINCIPAL_ID=$(az webapp identity show \
  --resource-group $APP_RESOURCE_GROUP \
  --name $APP_NAME \
  --query principalId \
  --output tsv)

echo "Managed Identity Principal ID: $PRINCIPAL_ID"

Granting Key Vault Access using RBAC

With the Managed Identity created and its Principal ID retrieved, you can now grant it specific permissions to your Key Vault using Azure RBAC. The most common role for secret retrieval is "Key Vault Secrets User".

# Retrieve the Key Vault resource ID
KV_RESOURCE_ID=$(az keyvault show \
  --name $KV_NAME \
  --resource-group $KV_RESOURCE_GROUP \
  --query id \
  --output tsv)

# Grant "Key Vault Secrets User" role to the Managed Identity
az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee-object-id $PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --scope $KV_RESOURCE_ID

This command assigns the "Key Vault Secrets User" role at the Key Vault resource level, allowing the managed identity to read any secret within that vault. For more granular control, you could assign roles at a secret-specific scope, though this is less common for typical application use cases.

The --assignee-principal-type ServicePrincipal argument is crucial here, as Managed Identities are represented as Service Principals in Azure AD.

Accessing Secrets from Applications

Azure SDKs provide a unified authentication library, Azure.Identity (for .NET) or @azure/identity (for Node.js), which simplifies credential handling. The DefaultAzureCredential class is designed to handle multiple authentication methods automatically, including Managed Identities, making it ideal for both cloud deployment and local development.

Node.js Application

First, install the necessary packages:

npm install @azure/identity @azure/keyvault-secrets

Then, in your application code:

import { DefaultAzureCredential } from "@azure/identity";
import { SecretClient } from "@azure/keyvault-secrets";
import * as dotenv from 'dotenv';

dotenv.config(); // Load environment variables from .env for local testing

async function getSecrets() {
  const keyVaultName = process.env.KEY_VAULT_NAME || "acme-kv-prod";
  const keyVaultUri = `https://${keyVaultName}.vault.azure.net`;

  // DefaultAzureCredential automatically handles authentication for various environments:
  // - Managed Identity in Azure environments (App Service, VM, etc.)
  // - Azure CLI login credentials during local development
  // - Environment variables (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET)
  // - Visual Studio Code (if signed in)
  const credential = new DefaultAzureCredential();

  const client = new SecretClient(keyVaultUri, credential);

  try {
    const dbConnectionString = (await client.getSecret("db-connection-string")).value;
    console.log("Database Connection String:", dbConnectionString);

    const apiKey = (await client.getSecret("external-api-key")).value;
    console.log("External API Key:", apiKey);

    // Use these secrets to configure your application
    // For example, establish a database connection
  } catch (error) {
    console.error("Error retrieving secret:", error);
    // Depending on the error, you might want to retry or crash gracefully
    if (error.statusCode === 403) {
      console.error("Permission denied. Ensure the Managed Identity has 'Key Vault Secrets User' role.");
    }
  }
}

getSecrets();

.NET Application

Add the required NuGet packages to your project:

dotnet add package Azure.Identity
dotnet add package Azure.Security.KeyVault.Secrets

Then, in your C# code (e.g., in Program.cs or a service constructor):

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration; // For local config management

public class SecretReader
{
    private readonly SecretClient _secretClient;

    public SecretReader(IConfiguration configuration)
    {
        // For local development, load KEY_VAULT_NAME from appsettings.json or environment variables
        // In Azure, it might be an environment variable or directly known.
        string keyVaultName = configuration["KeyVaultName"] ?? Environment.GetEnvironmentVariable("KEY_VAULT_NAME") ?? "acme-kv-prod";
        string keyVaultUri = $"https://{keyVaultName}.vault.azure.net/";

        // DefaultAzureCredential provides a chain of authentication methods:
        // - Managed Identity (system or user-assigned) in Azure environments
        // - Environment variables (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET)
        // - Visual Studio/Visual Studio Code credentials
        // - Azure CLI credentials
        var credential = new DefaultAzureCredential();
        _secretClient = new SecretClient(new Uri(keyVaultUri), credential);
    }

    public async Task GetAndUseSecrets()
    {
        try
        {
            KeyVaultSecret dbConnectionStringSecret = await _secretClient.GetSecretAsync("db-connection-string");
            string dbConnectionString = dbConnectionStringSecret.Value;
            Console.WriteLine($"Database Connection String: {dbConnectionString}");

            KeyVaultSecret apiKeySecret = await _secretClient.GetSecretAsync("external-api-key");
            string apiKey = apiKeySecret.Value;
            Console.WriteLine($"External API Key: {apiKey}");

            // Your application logic here, e.g., configuring services:
            // services.AddDbContext(options => options.UseSqlServer(dbConnectionString));
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error retrieving secret: {ex.Message}");
            // Log the full exception and potentially handle specific Azure.RequestFailedException errors
            if (ex is Azure.RequestFailedException rfe && rfe.Status == 403)
            {
                Console.WriteLine("Permission denied. Check if the Managed Identity has 'Key Vault Secrets User' role on the Key Vault.");
            }
        }
    }
}

// Example usage in Program.cs (minimal API or console app)
/*
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables(); // Crucial for Azure environments and local .env
var app = builder.Build();

var secretReader = new SecretReader(app.Configuration);
await secretReader.GetAndUseSecrets();

app.Run();
*/

Local Development Considerations

DefaultAzureCredential is designed to work seamlessly during local development. When running your application locally, it attempts to authenticate in the following order (among others):

  1. Environment Variables: Checks for AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET (or AZURE_CLIENT_CERTIFICATE_PATH and AZURE_CLIENT_CERTIFICATE_PASSWORD) for service principal authentication.
  2. Azure CLI: If you're logged in via az login, it uses your local Azure CLI credentials.
  3. Azure PowerShell: If logged in via Connect-AzAccount.
  4. Visual Studio / Visual Studio Code: If you're signed in to your Azure account through these IDEs.

The simplest approach for local testing is to perform az login in your terminal. DefaultAzureCredential will automatically pick up these credentials, allowing your local application to access Key Vault secrets, assuming your logged-in Azure user account has the "Key Vault Secrets User" role assigned to the target Key Vault.

az login

To grant your local user access to the Key Vault:

# Replace YOUR_USER_EMAIL with your Azure login email
YOUR_USER_OBJECT_ID=$(az ad user show --id YOUR_USER_EMAIL --query id --output tsv)

az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee-object-id $YOUR_USER_OBJECT_ID \
  --scope $KV_RESOURCE_ID

Common Pitfalls and Troubleshooting

  • AADSTS700016: Application not found: This error often indicates that a newly assigned managed identity hasn't fully propagated in Azure AD. This can take anywhere from 30 seconds to a few minutes. Retrying after a short wait typically resolves it.
  • 403 Forbidden from Key Vault: This is a classic permissions issue.
    • Missing RBAC role: Double-check that the "Key Vault Secrets User" role is assigned to the managed identity's Principal ID.
    • Wrong scope: Ensure the role assignment is on the correct Key Vault resource (or a more specific scope if intended). The --scope parameter in az role assignment create is critical.
    • Legacy Access Policies: If your Key Vault is still using access policies, the RBAC role won't apply. You'd need to grant "Get" permission for secrets directly through Key Vault access policies, or preferably, migrate to the RBAC model.
    • Firewall or VNet rules: If your Key Vault has network access restrictions, ensure your application's source IP or VNet is allowed. Managed Identities use Microsoft's internal network, but if your application is egressing through a specific VNet, that VNet must be granted access.
  • Working locally, failing in Azure: Your local az login account likely has more permissions than your managed identity. This mismatch confirms a permissions issue with the managed identity. Explicitly grant the managed identity the necessary "Key Vault Secrets User" role.
  • Incorrect Key Vault URI: Ensure the URI used to instantiate SecretClient is correct, e.g., https://.vault.azure.net/.

Back to the knowledge base · Ask the AI assistant