Azure Key Vault + Managed Identity — How I Eliminated Every Secret From Our Codebase
The right way to handle secrets in .NET Core applications on Azure — no API keys, no connection strings, no credentials in code
Azure Key Vault + Managed Identity — How I Eliminated Every Secret From Our Codebase
The right way to handle secrets in .NET Core applications on Azure — no API keys, no connection strings, no credentials in code
The Secret That Almost Made It to Production
During a routine code review, I spotted something that made my stomach drop.
A junior engineer had hardcoded an Azure OpenAI API key directly in appsettings.json:
json
{
"AzureOpenAI": {
"Endpoint": "https://myinstance.openai.azure.com/",
"ApiKey": "sk-abc123def456..."
}
}
The file was about to be committed to our Git repository. If it had been pushed — the key would have been in version control history forever. Even after deletion, it would be recoverable.
We caught it in time. But it triggered a team-wide initiative to make hardcoded secrets architecturally impossible — not just against our coding standards.
This article documents exactly how we did it using Azure Key Vault and Managed Identity.
Why Secrets in Code and Config Are Dangerous
Before the solution — let me be precise about the problem.
Secrets appear in codebases in three ways:
csharp
// ❌ Way 1 — Hardcoded in source code
var client = new OpenAIClient(
new Uri("https://myinstance.openai.azure.com/"),
new AzureKeyCredential("sk-abc123def456...")); // Never do this
// ❌ Way 2 — In appsettings.json (committed to Git)
// appsettings.json
{
"ConnectionStrings": {
"Database": "Server=prod;Password=SuperSecret123;"
}
}
// ❌ Way 3 — In environment variables (slightly better but still risky)
// Anyone with server access can read these
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
All three approaches share the same fundamental problem — the secret exists as a string somewhere that humans and processes can read. It can be logged accidentally, committed to version control, leaked in error messages, or exposed in a deployment artifact.
The solution is to never handle the secret as a string at all.
The Architecture — How It Works
Here is the architecture we use in production:
.NET Application (AKS Pod)
|
| Uses Managed Identity
| (no credentials needed)
↓
Azure Entra ID (Azure AD)
|
| Issues access token automatically
↓
Azure Key Vault
|
| Returns secret value
↓
.NET Application uses secret
The key insight is Managed Identity — an identity automatically assigned to your Azure resource (App Service, AKS pod, Azure Function) by Azure Entra ID. Your application proves its identity to Azure Key Vault using this managed identity — no username, no password, no API key required.
Step 1 — Set Up Azure Key Vault
First create a Key Vault and store your secrets:
bash
# Create Key Vault via Azure CLI
az keyvault create \
--name "kv-myapp-production" \
--resource-group "rg-myapp" \
--location "eastus" \
--sku standard
# Store secrets
az keyvault secret set \
--vault-name "kv-myapp-production" \
--name "Database--ConnectionString" \
--value "Server=prod.database.windows.net;Database=MyApp;Authentication=Active Directory Managed Identity;"
az keyvault secret set \
--vault-name "kv-myapp-production" \
--name "AzureOpenAI--ApiKey" \
--value "sk-abc123def456..."
az keyvault secret set \
--vault-name "kv-myapp-production" \
--name "AzureOpenAI--Endpoint" \
--value "https://myinstance.openai.azure.com/"
Note the double dash in secret names — Database--ConnectionString. Azure Key Vault does not support : in secret names, so the convention is to use -- which .NET automatically maps to : in IConfiguration.
Step 2 — Enable Managed Identity on Your Azure Resource
For Azure App Service:
bash
az webapp identity assign \
--name "myapp-api" \
--resource-group "rg-myapp"
# Returns the principal ID — save this
# {
# "principalId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
# "type": "SystemAssigned"
# }
For AKS (using Workload Identity):
yaml
# pod-identity.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp-service-account
namespace: production
annotations:
azure.workload.identity/client-id: "your-managed-identity-client-id"
Step 3 — Grant Access to Key Vault
bash
# Get your app's managed identity principal ID
PRINCIPAL_ID=$(az webapp identity show \
--name "myapp-api" \
--resource-group "rg-myapp" \
--query principalId \
--output tsv)
# Grant read access to Key Vault secrets
az keyvault set-policy \
--name "kv-myapp-production" \
--object-id $PRINCIPAL_ID \
--secret-permissions get list
Your application now has permission to read secrets from Key Vault — without any credentials.
Step 4 — Integrate Key Vault with .NET Core Configuration
This is where the magic happens. Add Key Vault as a configuration provider so secrets are available through the standard IConfiguration interface — exactly like appsettings.json values:
csharp
// Program.cs
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Azure.Extensions.AspNetCore.Configuration.Secrets;
var builder = WebApplication.CreateBuilder(args);
// Add Azure Key Vault as a configuration source
var keyVaultUri = builder.Configuration["KeyVault:Uri"];
if (!string.IsNullOrEmpty(keyVaultUri))
{
var credential = new DefaultAzureCredential();
builder.Configuration.AddAzureKeyVault(
new Uri(keyVaultUri),
credential);
}
// Now ALL Key Vault secrets are available via IConfiguration
// just like appsettings.json values
var app = builder.Build();
The KeyVault:Uri in appsettings.json is the only non-secret value you need:
json
// appsettings.json — safe to commit, contains NO secrets
{
"KeyVault": {
"Uri": "https://kv-myapp-production.vault.azure.net/"
}
}
Step 5 — How DefaultAzureCredential Works
DefaultAzureCredential is the key to making this work seamlessly across all environments:
csharp
// This single line handles authentication everywhere
var credential = new DefaultAzureCredential();
It tries authentication methods in this order:
1. EnvironmentCredential → CI/CD pipeline service principal
2. WorkloadIdentityCredential → AKS Workload Identity
3. ManagedIdentityCredential → App Service / Azure VM Managed Identity ✅ Production
4. SharedTokenCacheCredential → Visual Studio token
5. VisualStudioCredential → Visual Studio 2022 logged-in account
6. AzureCliCredential → 'az login' on developer machine ✅ Local dev
7. AzurePowerShellCredential → PowerShell Az module
8. InteractiveBrowserCredential → Browser login (last resort)
In production (App Service or AKS) — it uses Managed Identity automatically. In local development — it falls through to your az login credentials.
Zero code changes between environments. Zero secrets in code.
Step 6 — Using Secrets in Your Application
Once Key Vault is wired as a configuration source, secrets are consumed exactly like any other configuration value:
csharp
// Works for any service — secrets come from Key Vault transparently
// Option 1 — Inject IConfiguration directly
public class OpenAIService
{
private readonly OpenAIClient _client;
public OpenAIService(IConfiguration config)
{
var endpoint = config["AzureOpenAI:Endpoint"];
var apiKey = config["AzureOpenAI:ApiKey"];
_client = new OpenAIClient(
new Uri(endpoint),
new AzureKeyCredential(apiKey));
}
}
// Option 2 — Options pattern (recommended)
public class OpenAIOptions
{
public string Endpoint { get; set; }
public string ApiKey { get; set; }
}
// Program.cs
builder.Services.Configure<OpenAIOptions>(
builder.Configuration.GetSection("AzureOpenAI"));
// Service
public class OpenAIService
{
private readonly OpenAIClient _client;
public OpenAIService(IOptions<OpenAIOptions> options)
{
_client = new OpenAIClient(
new Uri(options.Value.Endpoint),
new AzureKeyCredential(options.Value.ApiKey));
}
}
// Option 3 — Direct Key Vault access (when you need more control)
public class SecretService
{
private readonly SecretClient _secretClient;
public SecretService(IConfiguration config)
{
var kvUri = config["KeyVault:Uri"];
_secretClient = new SecretClient(
new Uri(kvUri),
new DefaultAzureCredential());
}
public async Task<string> GetSecretAsync(string secretName)
{
var secret = await _secretClient.GetSecretAsync(secretName);
return secret.Value.Value;
}
}
Step 7 — Advanced — Secret Rotation Without Redeployment
One of the most powerful features of Key Vault integration is transparent secret rotation. When you update a secret in Key Vault, your application picks it up on the next configuration reload — no redeployment needed.
csharp
// Program.cs — enable periodic configuration reload
builder.Configuration.AddAzureKeyVault(
new Uri(keyVaultUri),
credential,
new AzureKeyVaultConfigurationOptions
{
// Reload secrets from Key Vault every hour
ReloadInterval = TimeSpan.FromHours(1)
});
This means rotating a database password or API key is a one-step operation — update the secret in Key Vault. Your application automatically picks up the new value within an hour.
The NuGet Packages You Need
xml
<!-- .csproj -->
<PackageReference Include="Azure.Identity" Version="1.10.4" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.5.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
Or via CLI:
bash
dotnet add package Azure.Identity
dotnet add package Azure.Security.KeyVault.Secrets
dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets
Local Development Setup
For local development, authenticate using the Azure CLI:
bash
# One-time setup on developer machine
az login
# Verify you are logged in as the right account
az account show
# Your DefaultAzureCredential will now use this identity
# Grant your personal Azure AD account access to Key Vault
az keyvault set-policy \
--name "kv-myapp-production" \
--upn "your-email@company.com" \
--secret-permissions get list
Now when you run the application locally — DefaultAzureCredential uses your az login session. No secrets on your laptop. No .env files. No secrets.json with real credentials.
The Complete Secure Pattern
Putting it all together — here is the complete pattern for a secret-free .NET Core application:
csharp
// Program.cs — complete setup
var builder = WebApplication.CreateBuilder(args);
// Step 1: Add Key Vault as configuration source
var keyVaultUri = builder.Configuration["KeyVault:Uri"];
builder.Configuration.AddAzureKeyVault(
new Uri(keyVaultUri),
new DefaultAzureCredential(),
new AzureKeyVaultConfigurationOptions
{
ReloadInterval = TimeSpan.FromHours(1)
});
// Step 2: Register services — secrets injected automatically
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection("Database"));
builder.Services.Configure<OpenAIOptions>(
builder.Configuration.GetSection("AzureOpenAI"));
// Step 3: Register OpenAI client as singleton using Managed Identity
// This is even better — uses Managed Identity directly,
// never touches the API key as a string
builder.Services.AddSingleton(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return new OpenAIClient(
new Uri(config["AzureOpenAI:Endpoint"]),
new DefaultAzureCredential()); // Uses Managed Identity directly
});
var app = builder.Build();
app.Run();
json
// appsettings.json — the ONLY config file you need, safe to commit
{
"KeyVault": {
"Uri": "https://kv-myapp-production.vault.azure.net/"
}
}
That is the entire configuration. No secrets. No connection strings. No API keys. Just a Key Vault URI — which is not a secret.
What This Looks Like in a Code Review
After implementing this pattern, here is what secret-related code looks like in our PRs:
csharp
// ✅ What we see in code reviews — no secrets anywhere
public class DatabaseService
{
private readonly string _connectionString;
public DatabaseService(IOptions<DatabaseOptions> options)
{
_connectionString = options.Value.ConnectionString;
// ConnectionString comes from Key Vault — not hardcoded
}
}
json
// ✅ What we see in appsettings.json — no secrets
{
"KeyVault": {
"Uri": "https://kv-myapp-production.vault.azure.net/"
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
If a reviewer ever sees a secret-shaped string in a PR — it is an immediate block. The architecture makes this kind of mistake obvious.
Results and Benefits
After implementing Key Vault and Managed Identity across all our services:
MetricBeforeAfterSecrets in source codeMultipleZeroSecrets in appsettings.jsonMultipleZeroSecret rotation requiring redeploymentYesNo — automaticDeveloper machines with production credentialsAllZeroSecurity audit findings on secret managementMultipleZero
The security posture improvement was significant. But the developer experience improvement was equally valuable — engineers no longer need to manage .env files, secrets.json, or ask someone for the production database password.
Getting Started
If your application currently has secrets in config files, here is your migration path:
Week 1: Create Key Vault, move one non-critical secret
Week 2: Install packages, wire up DefaultAzureCredential locally
Week 3: Deploy to staging — verify Managed Identity works
Week 4: Migrate all remaining secrets, remove from config files
Week 5: Add reloadInterval, rotate all secrets as validation
Start small. Move one secret. Verify the pattern works end-to-end. Then migrate the rest.
Final Thoughts
Secrets in source code is one of the most common and most preventable security vulnerabilities in enterprise software. It is not a sophisticated attack — it is just finding a string in a Git repository.
Azure Key Vault and Managed Identity eliminate this attack surface entirely. No secrets in code. No secrets in config. No secrets on developer laptops. Just a URI pointing to a vault that Azure’s identity platform manages on your behalf.
The setup takes a day. The protection is permanent.
Follow me for more articles on Azure cloud engineering, .NET Core development, and real-world enterprise security patterns.
메타데이터
- post_id
- f4e72e9c9e25
- slug
- azure-key-vault-managed-identity-how-i-eliminated-every-secret-from-our-codebase-f4e72e9c9e25
- url
- https://medium.com/@ethirajmurugan/azure-key-vault-managed-identity-how-i-eliminated-every-secret-from-our-codebase-f4e72e9c9e25
- canonical_url
- https://medium.com/@ethirajmurugan/azure-key-vault-managed-identity-how-i-eliminated-every-secret-from-our-codebase-f4e72e9c9e25
- author_url
- https://medium.com/@ethirajmurugan
- status
- ok
- fetched_at
- 2026-06-10 08:17:25