Problema pe care o ignorăm cu toții
Deschide orice proiect .NET mai vechi de câțiva ani și, cu mare probabilitate, vei găsi ceva de genul acesta în appsettings.json sau în variabilele de mediu:
{
"CosmosDb": {
"Endpoint": "https://myaccount.documents.azure.com:443/",
"AccountKey": "dGhpcyBpcyBub3QgYSByZWFsIGtleQ=="
},
"Storage": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=mystorage;AccountKey=abc123..."
},
"OpenAI": {
"ApiKey": "sk-proj-..."
}
}
Problema nu e că funcționează. Funcționează perfect. Problema e tot ce vine la pachet cu această abordare:
- Rotația cheilor e manual și dureroasă — dacă un AccountKey expiră sau e compromis, trebuie să îl schimbi în toate locurile unde e stocat, să refaci deployul și să speri că nu ai uitat nicăieri
- Secretele ajung în Git — un
git add .neatent și credențialele sunt în istoricul repository-ului pentru totdeauna - Suprafața de atac e mare — oricine are acces la configurația aplicației (un coleg nou, un contractor, un log file care a scăpat) are acces la toate resursele
- Audit imposibil — nu poți ști cine a folosit cheia și când; toate operațiile apar sub același identity
Există o alternativă mai bună, nativă Azure, care elimină toate aceste probleme: Managed Identity.
Ce este Managed Identity
Managed Identity e un mecanism prin care Azure atribuie o identitate (un service principal în Azure Active Directory) direct resursei tale — App Service, Azure Functions, Container Apps, VM etc. Această identitate poate primi roluri RBAC pe alte resurse Azure, exact ca orice utilizator sau aplicație.
Diferența față de un service principal clasic: nu gestionezi nicio credențială. Nu există client secret, nu există certificat, nu există cheie de rotit. Azure gestionează intern token-urile de autentificare. Aplicația ta cere un token, Azure îl furnizează, resursele îl acceptă.
| Abordare | Stocare secret | Rotație | Audit per-identitate |
|---|---|---|---|
| Connection string / API key | appsettings, env vars, Key Vault | Manuală, disruptivă | Nu |
| Service principal cu secret | Key Vault (client secret) | Manuală, periodică | Da, dar shared |
| Managed Identity | Niciunde | Automată, Azure o face | Da, per resursă |
Există două variante:
- System-assigned — identitatea e legată de ciclul de viață al resursei; se șterge automat când ștergi App Service-ul. Ideal pentru resurse cu un singur scop.
- User-assigned — identitatea e o resursă separată, poate fi atribuită mai multor resurse Azure. Ideal când mai multe App Service-uri sau Functions trebuie să acceseze aceleași resurse.
Activarea Managed Identity
Azure Portal
App Service → Identity → System assigned → Status: On → Save. Atât. Azure creează automat un service principal cu același nume ca App Service-ul tău.
Azure CLI
# System-assigned identity pentru App Service
az webapp identity assign \
--name my-app-service \
--resource-group my-rg
# Rezultat -- retine principalId pentru pasul urmator
# {
# "principalId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
# "tenantId": "yyyyyyyy-...
# }
# System-assigned pentru Azure Functions
az functionapp identity assign \
--name my-function-app \
--resource-group my-rg
Bicep / IaC
resource appService "Microsoft.Web/sites@2023-01-01" = {
name: appServiceName
location: location
identity: {
type: "SystemAssigned" // activeaza managed identity
}
properties: {
serverFarmId: appServicePlan.id
// ...
}
}
DefaultAzureCredential — un singur cod pentru toate mediile
Pachetul Azure.Identity oferă DefaultAzureCredential, care încearcă mai multe metode de autentificare în ordine, alegând prima care funcționează:
dotnet add package Azure.Identity
Lanțul de autentificare în ordine:
- EnvironmentCredential — variabile de mediu (
AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_TENANT_ID) - WorkloadIdentityCredential — Kubernetes workload identity
- ManagedIdentityCredential — Managed Identity în Azure (App Service, Functions, Container Apps, VM)
- SharedTokenCacheCredential — token cache local
- VisualStudioCredential — contul logat în Visual Studio
- AzureCliCredential — contul logat prin
az login - AzurePowerShellCredential — contul logat în PowerShell
- InteractiveBrowserCredential — browser (dezactivat implicit)
Practic: pe mașina ta de development, se autentifică prin az login sau Visual Studio. În Azure, folosește automat Managed Identity. Același cod funcționează în ambele medii, fără nicio modificare.
// Un singur rând -- functioneaza local (az login) si in Azure (Managed Identity)
var credential = new DefaultAzureCredential();
Dacă vrei control mai fin și vrei să eviți timeout-urile la local (sărit peste metode lente), poți configura explicit:
// Local: incearca doar az login si Visual Studio
// In Azure: incearca doar Managed Identity
var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
{
ExcludeEnvironmentCredential = true,
ExcludeWorkloadIdentityCredential = true,
ExcludeSharedTokenCacheCredential = true,
ExcludeInteractiveBrowserCredential = true
});
Acces la Cosmos DB fără AccountKey
Pasul 1: atribuie rolul RBAC pe Cosmos DB
Cosmos DB are roluri built-in pentru data plane: Cosmos DB Built-in Data Reader și Cosmos DB Built-in Data Contributor. Atribuie rolul potrivit pe identity-ul aplicației tale:
# Obtine principalId al App Service-ului
PRINCIPAL_ID=$(az webapp identity show \
--name my-app-service \
--resource-group my-rg \
--query principalId -o tsv)
COSMOS_ACCOUNT_ID=$(az cosmosdb show \
--name my-cosmos-account \
--resource-group my-rg \
--query id -o tsv)
# Atribuie Cosmos DB Built-in Data Contributor
az cosmosdb sql role assignment create \
--account-name my-cosmos-account \
--resource-group my-rg \
--role-definition-name "Cosmos DB Built-in Data Contributor" \
--principal-id $PRINCIPAL_ID \
--scope $COSMOS_ACCOUNT_ID
Pasul 2: configurează CosmosClient fără cheie
// Program.cs -- fara AccountKey, fara connection string
builder.Services.AddSingleton(sp =>
{
var endpoint = builder.Configuration["CosmosDb:Endpoint"]
?? throw new InvalidOperationException("CosmosDb:Endpoint nu e configurat");
return new CosmosClient(
accountEndpoint: endpoint,
tokenCredential: new DefaultAzureCredential(),
clientOptions: new CosmosClientOptions
{
SerializerOptions = new CosmosSerializationOptions
{
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
}
}
);
});
În appsettings.json rămâne doar endpoint-ul — care nu e un secret:
{
"CosmosDb": {
"Endpoint": "https://my-cosmos-account.documents.azure.com:443/",
"DatabaseName": "MyAppDb"
}
}
AccountKey-ul a dispărut complet din configurație.
Acces la Blob Storage fără connection string
Pasul 1: atribuie rolul RBAC pe Storage Account
STORAGE_ID=$(az storage account show \
--name mystorageaccount \
--resource-group my-rg \
--query id -o tsv)
# Storage Blob Data Contributor -- read + write
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Storage Blob Data Contributor" \
--scope $STORAGE_ID
# Sau Storage Blob Data Reader -- doar read
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Storage Blob Data Reader" \
--scope $STORAGE_ID
Pasul 2: configurează BlobServiceClient fără connection string
// Program.cs
builder.Services.AddSingleton(sp =>
{
var uri = new Uri(builder.Configuration["Storage:ServiceUri"]
?? throw new InvalidOperationException("Storage:ServiceUri nu e configurat"));
return new BlobServiceClient(uri, new DefaultAzureCredential());
});
// appsettings.json -- doar URI-ul, niciun secret
// "Storage": { "ServiceUri": "https://mystorageaccount.blob.core.windows.net" }
Dacă lucrezi cu containere individuale:
public class BlobRepository
{
private readonly BlobContainerClient _container;
public BlobRepository(IConfiguration config)
{
var containerUri = new Uri(
$"{config["Storage:ServiceUri"]}/{config["Storage:ContainerName"]}");
_container = new BlobContainerClient(containerUri, new DefaultAzureCredential());
}
public async Task UploadAsync(string blobName, Stream content)
{
await _container.UploadBlobAsync(blobName, content);
}
public async Task<Stream> DownloadAsync(string blobName)
{
var blob = _container.GetBlobClient(blobName);
var response = await blob.DownloadStreamingAsync();
return response.Value.Content;
}
}
Acces la Key Vault — secretele rămase
Există situații în care trebuie să stochezi un secret: o cheie API pentru un serviciu terț (Stripe, SendGrid, un SMS gateway) care nu suportă Managed Identity. Pentru acestea, Key Vault e răspunsul corect.
Pasul 1: atribuie rolul pe Key Vault
KV_ID=$(az keyvault show \
--name my-keyvault \
--resource-group my-rg \
--query id -o tsv)
# Key Vault Secrets User -- read only
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Key Vault Secrets User" \
--scope $KV_ID
Pasul 2: integrare cu sistemul de configurare ASP.NET Core
// Program.cs -- Key Vault ca provider de configurare
if (builder.Environment.IsProduction())
{
var kvUri = builder.Configuration["KeyVaultUri"]
?? throw new InvalidOperationException("KeyVaultUri nu e configurat");
builder.Configuration.AddAzureKeyVault(
new Uri(kvUri),
new DefaultAzureCredential());
}
// Dupa aceasta linie, secretele din Key Vault sunt accesibile
// exact ca orice alta valoare de configurare:
// builder.Configuration["Stripe--ApiKey"] (-- = : in Key Vault naming)
// builder.Configuration["SendGrid--ApiKey"]
Convenția de naming în Key Vault: folosești -- (double dash) în loc de : sau __, deoarece Key Vault nu acceptă : în numele secretelor. Deci SendGrid--ApiKey în Key Vault devine SendGrid:ApiKey în configurarea ASP.NET Core.
Acces direct la secrete (fără provider de configurare)
// Util cand ai nevoie de un secret in cod, nu in configurare
public class StripeService
{
private readonly SecretClient _kvClient;
public StripeService(IConfiguration config)
{
var kvUri = config["KeyVaultUri"]
?? throw new InvalidOperationException("KeyVaultUri nu e configurat");
_kvClient = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
}
public async Task<string> GetApiKeyAsync()
{
var secret = await _kvClient.GetSecretAsync("Stripe--ApiKey");
return secret.Value.Value;
}
}
Managed Identity în Azure Functions
Azure Functions Isolated Worker funcționează identic cu App Service din perspectiva Managed Identity. Activezi identity, atribui roluri, folosești DefaultAzureCredential.
// Program.cs (Azure Functions Isolated Worker)
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices((context, services) =>
{
// Key Vault in productie
if (context.HostingEnvironment.IsProduction())
{
var kvUri = context.Configuration["KeyVaultUri"];
if (!string.IsNullOrEmpty(kvUri))
{
// Adauga Key Vault ca sursa de configurare
// Necesita: builder.Configuration la nivel de HostBuilder
}
}
// Cosmos DB fara cheie
var cosmosEndpoint = context.Configuration["CosmosDb:Endpoint"]!;
services.AddSingleton(_ => new CosmosClient(
cosmosEndpoint, new DefaultAzureCredential()));
// Blob Storage fara connection string
var storageUri = context.Configuration["Storage:ServiceUri"]!;
services.AddSingleton(_ => new BlobServiceClient(
new Uri(storageUri), new DefaultAzureCredential()));
})
.Build();
await host.RunAsync();
O diferență importantă pentru Azure Functions: variabilele de configurare se setează în Application Settings din portal, nu în local.settings.json (care e doar pentru development local și nu se comite în Git).
// local.settings.json -- DOAR pentru development local, nu se comite in Git
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"CosmosDb:Endpoint": "https://localhost:8081/"
}
}
Development local cu az login
Când rulezi local, DefaultAzureCredential va folosi contul tău Azure logat prin Azure CLI sau Visual Studio. Nu trebuie să pui nicio cheie în fișiere locale.
# O singura data, pe masina de development
az login
az account set --subscription "My Subscription"
De acum, orice aplicație care folosește DefaultAzureCredential se va autentifica cu contul tău. Asigură-te că și tu ai rolurile necesare pe resursele respective (Cosmos DB Built-in Data Contributor, Storage Blob Data Contributor etc.).
Dacă lucrezi cu mai mulți membri în echipă, fiecare primește rolurile necesare individual. Nu mai există un secret partajat pe care toată lumea îl știe.
# Atribuie rolurile si pentru developeri
DEV_OBJECT_ID=$(az ad user show --id developer@company.com --query id -o tsv)
az cosmosdb sql role assignment create \
--account-name my-cosmos-account \
--resource-group my-rg \
--role-definition-name "Cosmos DB Built-in Data Contributor" \
--principal-id $DEV_OBJECT_ID \
--scope $COSMOS_ACCOUNT_ID
User-assigned Managed Identity — când e mai bun
System-assigned e simplu, dar are o limitare: dacă ștergi App Service-ul și îl recreezi (lucru frecvent în IaC), identity-ul se schimbă și trebuie să re-atribui toate rolurile RBAC.
User-assigned Managed Identity e o resursă separată cu ciclu de viață independent:
# Creeaza user-assigned identity
az identity create \
--name my-app-identity \
--resource-group my-rg
IDENTITY_ID=$(az identity show \
--name my-app-identity \
--resource-group my-rg \
--query id -o tsv)
PRINCIPAL_ID=$(az identity show \
--name my-app-identity \
--resource-group my-rg \
--query principalId -o tsv)
# Ataseaza identity la App Service
az webapp identity assign \
--name my-app-service \
--resource-group my-rg \
--identities $IDENTITY_ID
// Cand folosesti user-assigned, specifica clientId
// (gasit in portal sau via az identity show --query clientId)
var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
{
ManagedIdentityClientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
});
// Sau mai explicit:
var credential = new ManagedIdentityCredential(
clientId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
Folosește user-assigned când: mai multe resurse Azure au nevoie de același set de permisiuni, recreezi frecvent resursele prin IaC, sau vrei să pre-configurezi permisiunile înainte de deployment.
Configurație completă fără niciun secret
Să recapitulăm cum arată un appsettings.json curat, fără niciun secret:
{
"Logging": {
"LogLevel": { "Default": "Information" }
},
"KeyVaultUri": "https://my-keyvault.vault.azure.net/",
"CosmosDb": {
"Endpoint": "https://my-cosmos.documents.azure.com:443/",
"DatabaseName": "MyAppDb"
},
"Storage": {
"ServiceUri": "https://mystorage.blob.core.windows.net",
"ContainerName": "uploads"
}
}
// Niciun AccountKey, niciun ConnectionString, niciun ApiKey.
// Toate acestea sunt URI-uri publice -- nu sunt secrete.
Și Program.cs complet cu toate cele trei resurse:
var builder = WebApplication.CreateBuilder(args);
// Key Vault in productie
if (builder.Environment.IsProduction())
{
var kvUri = builder.Configuration["KeyVaultUri"]!;
builder.Configuration.AddAzureKeyVault(new Uri(kvUri), new DefaultAzureCredential());
}
var credential = new DefaultAzureCredential();
// Cosmos DB
builder.Services.AddSingleton(_ => new CosmosClient(
builder.Configuration["CosmosDb:Endpoint"]!,
credential,
new CosmosClientOptions
{
SerializerOptions = new CosmosSerializationOptions
{
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
}
}));
// Blob Storage
builder.Services.AddSingleton(_ => new BlobServiceClient(
new Uri(builder.Configuration["Storage:ServiceUri"]!),
credential));
// Key Vault client (pentru acces direct la secrete in cod)
builder.Services.AddSingleton(_ => new SecretClient(
new Uri(builder.Configuration["KeyVaultUri"]!),
credential));
// Restul configurarii aplicatiei...
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
await app.RunAsync();
Checklist Managed Identity
- Managed Identity activată — System-assigned sau User-assigned pe App Service / Functions
- Roluri RBAC atribuite — Cosmos DB Built-in Data Contributor, Storage Blob Data Contributor, Key Vault Secrets User
- CosmosClient instanțiat cu DefaultAzureCredential — fără AccountKey în configurare
- BlobServiceClient instanțiat cu DefaultAzureCredential — fără connection string în configurare
- Key Vault pentru secretele rămase — chei API terțe, integrat ca provider de configurare
- Development local prin az login — developerii primesc roluri individuale, nu un secret partajat
- Niciun secret în Git — appsettings.json conține doar endpoint-uri, nu credențiale
Dacă ai întrebări sau vrei să discuți cum aplici Managed Identity în proiectul tău, scrie-mi la contact@ludoprogramming.com.