Skip to content
James Dawson By James Dawson Principal I
Adventures in Least Privilege: When an owner isn't an owner

There are often situations where we need to automate the configuration of Microsoft Entra ID resources as part of our infrastructure deployments. When doing so with a least-privilege mindset, we carefully select the minimum permissions required for our automation to succeed. Or at least that's the theory and as I discovered recently, the relationship between App Registrations and Service Principals has a subtle gotcha that can break even the most carefully planned least-privilege automation.

Let me walk you through a troubleshooting journey that taught me more about some of Entra ID's behind-the-scenes activities than I bargained for.

The Setup

Picture this: you're deploying a Streamlit application on Azure Container Apps with Entra ID-based authentication and authorisation. The entire deployment is automated using Infrastructure-as-Code, including the Entra ID configuration itself. To keep things secure, you're using a user-assigned managed identity with carefully selected Microsoft Graph permissions:

  • Application.ReadWrite.OwnedBy - Create apps (& service principals) and manage those we create
  • Directory.Read.All - Read directory information for lookups
  • Group.Create - Create security groups; also enables the option of setting ourselves as owners (but only at creation time!)

These permissions are utilised through PowerShell ARM Deployment Scripts, and everything seems perfectly scoped. The Application.ReadWrite.OwnedBy permission is particularly elegant as it allows our managed identity to create App Registrations and automatically become their owner, without requiring the overly broad Application.ReadWrite.All permission.

The deployment script creates the App Registration, defines the necessary app roles, and configures the authentication settings. So far, so good.

NOTE: In case you're wondering why we're not using the Bicep Graph Extension, there are some subtleties in its current behaviour that are incompatible with a least-privilege mindset; for example, this GitHub issue.

The Problem

Then comes the moment to assign groups to the app roles exposed by our newly created app registration, enabling user authorisation within the Streamlit app. This is where things went wrong.

Error: Insufficient privileges to complete the operation

Wait, what? We own the App Registration. We can see our managed identity listed as an owner in the portal. We should have full control over this resource. Time to investigate.

Down the 'Missing Permission' Rabbit Hole

All good troubleshooting sessions must send you down a rabbit hole at some point!

Power BI Weekly is a collation of the week's top news and articles from the Power BI ecosystem, all presented to you in one, handy newsletter!

The first instinct is to check whether we need an additional Microsoft Graph permission. After all, we're trying to perform an app role assignment operation. Perhaps we need AppRoleAssignment.ReadWrite.All?

But here's the problem: AppRoleAssignment.ReadWrite.All is what Microsoft classifies as a "Tier 0" permission; one of the highest privilege levels in the entire tenant. According to the Microsoft Graph permissions reference, this permission allows an application to:

Grant additional privileges to itself, other applications, or any user

That's essentially the keys to the kingdom. An application with this permission can escalate to ANY permission in the tenant by assigning itself app roles on Microsoft Graph or other resources. This is precisely the kind of broad privilege we're trying to avoid.

Surely there must be a least-privilege variant, similar to how Application.ReadWrite.OwnedBy exists for application management?

There is no AppRoleAssignment.ReadWrite.OwnedBy permission (as we'll see, it's not necessary). Microsoft Graph API permissions are tenant-wide and cannot be scoped down to specific applications. This is a deliberate security design decision - the ability to assign arbitrary app roles is simply too powerful to scope safely.

Programming C# 12 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

For a security-conscious deployment following least-privilege principles, granting AppRoleAssignment.ReadWrite.All just to assign roles to our own application is unacceptable. There must be another way.

The First Clue

While checking the App Registration in the portal, something catches my eye. Under Enterprise Applications, there's a Service Principal for our application. But curiously our deployment script does not include creating a Service Principal, so where did it come from?

Let's check the owners of this Service Principal. And there it is, our managed identity is NOT listed as an owner. Despite owning the App Registration, we don't own the corresponding Service Principal.

This is the smoking gun.

Applications & Service Principals: Two Objects, Two Ownership Models

Let's step back and understand what's really happening here. Microsoft Entra ID has a concept that's easy to overlook if you primarily work through the Azure Portal: Application objects and Service Principal objects are separate entities with independent ownership.

As explained in Microsoft's App objects and service principals documentation:

Application Object (App Registration):

  • The globally unique definition of your application
  • Lives in your "home" tenant
  • Acts as a template or blueprint

Service Principal Object:

  • The local representation of an application in a specific tenant
  • A concrete instance derived from the application object
  • Required for the application to actually authenticate users or access resources as itself

