Cleaning Up Our Act: How to Migrate Legacy Service Accounts to Entra Workload Identities (Before It Breaks Production)

Let’s be honest: almost every M365 environment has that one legacy service account. You know the one—created three years ago as a standard user object (svc_backup or m365_reporter), password set to never expire, excluded from Conditional Access because MFA would break the scheduled script, and hardcoded into an old XML file on a local VM.

When Microsoft pushes harder toward modern authentication and Zero Trust, these legacy accounts become ticking time bombs. Between legacy protocol deprecations and security baselines screaming about non-expiring passwords, keeping user-based service accounts alive is a risk we shouldn’t be taking anymore.

The right move is shifting them to Microsoft Entra Workload Identities (App Registrations paired with certificates or secrets). Here is how I audit them, replace them, and clean up the environment without breaking production workflows.


Why Standard User Accounts Make Terrible Service Accounts

Before jumping into scripts, let’s look at why relying on regular user accounts for automation hurts us operationally:

  • The MFA Exemption Dilemma: To keep a script running, you usually have to carve out risky exceptions in your Conditional Access policies.
  • Plaintext Exposure: Static passwords living inside local PowerShell scripts or Task Scheduler definitions are easily accessible if someone compromises that management VM.
  • The Deprecation Clock: Microsoft is steadily pulling the plug on legacy authentication endpoints. Password-only script execution is on borrowed time.

Moving to Workload Identities lets us ditch passwords entirely and scope permissions strictly to what the script actually needs via Microsoft Graph.


Step 1: Run the Audit Script to Find Your Legacy Accounts

Before changing anything, you need to know the scale of the problem. I use a quick PowerShell 7 script to scan the tenant for user accounts that look like service accounts (checking for non-expiring passwords or specific naming conventions).

<#
.SYNOPSIS
    Quick audit script to find potential user-based service accounts in Entra ID.
.NOTES
    Author: DS Innovation Guru
    Site: https://dsinnovationguru.com
#>

# Make sure you've got the Microsoft.Graph module installed
Connect-MgGraph -Scopes "User.Read.All", "Directory.Read.All"

Write-Host "Scanning tenant user objects..." -ForegroundColor Cyan

$serviceAccounts = Get-MgUser -All -Property Id, DisplayName, UserPrincipalName, AccountEnabled, PasswordPolicies, CreatedDateTime | 
    Where-Object { 
        $_.PasswordPolicies -match "DisablePasswordExpiration" -or 
        $_.UserPrincipalName -like "svc_*" -or 
        $_.UserPrincipalName -like "*service*" 
    }

$report = foreach ($account in $serviceAccounts) {
    [PSCustomObject]@{
        DisplayName           = $account.DisplayName
        UserPrincipalName     = $account.UserPrincipalName
        AccountEnabled        = $account.AccountEnabled
        PasswordNeverExpires  = ($account.PasswordPolicies -contains "DisablePasswordExpiration")
        CreatedDate           = $account.CreatedDateTime
    }
}

$outputPath = ".\EntraID_LegacyServiceAccounts_Audit.csv"
$report | Export-Csv -Path $outputPath -NoTypeInformation

Write-Host "Done! Audit sheet dropped at $outputPath" -ForegroundColor Green

Step 2: Register a Workload Identity (App Registration)

Let’s say you found an old account running a daily user licensing report. Instead of using that user login, we’ll build a dedicated App Registration.

You can spin this up quickly via PowerShell rather than clicking through the portal:

# Create the App Registration for our automation task
$app = New-MgApplication -DisplayName "App-M365-Licensing-Report"
$sp = New-MgServicePrincipal -AppId $app.AppId

Write-Host "App successfully registered!" -ForegroundColor Green
Write-Host "Client ID: $($app.AppId)" -ForegroundColor Yellow

Step 3: Lock Down Permissions & Attach a Certificate

Rule number one: Never give an app Global Admin. Scope it strictly.

  1. Head over to the Entra Admin Center → App registrations → find your new app.
  2. Go to API permissions → Add a permission → Microsoft Graph → Application permissions.
  3. Check only what’s required (e.g., User.Read.All) and hit Grant admin consent.

Next, skip client secrets. For production scripts, use a self-signed certificate:

# Generate a clean 2-year cert locally
$cert = New-SelfSignedCertificate -Subject "CN=App-M365-Licensing-Report" `
    -CertStoreLocation "Cert:\CurrentUser\My" `
    -KeyExportPolicy Exportable `
    -KeySpec Signature `
    -KeyLength 2048 `
    -KeyAlgorithm RSA `
    -HashAlgorithm SHA256 `
    -NotAfter (Get-Date).AddYears(2)

# Export the public key (.cer) to upload to Entra
Export-Certificate -Cert $cert -FilePath ".\AppLicensingReport.cer"

Write-Host "Certificate ready! Upload the .cer file to your App Registration." -ForegroundColor Green

Upload that .cer file under Certificates & secrets in the Azure portal.


Step 4: Rewrite the Script Authentication

Now update your scheduled task or background script to authenticate using the certificate thumbprint instead of interactive credentials or stored passwords:

$TenantId       = "YOUR_TENANT_ID"
$AppId          = "YOUR_CLIENT_ID"
$CertThumbprint = "YOUR_CERT_THUMBPRINT"

# Connect silently via certificate
Connect-MgGraph -TenantId $TenantId -AppId $AppId -CertificateThumbprint $CertThumbprint

Write-Host "Authenticated cleanly via Workload Identity certificate." -ForegroundColor Green

# Run your core automation logic safely
Get-MgUser -Top 5 | Select-Object DisplayName, UserPrincipalName

Step 5: The Safe Decommissioning Process

Don’t just delete the old user account right away—something will inevitably break if you missed a hidden script.

  1. Disable it first: Set AccountEnabled = $false on the old user object.
  2. Watch the logs: Give it a week or two. If nobody complains and your logs are clean, you’re safe.
  3. Kill active sessions: Revoke tokens just to be clean:
    Revoke-MgUserSignInSession -UserId "svc_licensing@yourdomain.com"

Tidying this up takes a little upfront effort, but replacing fragile user service accounts with proper Workload Identities lets you sleep a lot better when audit time rolls around.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top