Skip to content

2026-09-18 · [email protected]

Your Offboarding Playbook Stops at the Human

Disabling the account ends the person's sessions. It does nothing to the app registrations they own, the secrets and federated credentials on those apps, the role assignments their object ID created, or the pipeline service connections still holding standing access. This is the inventory you can run against your own tenant today, and the cascade that inventory specifies.

Offboarding, in most shops, is a checkbox. The account is disabled, the sessions are revoked, the laptop is wiped, the ticket closes. The person is gone, and the control plane agrees.

The control plane agrees about exactly one object. Everything that person created is a different object, with a different lifecycle, and not one of those lifecycles is attached to their employment. The app registration they stood up in 2024 still exists. The client secret on it still authenticates. The federated credential they added so a pipeline could deploy without a secret still exchanges tokens. The role assignment they created still grants what it granted. The service connection still has standing access. None of that appears in a check that the person’s account is disabled, because none of it is the person.

This essay has a spine you can run yourself. There is an inventory, it is six queries, and each one produces a number about your tenant rather than a number I am asking you to believe. Run it against your own account first — you will learn more from your own surviving footprint than from anyone else’s anecdote. The second half is what to build once you have the list, because the list is the specification for the cascade.

What the checkbox actually does

Start by being precise about the control you already have, because it is a good control and it is not the one people think it is.

Microsoft’s documented procedure for revoking a user’s access in an emergency is three steps: clear Account enabled on the user, select Revoke sessions, and disable the user’s registered devices. In PowerShell that is Update-MgUser -AccountEnabled:$false, then Revoke-MgUserSignInSession, then a loop over Get-MgUserRegisteredDevice. The revoke step invalidates all refresh tokens issued to applications for that user and the session cookies in their browser, by resetting the signInSessionsValidFromDateTime property on the user object. Microsoft notes there may be “a small delay of a few minutes before tokens are revoked.”

What it does not do is retract access tokens already in flight. By default, access tokens issued by Microsoft Entra ID last one hour. Continuous access evaluation narrows that window: enabled services subscribe to critical Entra events and enforce them near real time, with latency of up to fifteen minutes from event propagation. Read the list of events CAE evaluates — account deleted or disabled, password changed or reset, multifactor authentication enabled for the user, an administrator explicitly revoking all refresh tokens, high user risk detected by ID Protection. All five are events about a user. The initial implementation, per the same page, focuses on Exchange, Teams, and SharePoint Online. Azure Resource Manager is not among them.

So the mechanism is sound, and the mechanism is scoped to a principal. It fires on a user object and propagates to resource providers that subscribe to user events. The app registration that person owns is a separate directory object with its own credentials and its own token flow, and nothing in that flow references the user at all. Disabling the human does not disable it, and no event in the critical-event list says it should.

That is the whole gap, stated as a testable claim: every control in the standard offboarding runbook operates on the departing person’s principal, and the things that outlive them are not that principal.

The inventory is six queries

Run these against your own object ID. You will produce the number; I have none to offer you.

Owned directory objects. GET /users/{id}/ownedObjects in Microsoft Graph v1.0 returns the directory objects a user owns — applications, service principals, and groups come back in the same collection. Check the permissions table before you script it: this method supports delegated permissions only, and application permissions are not supported in v1.0. An unattended offboarding job cannot call it app-only, which is a constraint worth discovering now rather than during an incident.

Credentials on each of those applications. Every application object carries a passwordCredentials collection and a keyCredentials collection, managed through the addPassword / removePassword and addKey / removeKey actions. Pull both for each owned app and sort by expiry. A secret is a bearer credential; anyone who held it once holds it until it is removed or expires, and the directory keeps no record of who copied it.

Federated identity credentials on each of those applications. GET /applications/{id}/federatedIdentityCredentials. These are the ones people forget, because the pitch for workload identity federation is that it eliminates secrets — and it does. It does not eliminate standing trust. Each credential is an issuer plus a subject plus an audiences value, and any workload presenting a token whose claims match that issuer and subject gets an access token for the application. A maximum of twenty federated identity credentials can be added to an application or user-assigned managed identity. Microsoft states the authorization model plainly: “Anyone with permissions to create an app registration and add a secret or certificate can add a federated identity credential to an app.” These credentials also do not consume the tenant’s service principal object quota, so no quota pressure will ever surface them for you.