One Application object can have many Service Principals (one per tenant where it's used in multi-tenant scenarios), but in single-tenant applications, there's typically one Service Principal in your home tenant.

The Portal's Helpful Deception

When you register an application through the Azure Portal, both the Application object AND its Service Principal are created simultaneously, and you automatically become owner of both. This convenience feature hides the fact that these are separate operations.

The API's Honest Truth

When using the Microsoft Graph API (which is what our ARM Deployment Script does under the hood) creating an Application object and creating a Service Principal are separate, explicit operations.

When you create an App Registration using the Graph API with Application.ReadWrite.OwnedBy permission, you automatically become owner of the Application object. However, the Service Principal must be created separately. If you don't explicitly create it, certain operations (like first consent or first app role assignment) will automatically create one for you.

And here's the gotcha: when a Service Principal is automatically created, it does NOT inherit ownership from the Application object or the user that triggered the implicit creation.

This is documented behaviour in the API documentation, though it's easy to miss the implications if you're not specifically looking for them.

In our case, the web app hadn't yet been accessed, so it was the 'first app role assignment' that triggered it; but it's interesting to learn that there are two scenarios for this implicit creation.

Why This Design?

This isn't a bug, it's a deliberate security boundary. Service Principals represent actual runtime identities in your tenant. They're what authenticate, what hold credentials, and what get assigned permissions; therefore ownership of them has significant security implications.

The principle here is explicit intent. The API requires you to explicitly state "I want a Service Principal for this Application" to prevent accidental sprawl of identities and ensuring proper ownership assignment.

The Solution: Embrace Explicit Creation

Once we understand the problem, the solution becomes clear. Our ARM Deployment Script needs to explicitly create the Service Principal, not rely on automatic creation.

The approach is straightforward:

  1. Create the App Registration (we automatically become owner)
  2. Explicitly create the related Service Principal (we automatically become owner)
  3. Now we can perform app role assignments because we own both objects

The beauty of this approach is that it works perfectly with Application.ReadWrite.OwnedBy. According to the permissions documentation, creating a Service Principal requires either:

  • Application.ReadWrite.OwnedBy (least privileged)
  • Application.ReadWrite.All

We already have the least-privilege permission we need.

Cleaning Up the Orphaned Service Principal

Before implementing the fix, we need to delete the automatically created Service Principal that our managed identity doesn't own or manually add it as an owner. Since we can't manage it with our current permissions (we don't own it), this requires either:

  • Using a Global Administrator account
  • Using the Application.ReadWrite.All permission temporarily
  • Using an account that has the appropriate Entra ID role (Application Administrator or higher)

Once cleaned up, update our deployment script to explicitly create the Service Principal immediately after creating the App Registration. When we run the updated script, both objects will be created with the managed identity as owner, and app role assignments will work as expected.

Lessons Learned: Principles for Least-Privilege Entra ID Automation

This troubleshooting journey reinforced several important principles for anyone automating Entra ID configuration.

1. Understand the Dual-Object Model

App Registrations and Service Principals are distinct objects. The portal's convenience features can mask this reality. When working with APIs or automation, treat them as separate entities that both require explicit management.

Ref: App objects and service principals

2. Application.ReadWrite.OwnedBy is Powerful but Has Boundaries

The Application.ReadWrite.OwnedBy permission is excellent for least-privilege automation. It allows you to:

  • Create applications (you automatically become owner)
  • Create service principals (you automatically become owner)
  • Fully manage resources you own
  • List all applications and service principals in the tenant

However, it does NOT allow you to:

  • Manage applications or service principals you don't own
  • Be an owner of Service Principals created by automatic processes

Ref: Microsoft Graph permissions reference

3. Explicit is Better than Implicit

Following Python's zen, explicit is better than implicit. In infrastructure automation, this is doubly true. Be aware of when automatic object creation can happen, so you can avoid relying on it (unwittingly or otherwise) and create them explicitly in your deployment scripts. This ensures:

  • Predictable ownership assignment
  • Clear audit trails of what was created when
  • Proper permissions from the start
  • No orphaned resources

4. AppRoleAssignment.ReadWrite.All is Too Privileged

While it might seem like an easy solution to app role assignment problems, AppRoleAssignment.ReadWrite.All is classified as Tier 0 privilege. It can be used to escalate to any permission in the tenant. Reserve this permission only for scenarios where it's absolutely necessary, and never use it when a more scoped approach exists.

5. The Portal and API Have Different Behaviours

The Azure Portal provides convenience features that streamline common workflows. This is excellent for interactive use, but automation requires understanding the underlying API behaviour. What happens automatically in the portal often requires explicit steps in API-based automation.

6. Consider Owners in Your Automation

When creating Applications & Service Principals, you can add additional owners using the Add application owner & Add service principal owner APIs respectively. This requires Application.ReadWrite.OwnedBy, although Directory.Read.All is often useful if you need to look up identities by name (as the APIs require the 'ObjectId', also known as the 'PrincipalId').

Consider whether your automation needs multiple owners for lifecycle management.

Closing Thoughts

The beauty of Infrastructure-as-Code and automation is that once you understand the correct pattern, you can codify it and never encounter this problem again. The pain of troubleshooting becomes the foundation of better practices.

Microsoft's security model for Entra ID is sophisticated and well-thought-out. The separation between Application objects and Service Principal objects provides important security boundaries. The challenge is that this sophistication isn't always obvious, especially when the Azure Portal's convenience features smooth over the rough edges.

For those of us building automated deployments with least-privilege principles, understanding these nuances isn't optional — it's essential. The difference between Application.ReadWrite.OwnedBy and Application.ReadWrite.All might seem subtle, but it represents the difference between scoped, secure automation and unnecessarily broad permissions.

I hope this troubleshooting journey saves you the hours I spent tracking down this particular gotcha.

Have you encountered similar gotchas in Azure or Entra ID automation? Leave a comment below, or ping me via Bluesky @jdawson.bsky.social.

FAQs

What are the least-privilege Entra ID permissions required by a DevOps process to manage a given Entra Application? Grant the following Microsoft Graph 'Application' permissions - `Application.ReadWrite.OwnedBy`

James Dawson

Principal I

James Dawson

James is an experienced consultant with a 20+ year history of working across such wide-ranging fields as infrastructure platform design, internet security, application lifecycle management and DevOps consulting - both technical and in a coaching capacity. He enjoys solving problems, particularly those that reduce friction for others or otherwise makes them more effective.