Table of Contents

Introduction

I encourage you to read these posts first to get the most out of the information provided here:

We already know that a security principal can take various forms:

  • an Entra ID user
  • an Entra ID group
  • a service principal

But if you have ever opened the Azure portal and noticed the option for Managed Identity, you might have wondered: what exactly does that mean, and how does it fit into the RBAC model?

Managed Identity option visible on the Add role assignment screen (Access control IAM).

Managed Identity

Solving this puzzle is very simple.

A Managed Identity is simply a service principal that is managed by Microsoft. You may now be wondering: what does that actually mean?

I believe that an explanation based on an example is the most informative, so here it is! Let’s take a step back to understand it better.

Diagram showing Azure Function App reading data from Azure AI Search using a service principal.

Objective: Allow the Azure Function App to read data from the Azure AI Search service without using a connection string that includes an admin access key.

So far, we have learned that we need a security principal in order to assign an appropriate RBAC role to it. After reviewing the short list of 3 available security principal types, we quickly come to the conclusion that the appropriate type is a service principal. Great, let’s create one!

App registration

In order to create a service principal, we first need to create a new app registration in Entra ID.

An app registration is an entry in Microsoft Entra ID that defines an application’s identity and enables it to authenticate and receive tokens.

First, let’s open Entra ID from the sidebar menu, then click App registrations, and finally select New registration. Then just provide a name and click Register.

Entra ID, App Registrations tab, New registration option.

We have just created a new app registration!

App Registration overview screen.

As you can see there are 3 GUIDs displayed on the overview screen and we have to understand what each one means.

  • Application (client) ID – a unique identifier for our app registration, used by clients to request tokens.
  • Object ID – the unique identifier for this specific app registration instance within Entra ID a.k.a. a security principal ID.
  • Directory (tenant) ID – the unique identifier of my Entra ID tenant, representing the organization where the app is registered.

Now here’s the important part to understand: when there is a connection string with an access key, our app can prove it has access because it possesses that specific key.

The same rule applies here:

Our Function App must prove that it is eligible to use the service principal we have just created.

Of course, at this point the most important question arises: how!?

Certificates and Client secrets visible in the App Registration resource in Azure.

We have 2 options: we can prove it using a certificate or a client secret.

Let’s assume that we decided to prove it using a client secret.

Client secret created within an app registration in Azure Entra.

We have just created a new secret OurFirstClientSecret . It is reasonable to copy that secret value to an Azure Key Vault and keep it there. Please note as well that it has the expiration date set!

Okay, great, we have a secret! But what do we do next? How would this look on the Function App code side?

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Azure.Identity;
using Azure.Search.Documents;

namespace DeployedInAzure.AzureAiSearchSamples
{
    public static class ServiceCollectionExtensions
    {
        extension(IServiceCollection services)
        {
            public IServiceCollection RegisterAzureAISearchClientsWithEnvironmentCredential(IConfiguration configuration)
            {
                var credential = new EnvironmentCredential();
                var endpoint = new Uri(configuration["AiSearch:Uri"]);

                services.AddKeyedSingleton(
                    serviceKey: AzureAiSearchIndexes.SPACE_ENTITIES,
                    new SearchClient(endpoint, configuration["AiSearch:Indexes:SpaceEntities:IndexName"], credential));

                return services;
            }
        }
    }
}

As you can see, we are registering a new SearchClient (from the Azure.Search.Documents library), which allows our function to read data from the Azure AI Search service. But the real hero of this section is not the SearchClient – it’s the EnvironmentCredential.

Environment Credential

EnvironmentCredential is part of the Azure.Identity library. It’s a credential type that tells your application: “Look at the environment variables to find the identity I should use.”

You can find in the documentation that if you want to use a client secret, the required environment variables are:

VariableDescription
AZURE_TENANT_IDThe Microsoft Entra tenant (directory) ID.
AZURE_CLIENT_IDThe client (application) ID of an App Registration in the tenant.
AZURE_CLIENT_SECRETA client secret that was generated for the App Registration.

As a careful reader, you can now see why it was so important for you to understand the individual GUIDs in the application registration we have created.

