Logo
  • English icon
  • German icon

Azure OpenAI ohne API-Keys in .NET — Managed Identity auf Microsoft Foundry

azure-openaimanaged-identityentra-iddotnetterraformmicrosoft-foundry

Einen API-Key in die Config zu kopieren ist der einfache Weg, Azure OpenAI aufzurufen — und der schnellste Weg, durch ein Enterprise-Security-Review zu fallen. Ein Key ist ein langlebiges Geheimnis: Er landet in Config-Dateien, in Pipelines, manchmal in der Git-Historie, und jeder Request mit dem Key sieht gleich aus — bei Audits lässt sich nicht sagen, welche App oder welche Person den Aufruf gemacht hat. In regulierten Branchen ist diese Kombination ein No-Go.

Dieser Walkthrough zeigt stattdessen das Muster, das Reviews besteht: schlüssellose Authentifizierung mit Managed Identity und DefaultAzureCredential — derselbe Code lokal und in Azure, und der schlüsselbasierte Zugriff auf die Ressource ist komplett deaktiviert. Er richtet sich an .NET-Entwickler, die nach Azure deployen und deren AI-Integration den Kontakt mit einem Security-Team überleben soll.

Was sich 2026 geändert hat

Wer Azure OpenAI zuletzt vor ein, zwei Jahren angefasst hat, findet vier Dinge anders vor:

  • Microsoft Foundry ersetzt die eigenständige Azure-OpenAI-Ressource als empfohlenen Ressourcentyp (Provider Microsoft.CognitiveServices, Kind AIServices). Für Entra-ID-Authentifizierung ist eine Custom Subdomain erforderlich.
  • Die RBAC-Rolle für Inferenz heißt „Foundry User" — kürzlich umbenannt von „Azure AI User", gleiche Rollen-ID. Microsoft rät für Foundry-Szenarien von den älteren Cognitive Services *-Rollen ab.
  • Das offizielle OpenAI .NET SDK gegen den OpenAI-v1-Endpunkt (https://<resource>.openai.azure.com/openai/v1) ersetzt die veralteten Azure-spezifischen Inferenz-SDKs. Kein api-version-Jonglieren mehr.
  • Der Token-Scope für Foundry ist https://ai.azure.com/.default — die Brücke zwischen Azure.Identity und dem OpenAI SDK ist BearerTokenPolicy.

Wie es funktioniert

Die API läuft auf App Service mit einer system-assigned Managed Identity — quasi ein Service-Konto, das Azure verwaltet: kein Passwort, kein Key. Wenn die App das Modell aufrufen will, holt sie sich ein Token von Entra ID, und Entra ID prüft, ob diese Identität die Rolle Foundry User auf der Ressource hat. Diese Rolle erlaubt das Aufrufen der Modelle und sonst nichts.

DefaultAzureCredential sorgt dafür, dass derselbe Code überall funktioniert. Es ist nicht ein einzelnes Credential, sondern eine Kette: Der Code probiert verschiedene Credential-Quellen durch und nimmt die erste, die funktioniert. Auf dem Laptop gibt es keine Managed Identity, also wird das Azure-CLI-Login verwendet; auf App Service findet er die Managed Identity. Kein If-Statement, keine Sonder-Config — was auch bedeutet, dass der eigene Benutzer ebenfalls die Foundry-User-Rolle braucht, damit die lokale Entwicklung funktioniert.

Der schlüssellose Code

Die ganze API ist eine .NET-10-Minimal-API mit einem Endpunkt. Der schlüssellose Teil sind etwa zehn Zeilen — der Umstieg ist kein großer Rewrite:

// DefaultAzureCredential probiert eine Kette von Credential-Quellen:
// lokal greift es auf das Azure-CLI-Login zu (az login),
// auf Azure App Service nutzt es die system-assigned Managed Identity.
var credential = new DefaultAzureCredential();
// "https://ai.azure.com/.default" ist der Token-Scope
// für Microsoft-Foundry-Ressourcen.
var tokenPolicy = new BearerTokenPolicy(credential, "https://ai.azure.com/.default");
return new ChatClient(
model: deployment,
authenticationPolicy: tokenPolicy,
options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) });
ℹ️Wenn die Authentifizierung trotz gültiger Identität fehlschlägt

