DEV Community

Anton Staykov
Anton Staykov

Posted on

Every change to an Entra extension is a Control Plane event: the monitoring contract

Parts 1 and 2 of this series (Microsoft Entra extensibility is a gift. It is also Control Plane. and Securing the code that decides who Entra trusts) made two static decisions. Where the code lives: a dedicated Control Plane subscription, directly under the root management group or under a dedicated Control Plane management group, never in the platform identity subscription or an application landing zone. What credential it uses to call out: a managed identity by default, federated identity credentials when the call must leave Azure, certificates as a tolerated middle step, and static symmetric keys never.

Both decisions are one-time. You make them, you walk away, you do not touch them for months.

The third decision is not like that. It is continuous, and it is the one most teams quietly skip: how do you know the deployed code on that Function App is still the code your reviewers approved? How do you know the Logic App workflow definition has not been rewritten since last Tuesday? How do you know nobody added a federated identity credential to the managed identity at 3 a.m. on a Saturday?

The answer is monitoring. Not "we have Log Analytics turned on." Monitoring with a specific operating contract attached.

The posture inversion

For most Azure workloads, the default operating posture is reasonable trust. Engineers deploy. Pipelines run. Configuration drifts a little. The team reviews changes weekly. Anomalies are caught eventually.

For a Microsoft Entra extension, that posture is wrong. The default has to be inverted.

Once an Entra extension lands in production, every change to it is suspicious by default.

Not "needs review." Not "let's check first." Suspicious. The default state of an alert firing on a Function App that hosts a custom claims provider is "the SOC is investigating, prove this was approved." If you cannot prove the change was approved within the team's response SLA, the change is treated as an incident and rolled back. That posture is harsh on purpose. The same RBAC inheritance chain that lets a Contributor four management groups above your Function App quietly replace its code (the central argument of Part 1) is what forces the inverted default. You cannot rely on the deployment surface to be small. You have to rely on the alerting surface to be loud.

That inversion is the monitoring contract this article is about.

The three change surfaces

A production Entra extension has three change surfaces, and each one needs its own forced-diagnostics control, its own alert, and its own row on the operating contract.

Surface 1: the runtime itself

This is the surface most teams think of first, and the one most teams under-instrument. The deployed code on the Function App. The workflow definition on the Logic App. The App Settings. The environment variables. The runtime version. The configured custom domain. Each of these is a knob that, if turned, changes the behavior of the extension the next time Entra calls it.

For the custom claims provider Function App, the relevant Azure Resource Manager operations to alert on are:

  • Microsoft.Web/sites/extensions/write (a new deployment was pushed).
  • Microsoft.Web/sites/config/appsettings/write (App Settings were changed, including any reference to a Key Vault secret).
  • Microsoft.Web/sites/config/web/write (runtime version, startup command, or general configuration was changed).
  • Microsoft.Web/sites/publishxml/action (publish profile credentials were generated, which is one of the easiest ways to deploy code outside the normal pipeline).

For the dynamic approval Logic App, the analogous operations are:

  • Microsoft.Logic/workflows/write (the workflow definition itself was modified).
  • Microsoft.Logic/workflows/connections/write and Microsoft.Web/connections/write (an API connection was created or modified, including the authentication target it points at).

Each of these should be backed by an Activity Log alert scoped to the Control Plane subscription, with an action group that routes to the SOC's primary intake. Not to the team's chat channel. Not to an email list. The SOC's intake.

Surface 2: identity and RBAC

This is the surface that does not look like the extension at all and is therefore the easiest to ignore. The extension is unchanged. The blast radius around it is what changed.

Specific things to alert on, across the Control Plane subscription:

  • Microsoft.Authorization/roleAssignments/write anywhere on the inheritance path, from the resource up to the subscription. A Contributor (or User Access Administrator, or Owner) just appeared on a tier in the chain. Walk the inheritance (Azure RBAC overview) and ask: does this new principal now have effective Contributor on the Function App? If yes, the SOC needs to know.
  • Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials/write on any user-assigned managed identity in scope. A new federated identity credential is, by construction, a new way to act as that identity from outside Azure. Treat it as a credential issuance event, because that is what it is.
  • Microsoft.KeyVault/vaults/accessPolicies/write and Microsoft.Authorization/roleAssignments/write scoped to the Key Vault. Same logic as the RBAC inheritance, sharper consequence: Owner on the Key Vault is operationally Owner of every certificate in it.
  • App registration credential events in the Entra audit log. The Function App and the Logic App that back the extension are both fronted by app registrations or managed identities in Entra. A new client secret, certificate, or federated credential added to that app registration is an alert-worthy event, full stop. The Entra audit logs capture these as Update application – Certificates and secrets management events; pipe them into the same SOC intake.

Surface 3: the Entra-side binding

This surface lives entirely in Entra, not in Azure. The custom claims provider is bound to the application through a custom authentication extension configuration. The dynamic approval Logic App is bound to an access package through an entitlement management custom extension. Those bindings are what make Entra actually call your code. Change the binding, and Entra now calls somebody else's code instead.

Two specific events worth their own alerts:

  • Changes to custom authentication extensions in the Entra audit log: the endpoint URL was repointed, the API authentication configuration was changed, the extension was disabled.
  • Changes to entitlement management custom extensions: a new callback URL, a new authentication method, a change to the access package policy that consumes the extension.