But wait a second – you may be wondering: how should I pass the secret to my Function App, given that it is such a sensitive value?

Your first thought will probably be to simply copy this value as a secret to your CI/CD tool like GitHub or Azure DevOps and just use it when specifying appSettings when you deploy your function to Azure (using AzureFunctionApp@2 task for example).

It could work, but the problem is that the secret would be visible on the Environment Variables page of the Function App (of course to users with the right roles but visible nonetheless). In addition, you were forced to copy that secret outside of a secure Azure Key Vault, which is not ideal either.

And at this moment, Azure Key Vault References enters the arena of our struggles in a blaze of glory!

Azure Key Vault References

Azure Key Vault References let your app securely pull secrets directly from Key Vault into its configuration. Thanks to this, you don’t need to copy secrets into places like CI/CD tools, keeping everything secure and centralized, which is the most desired situation.

It’s the right time to enable a managed identity for our Function App. Why? Because in order to use Key Vault References, the Function App must authenticate to Key Vault without relying on a client secret.

System Managed identity enabled for a Function App.

Let’s enable the System Managed identity. We will get back to that concept in a second. Once it is enabled we can jump to the Key Vault and assign Key Vault Secrets User RBAC role following the principle of the least privilage.

Key Vault Secrets User RBAC role assigned to a function app.

Now that this is done, we can specify the 3 environment variables in our Function App that the EnvironmentCredential class requires.

Three environment variables which EnvironmentCredential class requires added to a function app.

Let’s focus on the AZURE_CLIENT_SECRET environment variable. As you can see on the right side, the source for that record is Key Vault, and the value is set to @Microsoft.KeyVault(VaultName=deployed-in-azure-kv;SecretName=OUR-APP-REG-CLIENT-SECRET) We can also see a green icon which indicates that the secret value from the Key Vault service was resolved successfully.

The last step is to assign the Search Index Data Reader role to our service principal in the Access Control (IAM) tab of the Azure AI Search service.

Search Index Data Reader role assigned to a service principal.

That’s it! Our Function App can now read data from Azure AI Search in a secure way, using the service principal identity provided through the EnvironmentCredential class.

But… wait a second…

If we used the managed identity to connect to the Key Vault service, couldn’t we use the same identity to read data from Azure AI Search? So why did we need the service principal and the EnvironmentCredential class at all?

That’s a great observation! The reason we walked through the service principal and EnvironmentCredential setup was simply to illustrate the full set of steps required to establish a secure connection the “traditional” way.

At the beginning of the post, I mentioned that “A Managed Identity is simply a service principal that is managed by Microsoft.” Now we can better understand what managed means in this context.

Microsoft automatically creates a service principal under the hood and takes responsibility for renewing client secrets (or certificates), so you, dear reader, don’t have to worry about it.

Isn’t that helpful?

If this concept is new to you, a valid question may arise: how can I prove in my C# code that I have access to the managed identity, assuming I no longer want to use the EnvironmentCredential class, which requires a secret?

Here comes the DefaultAzureCredential class to our rescue!

DefaultAzureCredential

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Azure.Identity;
using Azure.Search.Documents;

namespace DeployedInAzure.AzureAiSearchSamples
{
    public static class ServiceCollectionExtensions
    {
        extension(IServiceCollection services)
        {
            public IServiceCollection RegisterAzureAISearchClients(IConfiguration configuration)
            {
                var credential = new DefaultAzureCredential();
                var endpoint = new Uri(configuration["AiSearch:Uri"]);

                services.AddKeyedSingleton(
                    serviceKey: AzureAiSearchIndexes.SPACE_ENTITIES, 
                    new SearchClient(endpoint, configuration["AiSearch:Indexes:SpaceEntities:IndexName"], credential));

                return services;
            }
        }
    }
}

DefaultAzureCredential is part of the Azure.Identity library. It simplifies authentication by trying multiple credential sources in a fixed order until one works (the first source in that chain is the already familiar EnvironmentCredentialclass). This means you don’t have to hard‑code secrets or switch code between local development and production.