Zuerst den Token-Scope prüfen: Für Foundry-Ressourcen muss er https://ai.azure.com/.default lauten. Die BearerTokenPolicy-Konstruktor-Überladung ist noch als experimentell markiert (OPENAI001), daher braucht es ein Pragma, um die Warnung zu unterdrücken.

Die Config enthält nur den Endpunkt und den Deployment-Namen — keine Geheimnisse. Der echte Endpunkt liegt in einer gitignorierten appsettings.local.json, weshalb das Repo öffentlich sein kann.

Die Infrastruktur in Terraform

In echten Projekten klickt man das nicht im Portal zusammen. Drei Details im Terraform sind am wichtigsten:

resource "azurerm_cognitive_account" "foundry" {
kind = "AIServices"
sku_name = "S0"
# Für Entra-ID-Authentifizierung ist eine Custom Subdomain erforderlich.
custom_subdomain_name = "foundry-${var.base_name}-${random_string.suffix.result}"
# Keys sind nicht nur ungenutzt - sie sind deaktiviert.
# Jeder Request muss ein Entra-ID-Token tragen.
local_auth_enabled = false
}
  1. Die Custom Subdomain ist Pflicht. Ohne sie funktioniert die Entra-ID-Authentifizierung nicht — und die Fehlermeldung verrät nicht, warum.
  2. local_auth_enabled = false deaktiviert den schlüsselbasierten Zugriff komplett. Wenn das Security-Review fragt, wie die Keys rotiert werden, lautet die Antwort: Es gibt keine Keys.
  3. Die Rollenzuweisungen sind minimal. Die Managed Identity der Web-App bekommt die Foundry-User-Rolle, beschränkt auf diese eine Ressource — nicht die Resource Group, nicht die Subscription. Der eigene Benutzer bekommt dieselbe Rolle für die lokale Entwicklung. Das Terraform referenziert die Rolle per GUID (53ca6127-db72-4b80-b1b0-d745d6d5456d), sodass die Umbenennung „Azure AI User" → „Foundry User" nichts kaputt machen kann.

Modelle werden nach Zeitplan abgekündigt — vor dem Festlegen einer Version prüfen, was die Region anbietet:

az cognitiveservices model list --location swedencentral

Lokal ausführen, dann deployen

az login
cd infra
cp terraform.tfvars.example terraform.tfvars # Subscription + eigene Object-ID eintragen
terraform init
terraform apply

Die App über eine gitignorierte appsettings.local.json auf den openai_v1_endpoint-Output zeigen lassen, dann:

cd src/ChatApi
dotnet run
curl -X POST http://localhost:5002/chat \
-H "Content-Type: application/json" \
-d '{ "message": "Hello, who are you?" }'

Kein Key auf dem Rechner — DefaultAzureCredential hat das az login aufgegriffen, und die Foundry-User-Rolle hat den Rest erledigt. Das Deployment ist ein dotnet publish und ein az webapp deploy; die genauen Befehle stehen im README des Repos.

Was ein Security-Review am Ende tatsächlich anschaut, ist das Azure-Portal: Der App Service zeigt eine system-assigned Identity, für die es kein Passwort gibt, das leaken könnte; diese Identität hat genau eine Rolle auf genau einer Ressource — der gesamte Blast-Radius, falls die App je kompromittiert wird; und die Keys-Seite der Ressource ist deaktiviert. Nichts zu stehlen.

⚠️401 direkt nach terraform apply ist normal

Rollenzuweisungen brauchen ein paar Minuten, bis sie greifen. Warten und erneut versuchen — vor Ablauf von ca. 5 Minuten nicht mit dem Debuggen anfangen.

Was Sie brauchen

In einem echten Enterprise-Setup kommt noch mehr dazu — user-assigned Identities, Sicherheitsgruppen und ein API-Management-Gateway vor den Modellen — aber das hier gezeigte schlüssellose Muster ist das Fundament, auf dem all das aufbaut.

Video-Transkript (Englisch)

This is the easy way to call Azure OpenAI: you copy the API key, you put it in the config, and it works. But if you bring this to a real company, the security review will reject it. This line is the problem. I build software for enterprises in Germany — energy sector, regulated industry — and there, API keys in config files are not accepted. In this video, I show you the pattern that is accepted: keyless authentication with Managed Identity. Same code locally and in Azure, no key anywhere. At the end, you will have a working setup: a .NET API, the infrastructure in Terraform, and the proof in the Azure portal. Everything is in a public repo, link in the description.

