Joey Victorino

Field Notes · Technical Notes · 6 min read

Offboarding disables a person, not the access they built.

Method, not a matter. General tradecraft for the destructive-insider case — no client, no sector, no incident. The table names, event IDs, and role identifiers are checkable against vendor documentation.

The most expensive destruction is not an external actor. It is a privileged administrator who is leaving, who understands the recovery plane because they built it, and who has time. Offboarding disables their user account on schedule. It does not reach the identities that account created, and it does not move the backups out of their reach. Both of those are the whole incident.

Find the blast identity, not the person

The departing account almost always dies exactly when the leaver file says it should — 4725 on the on-prem DC, an Entra Disable account minutes later, badge time matching both. That is a decoy for the untrained eye. The identity that executes the destruction is one the offboarding process never inventoried: a service principal tied to an automation account, an automation run-as, or an on-prem service account sitting in the privileged groups. Start in the control plane by asking which identity did the damage, and let the answer collapse to a single application:

AzureActivity
| where TimeGenerated between (startofday(ago(45d)) .. now())
| where OperationNameValue has_any (
    "MICROSOFT.COMPUTE/VIRTUALMACHINES/DELETE",
    "MICROSOFT.COMPUTE/SNAPSHOTS/DELETE",
    "MICROSOFT.RECOVERYSERVICES/VAULTS/.../PROTECTEDITEMS/DELETE",
    "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE",
    "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/WRITE")
| extend AppId = tostring(parse_json(Claims).appid)
| summarize ops=count(), first=min(TimeGenerated), last=max(TimeGenerated),
            opsSet=make_set(OperationNameValue)
    by Caller, CallerIpAddress, AppId, ResourceGroup
| order by ops desc

That query is how a case stops being “the environment melted down” and becomes one appId. The person’s account and the blast identity are usually different objects; the leaver file caught the first and never knew the second existed.

The sign-in log that is usually off

AADServicePrincipalSignInLogs is the record that ties the application to where it ran and which credential it used. It is frequently not enabled, and turning it on is often finding number one. Where a diagnostic setting captured the backfill, the credential key resolves the exact secret:

AADServicePrincipalSignInLogs
| where AppId == "<blast-appid>"
| project TimeGenerated, ServicePrincipalName, ResourceDisplayName,
          IPAddress, ServicePrincipalCredentialKeyId, Location
| summarize count(), make_set(ResourceDisplayName), make_set(IPAddress)
    by bin(TimeGenerated, 1h), ServicePrincipalCredentialKeyId

ServicePrincipalCredentialKeyId maps to a specific client secret — join it with the application’s credential list to get the key, its owner, and its expiry. The portal’s credential view will mislead you about which key fired; the sign-in log will not. Expect to find a secret with a long-dated expiry that nobody rotated, and a sign-in origin that is an in-region hybrid worker, not a coffee shop.

Scheduled destruction lives in automation

A logic bomb needs a clock. In cloud estates the clock is often an automation runbook with a job schedule set to a fixed offset from a future date — commonly 2592000 seconds, thirty days. The runbook definition and its job history are evidence, and they are among the first things deleted, so pull them before anyone clicks remove. The artifact most responders never ask for is the job streams: where an automation account had JobLogs and JobStreams diagnostic categories pointed at a workspace — even briefly, during some past pilot — the commands survive the portal history being gone.

AzureDiagnostics
| where ResourceProvider == "MICROSOFT.AUTOMATION"
| where Category in ("JobLogs", "JobStreams")
| where ResultDescription has_any ("Remove-AzVM","Remove-AzSnapshot","Disable-ADAccount")

The recovery plane cannot share the admin’s trust boundary

This is the architectural finding that outlives any single case, and it is where the damage is decided long before the trigger. If the person who can delete production can also delete the backups of production, you do not have a recovery plane; you have a second copy inside the blast radius. The recurring failures:

  • Vault soft-delete off, immutability never enabled. The one setting that converts “a bad year” into “a bad week”, left at the default.
  • Backup and hypervisor administration on daily-driver identity. The credential that runs the estate should not be the credential that can destroy its recoverability.
  • Break-glass accounts as members of a privileged group. A script that enumerates Domain Admins -Recursive disables them too. Break-glass that dies with the group it was meant to outlive is theater; the tabletop assumed those accounts would still exist.
  • Cert-based run-as instead of managed identity. A managed identity dies with its resource. A certificate service principal keeps working after the resource, and the person, are gone.
  • Live sessions survive credential death. Killing a privileged group does not terminate a hypervisor management session already open in the actor’s script. Session lifetime is its own control.

The logic bomb on disk

The host side answers when it was planted, when it fired, and that no one planted it afterward. The questions, not a tool list:

  • Plant versus detonation. USN journal ($UsnJrnl:$J) shows the last human write to the payload on the day the admin left, then silence until the trigger date. Amcache first-execution matches the create; prefetch names the payload path alongside the binary.
  • Bytes at the right moment. SRUM network and application resource usage ties the process to outbound bytes at detonation, not at plant — the difference between a dormant file and an active one.
  • The task that carries a ghost. The scheduled task XML under System32\Tasks, not just event 4698; its <UserId> is the service account, and its SDDL may still carry an access-control entry for the departed admin’s former group. That ACE is the link between the person and the mechanism.
  • Logging turned off on purpose. Command-line process auditing (4688 with process command line) disabled by a Group Policy edited from the leaver’s own organizational unit, days before departure, is intent with a GUID: the gpt.ini version and gPCFileSysPath date it. Script-block logging (4104) and transcription are usually already off; note their absence rather than assume their coverage.

Where the process tree is still resident, memory confirms disk rather than replacing it — a payload spawning an encoded PowerShell child, live connections to the management API and hypervisor — and it closes the “someone planted this after the fact” argument before it is raised.

What survives is what they did not know about

Hard-deleted objects past their recycle window are gone; destroyed backup repositories are gone. The rebuild seed is almost always something outside the actor’s map — a forgotten offsite replica, a legacy share, an orphaned incremental from an old test in a different subscription. Find it by querying across the whole management group, not the one subscription anyone thinks to export:

Resources
| where type =~ 'microsoft.compute/snapshots'
| project name, resourceGroup, subscriptionId, properties.creationTime

Immutability is the difference between recovering from that seed in a week and rebuilding for a year. It is a setting, and it is cheaper than the year.

What you can defend

Not “we caught the insider.” The defensible account is narrower and stronger: a departing administrator retained control of the recovery plane through identities the offboarding process never inventoried; the destruction was scheduled, on disk, before they left; and, where the estate survives, it is usually through a copy the actor did not know existed. Every clause there is a control that was missing, and naming the missing control is worth more than naming the person.

Companion: the Windows intrusion artifacts reference · Cloud Security Architecture Is an Identity Problem · The Incident Is Contained. That Does Not Mean It Is Closed.

If a privileged departure is imminent or already went wrong, this is the work I do.