Automated employee offboarding using a PowerShell script handles Active Directory account disabling, Microsoft 365 suspension, group membership removal, email forwarding, and audit logging in minutes — ensuring nothing gets missed. This matters because 59% of companies have experienced data breaches linked to poor offboarding practices, and every hour an account remains active after departure is a potential cybersecurity essentials vulnerability.
When an employee leaves your company — whether they resign, get laid off, or are terminated — you have a narrow window to secure everything they had access to. And that window is smaller than you think. According to a 2025 Beyond Identity study, 59% of companies have experienced a data breach linked to poor employee offboarding practices. Not because the departing employee was malicious — though some are — but because nobody remembered to revoke their VPN access, or disable their email, or remove them from the shared QuickBooks account. The access just sat there, like an unlocked door nobody noticed.
The manual approach to offboarding is a checklist on a piece of paper. Someone in HR sends a request to IT, IT works through the list, things get missed because it is Tuesday and there are three other fires burning. The departing employee’s Microsoft 365 account stays active for two weeks. Their VPN credentials still work. Their access to the company Dropbox never gets revoked. Each one of those is a potential breach — and a compliance violation.
We can fix this. In this guide, we are building a PowerShell-based offboarding automation system that handles Active Directory, Microsoft 365, group memberships, email forwarding, and audit logging — all in one script, all with timestamps, all with evidence your auditor can review.
The Offboarding Security Checklist
Before we automate anything, let us establish what a complete offboarding process looks like. This is the checklist — everything that needs to happen when someone leaves: We cover this in more detail in What Happens When a Small Business Gets Hacked (Real Florida Examples).
Immediate (Within 1 Hour of Departure)
- [ ] Disable Active Directory account
- [ ] Reset password to random complex value
- [ ] Disable Microsoft 365 / Google Workspace account
- [ ] Revoke VPN access
- [ ] Revoke MFA tokens and sessions
- [ ] Disable remote desktop access
- [ ] Terminate active sessions (sign out everywhere)
- [ ] Lock physical access (badges, keys, alarm codes)
Within 24 Hours
- [ ] Remove from all security groups
- [ ] Remove from distribution lists
- [ ] Set email auto-reply (if appropriate)
- [ ] Forward email to manager or designated person
- [ ] Remove from shared mailboxes
- [ ] Revoke access to SaaS applications (Slack, Zoom, QuickBooks, etc.)
- [ ] Transfer ownership of shared files and folders
- [ ] Remove from shared calendars
- [ ] Revoke API keys and service account access
- [ ] Disable SSO app assignments
Within 1 Week
- [ ] Collect company hardware (laptop, phone, monitor, peripherals)
- [ ] Wipe mobile device if MDM-enrolled
- [ ] Archive user mailbox for retention
- [ ] Transfer OneDrive/Google Drive files to manager
- [ ] Document any shared passwords that need changing
- [ ] Update shared account credentials they knew
- [ ] Remove from vendor/partner portal access
- [ ] Cancel any software licenses assigned to them
Within 30 Days
- [ ] Delete or archive Active Directory account
- [ ] Remove from any remaining systems
- [ ] Complete offboarding audit report
- [ ] File offboarding documentation for compliance
That is 30+ individual actions. Doing them manually means someone needs a checklist, needs to remember every system, and needs to actually do it — all while handling their regular workload. Something always gets missed. Let us automate the critical parts.
The Automated Offboarding Script
This PowerShell script handles the heavy lifting — all the Active Directory and Microsoft 365 tasks that are both critical and repetitive. It runs in about 2-3 minutes per employee and generates a full audit log.
<#
.SYNOPSIS
Automated Employee Offboarding Script
.DESCRIPTION
Securely offboards an employee by disabling accounts, revoking access,
forwarding email, and generating an audit trail. Handles both on-premises
Active Directory and Microsoft 365.
.PARAMETER Username
The Active Directory sAMAccountName of the departing employee.
.PARAMETER ManagerUsername
The sAMAccountName of the employee's manager (for email forwarding
and file transfer).
.PARAMETER Reason
Reason for offboarding: Resignation, Termination, Layoff, Contract End
.PARAMETER SkipM365
Switch to skip Microsoft 365 offboarding (if not using M365).
.EXAMPLE
.\Offboard-Employee.ps1 -Username "jsmith" -ManagerUsername "mjones" -Reason "Resignation"
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string]$Username,
[Parameter(Mandatory)]
[string]$ManagerUsername,
[Parameter(Mandatory)]
[ValidateSet("Resignation", "Termination", "Layoff", "ContractEnd")]
[string]$Reason,
[switch]$SkipM365
)
# Configuration
$DisabledOU = "OU=Disabled Users,DC=yourdomain,DC=com"
$LogPath = "C:\IT\OffboardingLogs"
$Timestamp = Get-Date -Format "yyyy-MM-dd_HHmmss"
$LogFile = Join-Path $LogPath "Offboard_${Username}_${Timestamp}.log"
# Create log directory
New-Item -ItemType Directory -Path $LogPath -Force | Out-Null
# Logging Function
function Write-OffboardLog {
param(
[string]$Message,
[string]$Status = "INFO"
)
$Entry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [$Status] $Message"
Add-Content -Path $LogFile -Value $Entry
switch ($Status) {
"SUCCESS" { Write-Host " $Message" -ForegroundColor Green }
"ERROR" { Write-Host " $Message" -ForegroundColor Red }
"WARNING" { Write-Host " $Message" -ForegroundColor Yellow }
"SKIP" { Write-Host " → $Message" -ForegroundColor DarkGray }
default { Write-Host " ℹ $Message" -ForegroundColor Cyan }
}
}
# Pre-flight Checks
Write-Host "`n" -ForegroundColor Cyan
Write-Host " EMPLOYEE OFFBOARDING — SECURITY PROCESS " -ForegroundColor Cyan
Write-Host "`n" -ForegroundColor Cyan
Write-OffboardLog "Offboarding initiated for: $Username"
Write-OffboardLog "Reason: $Reason"
Write-OffboardLog "Manager: $ManagerUsername"
Write-OffboardLog "Initiated by: $($env:USERNAME)"
# Verify the user exists
try {
$User = Get-ADUser -Identity $Username -Properties `
MemberOf, Manager, EmailAddress, Department, Title,
LastLogonDate, DistinguishedName, Description
Write-OffboardLog "User found: $($User.Name) ($($User.EmailAddress))" "SUCCESS"
Write-OffboardLog "Department: $($User.Department) | Title: $($User.Title)"
}
catch {
Write-OffboardLog "User '$Username' not found in Active Directory" "ERROR"
exit 1
}
# Verify the manager exists
try {
$Manager = Get-ADUser -Identity $ManagerUsername -Properties EmailAddress
Write-OffboardLog "Manager verified: $($Manager.Name)" "SUCCESS"
}
catch {
Write-OffboardLog "Manager '$ManagerUsername' not found" "ERROR"
exit 1
}
# Phase 1: Active Directory Lockdown
Write-Host "`n Phase 1: Active Directory `n" -ForegroundColor Yellow
# 1a. Disable the AD account
try {
Disable-ADAccount -Identity $Username
Write-OffboardLog "AD account disabled" "SUCCESS"
}
catch {
Write-OffboardLog "Failed to disable AD account: $($_.Exception.Message)" "ERROR"
}
# 1b. Reset password to random complex value
try {
$RandomPW = -join ((33..126) | Get-Random -Count 32 |
ForEach-Object { [char]$_ })
$SecurePW = ConvertTo-SecureString $RandomPW -AsPlainText -Force
Set-ADAccountPassword -Identity $Username -NewPassword $SecurePW -Reset
Write-OffboardLog "Password reset to random 32-character value" "SUCCESS"
# Do NOT log the actual password
}
catch {
Write-OffboardLog "Failed to reset password: $($_.Exception.Message)" "ERROR"
}
# 1c. Set account expiration to today
try {
Set-ADUser -Identity $Username -AccountExpirationDate (Get-Date)
Write-OffboardLog "Account expiration set to today" "SUCCESS"
}
catch {
Write-OffboardLog "Failed to set account expiration: $($_.Exception.Message)" "ERROR"
}
# 1d. Update description with offboarding info
try {
$OffboardNote = "OFFBOARDED: $Reason on $(Get-Date -Format 'yyyy-MM-dd') by $($env:USERNAME)"
Set-ADUser -Identity $Username -Description $OffboardNote
Write-OffboardLog "Description updated with offboarding note" "SUCCESS"
}
catch {
Write-OffboardLog "Failed to update description: $($_.Exception.Message)" "ERROR"
}
# 1e. Record and remove all group memberships
try {
$Groups = Get-ADPrincipalGroupMembership -Identity $Username |
Where-Object { $_.Name -ne "Domain Users" }
Write-OffboardLog "Group memberships found: $($Groups.Count)"
# Save group list for records
$GroupList = $Groups | Select-Object Name, GroupScope, GroupCategory
$GroupsFile = Join-Path $LogPath "Groups_${Username}_${Timestamp}.csv"
$GroupList | Export-Csv -Path $GroupsFile -NoTypeInformation
Write-OffboardLog "Group list saved to: $GroupsFile" "SUCCESS"
# Remove from all groups
foreach ($Group in $Groups) {
try {
Remove-ADGroupMember -Identity $Group -Members $Username `
-Confirm:$false
Write-OffboardLog "Removed from group: $($Group.Name)" "SUCCESS"
}
catch {
Write-OffboardLog "Failed to remove from $($Group.Name): $($_.Exception.Message)" "WARNING"
}
}
}
catch {
Write-OffboardLog "Failed to process group memberships: $($_.Exception.Message)" "ERROR"
}
# 1f. Move account to Disabled Users OU
try {
if (Get-ADOrganizationalUnit -Filter "DistinguishedName -eq '$DisabledOU'" `
-ErrorAction SilentlyContinue) {
Move-ADObject -Identity $User.DistinguishedName -TargetPath $DisabledOU
Write-OffboardLog "Account moved to Disabled Users OU" "SUCCESS"
}
else {
Write-OffboardLog "Disabled Users OU not found — skipping move" "WARNING"
}
}
catch {
Write-OffboardLog "Failed to move account: $($_.Exception.Message)" "ERROR"
}
# Phase 2: Microsoft 365
if (-not $SkipM365) {
Write-Host "`n Phase 2: Microsoft 365 `n" -ForegroundColor Yellow
# Connect to Exchange Online and Microsoft Graph
try {
if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) {
Write-OffboardLog "ExchangeOnlineManagement module not installed" "ERROR"
}
else {
Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
Write-OffboardLog "Connected to Exchange Online" "SUCCESS"
}
}
catch {
Write-OffboardLog "Failed to connect to Exchange Online: $($_.Exception.Message)" "ERROR"
}
# 2a. Block sign-in for M365
try {
Connect-MgGraph -Scopes "User.ReadWrite.All" -ErrorAction Stop
$MgUser = Get-MgUser -Filter "userPrincipalName eq '$($User.UserPrincipalName)'" `
-ErrorAction Stop
Update-MgUser -UserId $MgUser.Id -AccountEnabled:$false
Write-OffboardLog "M365 sign-in blocked" "SUCCESS"
}
catch {
Write-OffboardLog "Failed to block M365 sign-in: $($_.Exception.Message)" "ERROR"
}
# 2b. Revoke all active sessions
try {
Revoke-MgUserSignInSession -UserId $MgUser.Id
Write-OffboardLog "All active M365 sessions revoked" "SUCCESS"
}
catch {
Write-OffboardLog "Failed to revoke sessions: $($_.Exception.Message)" "ERROR"
}
# 2c. Remove M365 licenses (saves money)
try {
$Licenses = Get-MgUserLicenseDetail -UserId $MgUser.Id
if ($Licenses) {
$LicenseList = $Licenses | ForEach-Object { $_.SkuId }
Set-MgUserLicense -UserId $MgUser.Id `
-RemoveLicenses $LicenseList -AddLicenses @()
Write-OffboardLog "Removed $($Licenses.Count) M365 license(s)" "SUCCESS"
foreach ($Lic in $Licenses) {
Write-OffboardLog " License removed: $($Lic.SkuPartNumber)" "SUCCESS"
}
}
else {
Write-OffboardLog "No M365 licenses to remove" "SKIP"
}
}
catch {
Write-OffboardLog "Failed to remove licenses: $($_.Exception.Message)" "ERROR"
}
# 2d. Set email forwarding to manager
try {
Set-Mailbox -Identity $User.UserPrincipalName `
-ForwardingAddress $Manager.UserPrincipalName `
-DeliverToMailboxAndForward $false
Write-OffboardLog "Email forwarding set to $($Manager.Name)" "SUCCESS"
}
catch {
Write-OffboardLog "Failed to set email forwarding: $($_.Exception.Message)" "ERROR"
}
# 2e. Set out-of-office auto-reply
try {
$AutoReply = "Thank you for your email. $($User.GivenName) " +
"$($User.Surname) is no longer with the company. " +
"Please direct your inquiries to $($Manager.Name) at " +
"$($Manager.EmailAddress)."
Set-MailboxAutoReplyConfiguration `
-Identity $User.UserPrincipalName `
-AutoReplyState Enabled `
-InternalMessage $AutoReply `
-ExternalMessage $AutoReply
Write-OffboardLog "Auto-reply configured" "SUCCESS"
}
catch {
Write-OffboardLog "Failed to set auto-reply: $($_.Exception.Message)" "ERROR"
}
# 2f. Remove from shared mailboxes
try {
$SharedMailboxes = Get-Mailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited
foreach ($SharedMB in $SharedMailboxes) {
$Perms = Get-MailboxPermission -Identity $SharedMB.Identity |
Where-Object { $_.User -like "*$Username*" }
if ($Perms) {
Remove-MailboxPermission -Identity $SharedMB.Identity `
-User $User.UserPrincipalName `
-AccessRights FullAccess -Confirm:$false
Write-OffboardLog "Removed from shared mailbox: $($SharedMB.DisplayName)" "SUCCESS"
}
}
}
catch {
Write-OffboardLog "Failed to check shared mailboxes: $($_.Exception.Message)" "WARNING"
}
# 2g. Convert mailbox to shared (preserves data without license)
try {
Set-Mailbox -Identity $User.UserPrincipalName -Type Shared
Write-OffboardLog "Mailbox converted to Shared (no license needed)" "SUCCESS"
}
catch {
Write-OffboardLog "Failed to convert mailbox: $($_.Exception.Message)" "ERROR"
}
# Disconnect Exchange Online
Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
}
else {
Write-OffboardLog "Microsoft 365 offboarding skipped (-SkipM365 flag)" "SKIP"
}
# Phase 3: Generate Audit Report
Write-Host "`n Phase 3: Audit Report `n" -ForegroundColor Yellow
$AuditReport = @"
EMPLOYEE OFFBOARDING — AUDIT REPORT
Employee: $($User.Name) ($Username)
Email: $($User.EmailAddress)
Department: $($User.Department)
Title: $($User.Title)
Last Logon: $($User.LastLogonDate)
Offboarding Details:
Reason: $Reason
Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
Initiated by: $($env:USERNAME)
Manager: $($Manager.Name) ($ManagerUsername)
Actions Taken:
AD account disabled
Password reset to random value
Account expiration set
Removed from $($Groups.Count) security/distribution groups
Account moved to Disabled Users OU
$(if (-not $SkipM365) { @"
M365 sign-in blocked
All active sessions revoked
M365 licenses removed
Email forwarding to manager
Auto-reply configured
Shared mailbox permissions removed
Mailbox converted to Shared type
"@ })
Files Generated:
Log: $LogFile
Groups: $GroupsFile
Remaining Manual Steps:
Collect physical hardware (laptop, phone, badge)
Wipe MDM-enrolled mobile device
Revoke SaaS app access (Slack, Zoom, QuickBooks, etc.)
Change shared passwords the employee knew
Notify relevant teams of departure
Update vendor/partner portal access
Transfer OneDrive files to manager (if needed)
"@
# Save audit report
$AuditFile = Join-Path $LogPath "AuditReport_${Username}_${Timestamp}.txt"
$AuditReport | Out-File -FilePath $AuditFile -Encoding UTF8
Write-OffboardLog "Audit report saved to: $AuditFile" "SUCCESS"
# Display the report
Write-Host $AuditReport -ForegroundColor White
# Summary
$ErrorCount = (Get-Content $LogFile | Select-String "\[ERROR\]").Count
$SuccessCount = (Get-Content $LogFile | Select-String "\[SUCCESS\]").Count
Write-Host "" -ForegroundColor Cyan
Write-Host " OFFBOARDING COMPLETE " -ForegroundColor Cyan
Write-Host "" -ForegroundColor Cyan
Write-Host " Successful actions: $SuccessCount" -ForegroundColor Green
Write-Host " Errors: $ErrorCount" -ForegroundColor $(if ($ErrorCount -gt 0) { "Red" } else { "Green" })
Write-Host " Full log: $LogFile"
Write-Host " Audit report: $AuditFile"
Write-Host ""
if ($ErrorCount -gt 0) {
Write-Host " Review errors in the log file before closing this case." `
-ForegroundColor Yellow
}
Let me walk through what this script does, because each step has a specific security reason.
Phase 1 locks down Active Directory. Disabling the account blocks domain authentication immediately — no more logging into workstations, VPN, or any AD-integrated system. Resetting the password to a random 32-character string is a belt-and-suspenders measure: even if someone re-enables the account, the old password will not work. Setting account expiration to today prevents accidental re-enablement. Recording and removing all group memberships is crucial — this is how you remove access to file shares, printers, applications, and everything else controlled by AD groups. We save the group list to CSV first because you may need to restore it if the employee returns.
Phase 2 handles Microsoft 365. Blocking sign-in prevents web access to Outlook, Teams, SharePoint, and OneDrive. Revoking sessions terminates any currently active connections — if they are logged in right now, they get kicked out. Removing licenses saves money (M365 licenses are not cheap) while converting the mailbox to “Shared” preserves the data for compliance without consuming a license. Email forwarding ensures nothing falls through the cracks, and the auto-reply lets external contacts know who to reach instead.
Phase 3 generates an audit trail that your compliance officer (or auditor) can file. It documents exactly what was done, when, and by whom. The remaining manual steps section is a deliberate reminder — some things genuinely cannot be automated, and the script is honest about that.
Running the Script
Usage is straightforward:
# Basic offboarding
.\Offboard-Employee.ps1 -Username "jsmith" -ManagerUsername "mjones" -Reason "Resignation"
# Offboarding without M365 (on-premises only)
.\Offboard-Employee.ps1 -Username "jsmith" -ManagerUsername "mjones" -Reason "Termination" -SkipM365
# Preview what would happen (WhatIf mode)
.\Offboard-Employee.ps1 -Username "jsmith" -ManagerUsername "mjones" -Reason "Layoff" -WhatIf
The -WhatIf flag is valuable — it shows you everything the script would do without actually doing it. Use it the first time to verify the script targets the right user.
Handling SaaS Applications
The script handles AD and M365, but most businesses use a dozen or more SaaS applications. Here is a secondary script that documents and assists with SaaS offboarding:
<#
.SYNOPSIS
SaaS Application Access Revocation Tracker
.DESCRIPTION
Generates a checklist of all SaaS applications the departing
employee may have access to, checks SSO-integrated apps
automatically, and tracks manual revocation for others.
#>
param(
[Parameter(Mandatory)]
[string]$Username,
[string]$OutputPath = "C:\IT\OffboardingLogs"
)
# Define your SaaS applications
# Mark 'sso' = $true if they use your Azure AD / Okta SSO
$SaaSApps = @(
@{ Name = "Slack"; SSO = $true; AdminURL = "https://yourteam.slack.com/admin" }
@{ Name = "Zoom"; SSO = $true; AdminURL = "https://zoom.us/account/user" }
@{ Name = "QuickBooks"; SSO = $false; AdminURL = "https://app.qbo.intuit.com" }
@{ Name = "Dropbox"; SSO = $true; AdminURL = "https://www.dropbox.com/team/admin" }
@{ Name = "Salesforce"; SSO = $true; AdminURL = "https://yourorg.salesforce.com" }
@{ Name = "GitHub"; SSO = $true; AdminURL = "https://github.com/orgs/yourorg/people" }
@{ Name = "HubSpot"; SSO = $false; AdminURL = "https://app.hubspot.com/settings" }
@{ Name = "Canva"; SSO = $false; AdminURL = "https://www.canva.com/teams" }
@{ Name = "Asana"; SSO = $true; AdminURL = "https://app.asana.com/admin" }
@{ Name = "Adobe CC"; SSO = $false; AdminURL = "https://adminconsole.adobe.com" }
)
$Timestamp = Get-Date -Format "yyyy-MM-dd_HHmmss"
$Results = @()
Write-Host "`n SaaS Access Revocation Checklist `n" -ForegroundColor Yellow
Write-Host "Employee: $Username`n"
foreach ($App in $SaaSApps) {
$Result = [PSCustomObject]@{
Application = $App.Name
SSO_Integrated = $App.SSO
Auto_Revoked = $false
Manual_Required = $false
AdminURL = $App.AdminURL
Status = "Pending"
Notes = ""
}
if ($App.SSO) {
# SSO-integrated apps: access is revoked when we disabled the AD account
$Result.Auto_Revoked = $true
$Result.Status = "Auto-revoked via SSO"
Write-Host " $($App.Name): Auto-revoked (SSO)" -ForegroundColor Green
}
else {
# Non-SSO apps require manual revocation
$Result.Manual_Required = $true
$Result.Status = "MANUAL ACTION REQUIRED"
$Result.Notes = "Log in to $($App.AdminURL) and remove user"
Write-Host " $($App.Name): MANUAL REVOCATION NEEDED" -ForegroundColor Yellow
Write-Host " → $($App.AdminURL)" -ForegroundColor DarkGray
}
$Results += $Result
}
# Export checklist
$ChecklistFile = Join-Path $OutputPath "SaaS_Revocation_${Username}_${Timestamp}.csv"
$Results | Export-Csv -Path $ChecklistFile -NoTypeInformation
$ManualCount = ($Results | Where-Object { $_.Manual_Required }).Count
$AutoCount = ($Results | Where-Object { $_.Auto_Revoked }).Count
Write-Host "`n Summary:" -ForegroundColor Cyan
Write-Host " Auto-revoked (SSO): $AutoCount" -ForegroundColor Green
Write-Host " Manual required: $ManualCount" -ForegroundColor Yellow
Write-Host " Checklist saved: $ChecklistFile`n"
This highlights one of the biggest benefits of Single Sign-On (SSO). When all your applications authenticate through Azure AD or Okta, disabling the user’s account in AD automatically locks them out of every SSO-integrated application. The applications that use separate credentials — those are the ones where access lingers, which is exactly why we track and flag them.
If you are still paying for individual logins to each SaaS tool, this is your motivation to consolidate. Every application that uses SSO is one less application you need to remember during offboarding. For New Smyrna Beach businesses evaluating their software stack, SSO integration should be a selection criterion, not an afterthought.
The Timing Problem: When to Pull the Trigger
One of the trickiest parts of offboarding is timing. Run the script too early and you disrupt the employee before they have finished their notice period. Run it too late and they have had time to download client lists or forward sensitive emails.
Here is the general guidance:
Voluntary resignation (2-week notice): Run the script at end of business on their last day. Set the M365 portions (email forwarding, auto-reply) to activate at a scheduled time using a delayed task.
Involuntary termination: Run the script during or immediately after the termination meeting. This is non-negotiable. While HR is having the conversation, IT should be executing the offboarding script. Every minute of delay is a risk.
Layoff (group): Pre-stage the script for each affected employee. Use a CSV-driven batch version:
# Batch offboarding from CSV
# CSV format: Username, ManagerUsername, Reason
$Employees = Import-Csv "C:\IT\layoff_list.csv"
foreach ($Emp in $Employees) {
Write-Host "`n========== Offboarding: $($Emp.Username) ==========" `
-ForegroundColor Cyan
& .\Offboard-Employee.ps1 `
-Username $Emp.Username `
-ManagerUsername $Emp.ManagerUsername `
-Reason $Emp.Reason
# Brief pause between accounts to avoid throttling
Start-Sleep -Seconds 5
}
Write-Host "`n========== ALL OFFBOARDING COMPLETE ==========" `
-ForegroundColor Green
Contract end (known date): Schedule the script to run automatically at a specific date and time using Task Scheduler. Set the AD account expiration date in advance, and schedule the full offboarding script for the morning after contract end.
Compliance Requirements
Different compliance frameworks have specific requirements around access revocation. Here is what the major ones say:
| Framework | Requirement | Timeframe |
|---|---|---|
| PCI DSS 4.0 (8.1.4) | Disable accounts for terminated users | Immediately |
| HIPAA (164.312(a)(1)) | Terminate electronic access of former workforce members | Immediately upon departure |
| SOC 2 (CC6.1) | Remove access upon termination | Within 24 hours |
| NIST 800-53 (PS-4) | Disable access upon termination | Same day |
| FTC Safeguards | Revoke access to customer financial information | Upon separation |
| CMMC 2.0 (AC.L1-3.1.1) | Limit system access to authorized users | Immediately |
Notice the theme: every framework says “immediately” or “same day.” None of them say “whenever IT gets around to it.” The automated script gets you to compliance-level speed — under 5 minutes from decision to execution.
Audit Evidence and Reporting
The script generates three files per offboarding event:
- Detailed log (
Offboard_jsmith_2026-03-20_143022.log): Every action with timestamps — this is your primary audit evidence - Group membership export (
Groups_jsmith_2026-03-20_143022.csv): What groups they were in before removal — needed for access review audits - Audit report (
AuditReport_jsmith_2026-03-20_143022.txt): Human-readable summary with manual action reminders
For compliance purposes, keep these files for at least as long as your retention policy requires — typically 3-7 years depending on your industry. Store them somewhere the departing employee never had access to (obviously).
Here is a script that generates a monthly offboarding summary report for management:
# Generate monthly offboarding summary
$LogPath = "C:\IT\OffboardingLogs"
$Month = (Get-Date).AddMonths(-1).ToString("yyyy-MM")
$Logs = Get-ChildItem -Path $LogPath -Filter "AuditReport_*" |
Where-Object { $_.LastWriteTime.ToString("yyyy-MM") -eq $Month }
Write-Host "`n" -ForegroundColor Cyan
Write-Host " Monthly Offboarding Summary: $Month" -ForegroundColor Cyan
Write-Host "" -ForegroundColor Cyan
Write-Host " Total offboardings: $($Logs.Count)"
$Logs | ForEach-Object {
$Content = Get-Content $_.FullName
$NameLine = $Content | Select-String "Employee:" | Select-Object -First 1
$ReasonLine = $Content | Select-String "Reason:" | Select-Object -First 1
Write-Host " - $($NameLine.ToString().Trim()) | $($ReasonLine.ToString().Trim())"
}
# Check for any accounts that should have been offboarded but were not
$DisabledUsers = Get-ADUser -Filter {Enabled -eq $false} `
-SearchBase "OU=Disabled Users,DC=yourdomain,DC=com" `
-Properties WhenChanged |
Where-Object { $_.WhenChanged.ToString("yyyy-MM") -eq $Month }
Write-Host "`n Accounts in Disabled OU from $Month`: $($DisabledUsers.Count)"
Write-Host "`n" -ForegroundColor Cyan
Preventing Common Offboarding Failures
Based on the offboarding incidents we see across Volusia County businesses, here are the most common failures and how to prevent them:
Shared account passwords. The departing employee knew the password to the company’s social media accounts, the shared admin login for the website, or the generic “[email protected]” email. The automated script cannot change these — you need a process to identify and rotate shared credentials. This is also why you should minimize shared accounts in the first place (see our password management guide).
Personal device access. If the employee was reading email on their personal phone and you do not have MDM, disabling their account stops new mail but does not wipe cached data. For businesses handling sensitive data, MDM enrollment should be a condition of using personal devices for work.
Knowledge transfer gaps. The script handles technical access, but if the employee was the only person who knew how the billing integration worked or where the vendor contracts were stored, that knowledge walks out the door with them. Build knowledge transfer into your offboarding process as a standard step during the notice period.
Vendor portal access. Many business applications — insurance portals, banking platforms, vendor management systems — have their own user databases completely separate from your AD. The SaaS tracker script helps, but you need to maintain a list of every system that has its own login.
Integrating with HR Workflow
The ideal state is HR triggers the offboarding automatically. When HR updates the employee’s status in the HR system (BambooHR, Gusto, Rippling, Paylocity), that change triggers the IT offboarding script. Here is a simplified example using a scheduled task that watches for a flag: Our guide to How to Tell If Your IT Provider Is Actually Keeping You Secure walks through this in more detail.
# Scheduled task: check for pending offboardings every 15 minutes
# HR drops a JSON file in a watched folder when someone needs to be offboarded
$WatchPath = "\\FileServer\HR_Offboarding_Requests"
$ProcessedPath = "\\FileServer\HR_Offboarding_Processed"
$PendingFiles = Get-ChildItem -Path $WatchPath -Filter "*.json"
foreach ($File in $PendingFiles) {
$Request = Get-Content $File.FullName | ConvertFrom-Json
Write-Host "Processing offboarding request: $($Request.employee_name)"
# Validate the request
if ($Request.username -and $Request.manager_username -and $Request.reason) {
# Execute the offboarding script
& "C:\IT\Scripts\Offboard-Employee.ps1" `
-Username $Request.username `
-ManagerUsername $Request.manager_username `
-Reason $Request.reason
# Move to processed folder
Move-Item -Path $File.FullName `
-Destination (Join-Path $ProcessedPath $File.Name)
Write-Host " Complete. Request file moved to processed."
}
else {
Write-Host " Invalid request file: $($File.Name)" -ForegroundColor Red
}
}
HR creates a simple JSON file like this:
{
"employee_name": "Jane Smith",
"username": "jsmith",
"manager_username": "mjones",
"reason": "Resignation",
"last_day": "2026-03-28",
"requested_by": "hr-admin",
"requested_at": "2026-03-20T14:30:00"
}
This removes the “IT forgot” failure mode entirely. HR makes the decision, drops the file, and the automation handles the rest. The processed folder creates a paper trail showing when HR requested offboarding and when IT completed it — exactly the evidence auditors want.
The Bottom Line
The right technology setup saves time, reduces costs, and lets you focus on running your business instead of troubleshooting IT problems. Start with the fundamentals, implement them properly, and build from there.
FAQ
How quickly should I disable accounts when someone is terminated?
Immediately — as in, during the termination meeting or within minutes of it. PCI DSS, HIPAA, and SOC 2 all require immediate access revocation for terminated employees. The offboarding script runs in 2-3 minutes, which means the bottleneck is not technology but coordination between HR and IT. Establish a communication protocol: HR calls IT before the termination meeting starts, IT runs the script as the meeting begins.
What if the departing employee is an IT administrator?
This is the highest-risk offboarding scenario. An IT admin typically has domain admin rights, access to backup systems, knowledge of all network architecture, and possibly access to service accounts. For admin offboarding: a different admin must run the script (obviously), all service account passwords the admin knew must be rotated immediately, VPN and firewall admin access must be revoked, and you should audit all admin actions from their last 30 days. Consider having an external IT consultant handle admin offboarding for separation of duties.
Should I delete the account after offboarding or just disable it?
Disable first, delete later. Most compliance frameworks require you to retain user account records for audit purposes — typically 1-3 years. A disabled account preserves the SID (Security Identifier), group membership history, and other metadata that auditors may request. After your retention period expires, then delete. The script moves disabled accounts to a dedicated OU, making them easy to find and manage without cluttering your active directory.
What about employees who have personal files on their work computer?
Company policy should clearly state that work computers are company property and personal files should not be stored on them. In practice, people do it anyway. Allow the employee to transfer personal files under supervision before the offboarding script runs — but only if the departure is amicable. For terminations, the company’s legal obligation is typically to preserve company data, not return personal data. Check with your attorney for your specific situation.
Can I use this script for Google Workspace instead of Microsoft 365?
The Active Directory portions work regardless of your email platform. For Google Workspace, replace the M365 section with Google Admin SDK API calls using the GAM (Google Apps Manager) command-line tool. The steps are equivalent: suspend user → set email forwarding → transfer Drive files → remove from groups. GAM makes this scriptable: gam update user [email protected] suspended on.
How do I handle offboarding for contractors and temporary workers?
The same script works — just pass their AD username and the reason “ContractEnd.” For contractors, best practice is to set an account expiration date when they are hired. AD will automatically disable the account on that date, and you can run the full offboarding script afterward. This prevents the scenario where a contractor’s account stays active months after their engagement ends because nobody remembered to disable it.