RO EN

Managed Identity — elimină secretele din configurație

Managed Identity — elimină secretele din configurație
Doru Bulubașa
22 iunie 2026
38 vizualizări

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:

  1. EnvironmentCredential — variabile de mediu (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID)
  2. WorkloadIdentityCredential — Kubernetes workload identity
  3. ManagedIdentityCredential — Managed Identity în Azure (App Service, Functions, Container Apps, VM)
  4. SharedTokenCacheCredential — token cache local
  5. VisualStudioCredential — contul logat în Visual Studio
  6. AzureCliCredential — contul logat prin az login
  7. AzurePowerShellCredential — contul logat în PowerShell
  8. 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.