Azure role assignments the person created. The RoleAssignment object exposes properties.createdBy, documented as “Id of the user who created the assignment,” alongside createdOn, updatedBy, and updatedOn. Query authorizationresources in Resource Graph at directory scope and filter on your object ID. This tells you what the person granted, which is a different and usually larger set than what the person held. Worth noticing while you are in there: the PrincipalType enumeration on role assignments now includes AgentUser and AgentServicePrincipal alongside User, Group, and ServicePrincipal. Agent identities are already first-class in the object model. They will be in this inventory too.

PIM eligibilities. GET /roleManagement/directory/roleEligibilityScheduleInstances, filtered on principalId. This is the current v1.0 surface, returning unifiedRoleEligibilityScheduleInstance objects with principalId, roleDefinitionId, directoryScopeId, startDateTime, endDateTime, and memberType. An eligibility is not an active grant, which is exactly why it survives a review of active grants.

Pipeline service connections. GET https://dev.azure.com/{organization}/{project}/_apis/serviceendpoint/endpoints?api-version=7.1. Each ServiceEndpoint carries createdBy, an IdentityRef documented as “the identity reference for the user who created the Service endpoint.” IdentityRef includes an isDeletedInOrigin boolean, which means Azure DevOps will cheerfully tell you that a live service connection was created by someone who no longer exists in the directory. Microsoft’s own framing of the risk is direct: “Service connections have standing access to the services they connect to. If a connection is still referenced in a pipeline but that pipeline is no longer being used, the access can remain longer than intended.”

There is a seventh query worth running for a different reason. POST /directory/deletedItems/getUserOwnedObjects, with a userId and a type of Group or Application, returns objects the person owned that have been deleted but are still restorable. Deleted directory items remain available to restore for up to thirty days before permanent deletion. The API returns up to 1,000 objects and does not paginate. Run it to see the shape of the problem: ownership is durable enough that Microsoft built an API to resolve it after the object was deleted.

Why the list does not shrink on its own

Three structural reasons, each checkable.

Ownership is a relationship, not a lifecycle. The owners relationship on an application is described as “a set of nonadmin users or service principals who are allowed to modify this object.” It confers modification rights. It does not confer a responsibility that expires, and nothing in the object model ties it to employment status.

Credentials have no owner at all. A passwordCredential has a key ID, a display name, and dates. It does not record who created it or who has read it since. Rotation is the only operation that invalidates a copy, and rotation is a deliberate act somebody has to schedule.

And createdBy is forensic metadata, not a foreign key. Deleting the creator does not cascade to the assignment, because the assignment’s functional reference is principalId — who it grants to — and principalId is not the leaver. That decoupling is correct design. It is also why a departure produces no referential-integrity event anywhere in the graph.

Which raises the harder constraint: your ability to reconstruct any of this decays on a clock. Azure retains activity log events for ninety days and then deletes them, and Microsoft is explicit that “the activity log is the only place that stores the creator of a resource,” and that preserving resource creator information is “one of the most common reasons to extend the retention period.” On the directory side, Entra audit logs and sign-in logs are retained seven days on Entra ID Free and thirty days on P1 and P2. If nobody configured a diagnostic setting to a Log Analytics workspace or a storage account, then for anyone who left more than ninety days ago the question “what did they create” is not a slow query. It is unanswerable from the platform.

So run the inventory now, and check your diagnostic settings while you are in there. The evidence has a shelf life, and it is shorter than most tenures.

Microsoft’s recommendation is a calendar reminder

The app registration security best practices page is clear-eyed about most of this. It says owners can manage all aspects of a registered application. It says to review credentials for freshness of use and expiration, because “an unused credential on an application can result in a security breach.” It says, flatly, not to use password credentials at all — prefer a managed identity, then a federated credential from a platform you trust, then a certificate from a trusted authority, and only then a secret.

Then it reaches the departure case and says this: “An administrator should review the owners list once every few months to make sure that owners are still part of the organization and should still own an application.”