First, let's talk about why keys are a problem. There are three points. First, a key is a secret. It lands in config files, in pipelines, sometimes in Git history. If it leaks, anyone can use it. Second, there is no traceability. Every request with the key looks the same — you cannot say which app or which person made the call. For audits, that's a big problem. Third, someone has to rotate the key. Extra work, and things break when you do it. With Managed Identity, there is no secret. Every caller has an identity, and Azure manages everything.

Let me show you how it works. Here is the whole idea: our API runs on App Service with a system-assigned Managed Identity. Think of it like a service account that Azure manages — no password, no key. When the app wants to call the model, it gets a token from Entra ID. Entra ID checks: does this identity have the Foundry User role on this resource? Foundry User is the built-in role that allows calling the models and nothing else. No key in this picture, and every call is connected to an identity. You can see in the logs exactly who called what.

And now the best part: DefaultAzureCredential. This is not one credential — it's a chain. The code tries different credential sources and takes the first one that works. On my laptop, there is no Managed Identity, so it uses my Azure CLI login. In Azure, it finds the Managed Identity and uses that. Same code, both places. No if statement, no special config. This also means for local development my own user needs the Foundry User role too.

This is the whole API: a minimal API in .NET 10, one endpoint. The important part is here: we use the official OpenAI SDK — two things make it work with Azure. First, the endpoint: our Foundry resource with /openai/v1 at the end. Second, these two lines: instead of an API key, we create a DefaultAzureCredential and wrap it in a BearerTokenPolicy with the scope ai.azure.com. Remember this scope — ai.azure.com. If your auth ever fails with a valid identity, check this string first. And look at this: this is the full difference between the key version and the keyless version — about ten lines. Going keyless is not a big rewrite. The config only has the endpoint and the deployment name, no secrets. My real endpoint is in a local file that is gitignored, so this repo can be public — and it is, link in the description.

The infrastructure is in Terraform, because in real projects you don't click this together in the portal. Three things I want to show you. First, the Foundry resource. Important detail: it needs a custom subdomain. Without it, Entra ID auth does not work, and the error message will not tell you why. Second, my favorite line: local auth disabled. The resource does not accept keys at all. When the security review asks "how do you rotate your keys?", the answer is: there are no keys. The model deployment — gpt-5.4-mini here. Models retire on a schedule, so check what your region offers before you pin a version; this command is for that. And third, the role assignments. The app's identity gets the Foundry User role, only on this one resource, nothing more. And my own user gets the same role, so local development works.

I already deployed the API to an App Service in Azure. The steps are in the README — it's one publish and one az webapp deploy command. What I want to show you instead is how this looks in Azure, because this is what a security review actually looks at. Here is the App Service, under Identity: you see the system-assigned Managed Identity is on, this object ID. This is the app as an identity in Entra ID. Nobody knows a password for it, because there is none. And here you see what this identity can do: exactly one role, Foundry User, on exactly one resource — not the resource group, not the subscription. If this app is ever compromised, this is the whole blast radius. Same picture from the other side: on the Foundry resource, two identities have access — the app, and me, for local development. This list is your answer when someone asks "who can call our AI models?". And the Keys page: disabled. Nothing to steal here.

And now the proof. Locally — no key on this machine, my CLI login does the auth — it works. And the same request against the deployed app also works. Same code, different identity.

Everything is in the repo, link in the description: code, Terraform, and a README with the exact steps and the errors you might see — like a 401 directly after terraform apply. That one is normal; role assignments need a few minutes. In a real enterprise setup there is more: user-assigned identities, security groups, and an API Management gateway in front of the models. If you build AI on the Microsoft stack for real projects, subscribe. And if your team works on exactly this right now, my LinkedIn is in the description. Thanks for watching.

Fragen zu diesem Projekt?

Wenn Sie diese Architektur besprechen möchten, Unterstützung bei einer ähnlichen Integration benötigen oder Enterprise-Support für Azure AI, .NET oder Dynamics 365 suchen — ich freue mich auf den Austausch.

Kontakt aufnehmen