Each credential source inherits from the TokenCredential abstract class and implements AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) method.

One of the credential sources is ManagedIdentityCredential. Thanks to this class, we only need to provide the base URI to our service – no secrets, no certificates, as simple as that!

Let’s say that, for whatever reason, you don’t want your C# code to go through every credential source. Instead, you’d like to allow only Managed Identities and Visual Studio (for local development) as authentication methods. You can easily achieve this by using the ChainedTokenCredential class.

var credential = new ChainedTokenCredential(new ManagedIdentityCredential(), new VisualStudioCredential());

I think that equipped with this dose of knowledge you are ready to safely connect to services in Azure.

There is one more concept we should understand to have a complete picture of managed identities. When enabling the managed identity for our Function App, you could see two options: System Managed and User Managed. The difference is really simple, so let’s break it down.

System Managed

With a system‑assigned managed identity, Azure automatically creates and manages the identity for your resource. You don’t need to configure anything because Azure takes care of provisioning and renewing credentials behind the scenes. This option is quick, secure, and requires no manual setup BUT…

The lifetime of the service principal created behind the scenes is tightly coupled with the lifetime of the resource. In simple terms:

If I remove the deployed-in-azure-fa Function App, the managed identity will be automatically removed as well!

User Managed

A user‑assigned managed identity is created and controlled by you. Unlike the system‑assigned version, it can be shared across multiple resources and reused wherever needed. This gives you more flexibility and control, while still removing the need to handle secrets or certificates yourself.

In addition, a resource can have more than one user‑assigned managed identity.

ℹ️ Both points stem from the fact that you can assign more than one user-assigned identity to a single resource.

1st – you have to modify your C# code and specify the identifier of a given user-assigned identity.

var userAssignedClientId = configuration["AiSearch:UserAssignedClientId"];

var credentialOptions = new DefaultAzureCredentialOptions
{
    ManagedIdentityClientId = userAssignedClientId
};

var credential = new DefaultAzureCredential(credentialOptions);

2nd – you must specify which identity should be used when connecting to Key Vault in order to resolve Key Vault References (assuming there are any).

For example, in an Azure Function App you can provide the identity’s GUID using the properties.siteConfig.keyVaultReferenceIdentity field in the resource’s JSON definition. This cannot be set directly in the Azure Portal, so it must be done through ARM templates, Bicep, or CLI/PowerShell.

Service principal vs Connection string

Dear reader, if you have made it this far, I am truly grateful. It also feels like I have your permission to share one more observation.

Imagine you have a process running outside of Azure that needs to read data from an Azure Storage Account. Right now, it uses the primary connection string. After reading this article, you might conclude it’s time to switch to a service principal (since managed identity isn’t an option for non‑Azure services).

You share this idea in a daily meeting, but a colleague pushes back: It won’t make any difference because both approaches require secret values or a certificate.

But here’s the nuance:

  • Connection string
    • Embeds account keys directly.
    • If leaked, it grants full access to the storage account.
  • Service principal
    • Can be granted only the permissions it needs through Azure RBAC role assignments (for example, read‑only access to a specific Storage Account).
    • Uses Entra ID for authentication. and therefore supports conditional access, auditing, and RBAC – features connection strings don’t provide.

So while both approaches involve a credential, using a service principal is better than relying on a connection string.

ℹ️ Many Azure services provide an option to completely disable access keys, and this is the recommended approach for stronger security. However, don’t apply this rule universally because there are exceptions where access keys must remain enabled.

Summary

I hope this article showed how managed identities fit into Azure RBAC. System‑assigned managed identities live and die with the resource, while user‑assigned managed identities are reusable across resources and have an independent lifecycle that you control.

I also hope you now see how authentication works in practice: EnvironmentCredential reads identity details from environment variables, while DefaultAzureCredential automatically tries multiple sources (including managed identity) so you don’t need to hard‑code secrets or switch code between environments.

Finally, I hope you see why service principals are safer than connection strings, especially for non‑Azure services.

Thanks for reading, and I hope to see you in the next post!

Categorized in:

Security,