That is the documented state of the art. Not an event, not a trigger, not a cascade — a periodic manual review, on a cadence measured in months, executed by a human who has to remember. It is not wrong advice. It is what remains available when the platform has no offboarding primitive for owned objects, and it tells you precisely where the gap is.

Azure DevOps has shipped the closest thing to an automatic answer, and its shape is instructive: Azure Pipelines “might automatically disable service connections that you didn’t use for 100 days,” recording the disable and enable events in the audit log. That is a decay timer on unused standing access, not a response to a person leaving. It disables rather than removes, and it is indifferent to whether the creator is still employed. A good control for a different problem.

The cascade has three verbs

Once you have the inventory, the design falls out of it, because every item resolves to exactly one of three dispositions. The hard engineering is deciding which, not doing which.

Reassign what the business depends on. An app registration that runs payroll does not get revoked because its owner quit. It gets a new owner, before the old one is removed. Microsoft’s guidance on removing an owner notes that “as a recommended best practice, apps should have at least two owners” — which is the reassign step, stated as a precondition. Establish the successor first, verify the successor can administer the object, then drop the leaver. Any cascade that revokes before it reassigns will eventually take production down and will be switched off after the second incident.

Rotate what they could have copied. Every passwordCredential and keyCredential on every app they owned is a credential they could have exfiltrated on any day of their tenure, and the directory cannot tell you whether they did. Rotation is not a punishment and implies nothing about the person; it is the only operation that makes the question moot. Rotate, deliver the new credential to the successor or to the vault the workload reads from, and record the rotation. Then stop the recurrence at the policy layer: an appManagementPolicy in Graph v1.0 carries a restrictions object of type appManagementConfiguration with passwordCredentials and keyCredentials restriction collections, assignable to specific applications and service principals through its appliesTo relationship, with tenantAppManagementPolicy supplying the default where no per-app policy exists.

Revoke the rest. Federated identity credentials whose subject points at a repository or workflow nobody runs. Role assignments granting access to resource groups that were decommissioned. PIM eligibilities on roles the successor does not need. Service connections whose pipelines have not run in a year. This is the largest bucket by count and the easiest to execute, which is precisely why teams that do only this bucket feel finished and are not.

Each of the three produces an audit record per item: object, disposition, actor, timestamp, and the evidence that justified it. One record per item, not one summary per termination. A summary tells you a cascade ran. A per-item record tells an auditor eighteen months later why a specific federated credential was deleted, and it answers the only question that matters mid-incident, which is what the cascade did not touch.

What separates a cascade from a script

Everyone writes the script. The script is a Saturday. What it lacks is the four properties that make it survivable.

It runs from the same event that disables the account, not from a ticket that references it. If the cascade requires a human to remember to start it, it inherits the failure mode it was built to eliminate.

It has a dry run that produces the full disposition list before anything executes, and the dry run is the artifact that gets approved. Reassignment decisions need a human, and you want that human reading a list, not a diff of what already happened.

It is idempotent and resumable. Half a cascade is worse than none, because it leaves you believing the job is done. Every step is a directory operation that can fail independently, and rerunning has to converge rather than double-act.

And it is scoped by the same inventory you ran at the top of this essay, which means the inventory is code: versioned, reviewed, diffable. When a new surface appears — and it will, because AgentUser and AgentServicePrincipal are already in the PrincipalType enumeration — the inventory gets another query and the cascade another disposition rule, and both land in the same pull request.

This is the same argument as ephemeral identity is not access control, pointed at the other end of the lifecycle. Short-lived credentials answer what happens while a workload is running. The cascade answers what happens when the person who authorized it is no longer here. Neither is optional, and the second one is the one nobody has built.

The offboarding checkbox asks a question about a person. Everything expensive that survives a departure is a question about the objects that person left behind, and that question has never once been answered by disabling an account. Go run the six queries against your own object ID. Whatever number comes back is the one your playbook has been quietly treating as zero.


This essay is by Will Barbier, CEO of Kloudmorph, LLC — a senior cloud-architecture practice. If you run cloud operators at scale and nobody can tell you what your last five departures left behind in the tenant, [email protected].