The same Entra audit log feed that the IGA team already monitors covers most of this. The extra step is making sure the SOC sees it alongside the Azure Activity Log events, because the correlation between "the Entra binding moved" and "a deployment happened on the new target one hour earlier" is exactly the story you want analysts to be able to tell.

Forced diagnostics: making the signal undeniable

Alerts only fire if the signal reaches the alert engine. Three controls turn diagnostics from "we enabled it on most resources" into a property of the subscription itself.

Azure Policy with deployIfNotExists for diagnostic settings. Apply built-in or custom policies at the Control Plane management group that force every Function App, every Logic App, every Key Vault, every Storage Account, and every Managed Identity created underneath it to ship diagnostic logs to a dedicated tenant-level Log Analytics workspace. The policy is deployIfNotExists, not audit, so a missing diagnostic setting is repaired by the platform, not just reported on. There are existing built-ins covering Logic Apps and many resource types; custom definitions cover the rest. The principle is: it is not optional, and it is not possible to deploy a resource into this management group without diagnostic settings.

Azure Policy deny for tampering with diagnostic settings. A deployIfNotExists policy creates the setting. A complementary policy denies its modification or removal by anyone except a tightly-scoped platform identity. If an engineer cannot turn the diagnostic setting off, an attacker who compromises that engineer cannot either.

Resource Locks on the resource groups that host the extensions. Resource Locks at CanNotDelete for everything, ReadOnly for the resource groups that hold the runtime resources, applied at the resource-group level. ReadOnly does not prevent code deployments through the Function App's own publish endpoint, so it is not a substitute for the alert on Microsoft.Web/sites/extensions/write, but it does block the entire class of "an attacker recreates the resource with the same name and a different definition" attack.

Layered: policy forces diagnostics on, policy prevents diagnostics from being turned off, resource locks prevent the resources from being deleted out from under the diagnostics, alerts on the resulting log stream surface every change. Each control compensates for the failure modes of the others.

Correlation: the story the SIEM has to be able to tell

Each individual alert is signal. The value lives in the correlation across feeds. The SOC should be able to ask, and answer, questions like:

  • A token issued by Entra at 14:07 carried a claim asserting "department=Finance." The custom claims provider for that application is hosted on Function App fa-claims-prod. Did the deployed code on fa-claims-prod change in the last 24 hours? Did its App Settings? Did any role assignment land on it, its resource group, its subscription, or any management group above it?
  • An access package request was auto-approved at 09:21 for a privileged group. The dynamic approval Logic App that decided is la-dynapproval-prod. Did the workflow definition change in the last 7 days? Did any of its API connections get repointed? Did the Logic App's managed identity get a new federated credential?

Both questions are answerable with Azure Activity Log, Entra audit log, Function App and Logic App resource logs, and the extension's own application logs, all sitting in one Log Analytics workspace, with Microsoft Sentinel on top stitching them together. The Sentinel queries are the easy part. The hard part is making sure the three feeds are guaranteed to be there, which is exactly what the forced diagnostics controls deliver.

The operating contract: three states, not four

Every alert from any of the three change surfaces lands in one of exactly three buckets. There is no fourth.

Approved change. The deployment was pre-declared in the change management system. The change record carries the artifact hash, the pipeline run ID, the requesting engineer, and a declared start and end window. The alert correlates to the change record automatically (on pipeline run ID, on deployment principal, on time window) and is closed within minutes. The audit trail shows the alert, the correlation, and the closure.

Informational platform change. Microsoft platform-driven activity that you do not control: automatic certificate rotations on platform-managed components, Microsoft-side runtime version refreshes, Azure-managed identity key rotations, and similar. These still ship to the same workspace and still fire as Activity Log entries, but they route to a low-severity "platform" queue, not to the SOC's primary intake. Two rules govern this bucket: the events must be attributable to a known Microsoft service principal or platform operation (the actor identity on the Activity Log entry is non-human and matches an allow-listed Microsoft pattern), and the bucket must be reviewed at least weekly so it does not become a quiet hiding place. Anything that does not cleanly match a platform pattern falls through to the third bucket by default.

Incident. Anything that is neither pre-declared by a change record nor cleanly attributable to a Microsoft platform operation. SOC intake, paged on-call, automatic rollback if the runtime change can be reverted, manual investigation either way. The default reaction is to revert first and investigate after. The cost of a wrongly-reverted legitimate change is a Slack apology. The cost of a wrongly-allowed malicious change is a forged claim in a token your applications already trusted.

That is the entire operating model. Approved, informational-platform, or incident. No "we will look at it later." No "probably the platform team." If it does not fit one of the first two buckets within a tight automatic correlation window, it is bucket three.

The three-legged stool

Part 1 placed the resources correctly. Part 2 made sure they authenticate without leaking secrets. Part 3 makes sure that, once they are running, every change to them is either justified or visibly investigated. None of the three legs stands alone. A correctly placed resource without monitoring is one quiet deployment away from being someone else's resource. A perfectly monitored resource using a static API key is monitoring around an already-compromised credential. A managed identity in the wrong subscription is a credential model wasted on an over-permissive blast radius.

Built together, they are how Entra extensibility stays the thing that makes Entra a great product, rather than the thing that quietly hollowed it out. Use the extensibility. Put the code where it belongs. Authenticate it with the right credentials. And treat every change to it, from the first day it is in production, as a Control Plane event worth proving.

References

Top comments (0)