This post explains how to implement the NIST SP 800-171 Rev.2 / CMMC 2.0 Level 2 control IA.L2-3.5.6 by configuring Active Directory (on-prem) and Azure AD to detect and disable user identifiers after a defined period of inactivity, with practical scripts, automation options, and operational guidance for small businesses.
Why this control matters and how to set your policy
IA.L2-3.5.6 requires organizations to disable identifiers (accounts) after a period of inactivity. The standard does not mandate a single number of days — you must document a policy that balances security and business needs (common thresholds are 30, 60, or 90 days). A small business with limited IT staff often chooses 90 days as a reasonable starting point; document that choice and the exception process in your Identity & Access Management (IAM) policy. The policy should define: inactivity threshold, notification cadence, who can approve exceptions, handling for service accounts, and retention steps (e.g., disable → move to retention OU → eventual deletion).
Active Directory (on-prem) — practical implementation
Windows Server Active Directory does not provide a built-in GPO toggle to "auto-disable" accounts after X days of inactivity. The standard approach is a scheduled script (runbook) that queries user last-logon data, notifies owners, and disables matching accounts. Use Get-ADUser with the LastLogonDate property (which is convenient because it's already converted to DateTime). Typical architecture: a management server (or domain-joined automation host) with the ActiveDirectory PowerShell module, a scheduled task running weekly, and an audit trail stored to a central share or SIEM.
# Example: disable AD users inactive 90+ days (run as account with AD privileges)
Import-Module ActiveDirectory
$cutoff = (Get-Date).AddDays(-90)
# Query enabled user accounts that have a last logon date and are older than cutoff
Get-ADUser -Filter {Enabled -eq $true} -Properties LastLogonDate,Description |
Where-Object { $_.LastLogonDate -and ($_.LastLogonDate -lt $cutoff) } |
ForEach-Object {
# Optionally exclude service accounts by description or attribute
if (-not ($_.Description -match 'ServiceAccount|DoNotDisable')) {
# Notify owner (implement email here) before disabling
# Disable and move to a "Disabled" OU for retention/audit
Disable-ADAccount -Identity $_.SamAccountName
Move-ADObject -Identity $_.DistinguishedName -TargetPath "OU=DisabledAccounts,DC=contoso,DC=com"
# Log action
"$((Get-Date).ToString()) Disabled account: $($_.SamAccountName)" | Out-File C:\Logs\AccountDisable.log -Append
}
}
Implementation notes: LastLogonDate is populated from lastLogonTimestamp (replicated) which has a ~14-day potential replication skew — that is generally acceptable for inactivity checks. For absolute precision (rarely required), query lastLogon across all domain controllers and take the most recent. Test on a staging OU first. Maintain an "exemption list" attribute (e.g., Description contains "DoNotDisable") for service accounts or special users to avoid accidental disruption.
Azure AD — practical implementation and automation
Azure AD does not have an out-of-the-box "disable after inactivity" toggle either, but Microsoft Graph exposes sign-in activity that you can use to enact automation. Two common patterns for small businesses: a PowerShell runbook (Azure Automation or a weekly Azure Function) that queries signInActivity or the SignIn logs and then disables the account, or use Azure AD Identity Governance (Access Reviews / Lifecycle workflows) for managed user lifecycle when available in your license. SignInActivity (lastSignInDateTime) is the recommended single-property approach if present in your tenant; otherwise, query the audit sign-in logs and infer inactivity.
# Example using Microsoft Graph PowerShell (Microsoft.Graph module)
# Pre-requisite: Install-Module Microsoft.Graph -Scope CurrentUser
Connect-MgGraph -Scopes "AuditLog.Read.All","User.ReadWrite.All"
$cutoff = (Get-Date).AddDays(-90)
# Get users including signInActivity (requires proper permissions and tenant support)
$users = Get-MgUser -All -Select "id,displayName,userPrincipalName,signInActivity"
foreach ($u in $users) {
$last = $u.SignInActivity.LastSignInDateTime
if ($last -and ([DateTime]$last -lt $cutoff)) {
# optional: skip guests or service accounts by checking userType or extensionAttribute
if ($u.UserPrincipalName -notmatch 'svc|service|admin-exempt') {
Update-MgUser -UserId $u.Id -BodyParameter @{ accountEnabled = $false }
# Log and notify owner
"$((Get-Date).ToString()) Disabled Azure AD user: $($u.UserPrincipalName)" | Out-File C:\Logs\AADDisable.log -Append
}
}
}
Permissions and caveats: the signInActivity property and audit log queries require AuditLog.Read.All and appropriate Graph permissions (application or delegated). The sign-in logs are retained per your tenant's settings — ensure log retention meets your evidence requirements. If signInActivity isn't available, use Get-MgAuditLogSignIn (or Graph API /auditLogs/signIns) to collect the most recent sign-ins per user; but note this approach is heavier and may require pagination and rate-limit handling.
Exception handling, service accounts, and HR integration
Practical deployments require exceptions: service accounts, shared accounts, and onboarding accounts. Maintain an allowlist (by attribute or an "ExemptFromInactivity" custom attribute). Integrate your process with HR—disable should be triggered immediately on termination, not only by inactivity. For small businesses: tie automation into the HR offboarding checklist so that the first step is manual disable on termination and the inactivity automation is a safety net for forgotten, orphaned accounts.
Risk of not implementing and monitoring recommendations
Failing to disable inactive identifiers increases attack surface and persistence risk. Stale accounts are frequently targeted for credential stuffing, lateral movement, and privilege escalation. Noncompliance also risks audit findings under NIST/CMMC. To be auditable, keep logs of detection, notifications, disables, and deletions; retain log files and runbook outputs for the period specified by your compliance policy. Add SIEM alerts for: mass re-enables, disabled accounts attempting sign-in, and accounts disabled by automation (so SOC can triage false positives).
Compliance tips and best practices: codify your inactivity threshold in policy, implement a staged workflow (notify → disable → delete) with configurable delays, use role-based access controls for the automation runbook, require multi-approval for deleting accounts, and run regular access reviews. Consider complementing this control with Azure AD Access Reviews, Privileged Identity Management (PIM) for admin accounts, and conditional access policies to reduce risk for idle accounts before disabling. Always test on a set of pilot accounts and keep an easy recovery path (re-enable + password reset) for false positives.
In summary, meeting IA.L2-3.5.6 is achievable with scripted automation: use Get-ADUser + scheduled tasks for on-prem AD, and Microsoft Graph / Azure Automation for Azure AD; document thresholds and exceptions, integrate with HR, and preserve audit logs. For small businesses, a 90-day inactivity threshold with a 7-day notification window before disable, plus a 30-day retention period in a "Disabled" OU is a pragmatic starting point—adjust based on risk and contractual requirements. Implement monitoring and an exception process, and you will reduce attack surface while providing clear evidence for audits.