Remediation Guides

AD Security Playbooks

Step-by-step remediation procedures for common Active Directory and Entra ID security findings.

Kerberoasting Remediation

High Risk
Effort: Medium
Time: 2-4 weeks
T1558.003

Attack Overview

Kerberoasting allows any authenticated user to request service tickets (TGS) for accounts with SPNs, then crack passwords offline without detection. Privileged service accounts with weak passwords are primary targets.

Remediation Steps

1

Identify All Kerberoastable Accounts

Run discovery to find all user accounts with SPNs. Prioritize by privilege level.

# Find all Kerberoastable user accounts Get-ADUser -Filter {ServicePrincipalName -like "*"} -Properties ` ServicePrincipalName, PasswordLastSet, adminCount, Enabled | Select-Object SamAccountName, @{N='SPN';E={$_.ServicePrincipalName -join '; '}}, PasswordLastSet, @{N='PasswordAgeDays';E={((Get-Date) - $_.PasswordLastSet).Days}}, @{N='IsPrivileged';E={$_.adminCount -eq 1}}, Enabled | Sort-Object IsPrivileged -Descending | Export-Csv "Kerberoastable-Accounts.csv" -NoTypeInformation
2

Rotate Passwords to 25+ Characters

Immediately rotate passwords for all Kerberoastable accounts to 25+ character random strings. This makes offline cracking computationally infeasible.

# Generate and set strong password for service account $ServiceAccount = "svc-sql-prod" # Generate 30-character random password Add-Type -AssemblyName System.Web $NewPassword = [System.Web.Security.Membership]::GeneratePassword(30, 8) $SecurePassword = ConvertTo-SecureString $NewPassword -AsPlainText -Force # Set the new password Set-ADAccountPassword -Identity $ServiceAccount -NewPassword $SecurePassword -Reset # Store password securely (use your PAM solution) Write-Host "New password for $ServiceAccount : $NewPassword" Write-Host "IMPORTANT: Store in password vault immediately!" # Document the change $ChangeLog = [PSCustomObject]@{ Account = $ServiceAccount ChangedBy = $env:USERNAME ChangeDate = Get-Date Reason = "Kerberoasting remediation" } $ChangeLog | Export-Csv "Password-Rotation-Log.csv" -Append -NoTypeInformation

Service Impact Warning

Coordinate with application owners before rotating service account passwords. Schedule changes during maintenance windows and have rollback procedures ready.

3

Convert to Group Managed Service Accounts (gMSA)

gMSAs automatically rotate passwords every 30 days with 240-character random passwords—completely eliminating Kerberoasting risk.

# Step 1: Create KDS Root Key (one-time, forest-level) # For production (wait 10 hours for replication): Add-KdsRootKey -EffectiveTime ((Get-Date).AddHours(-10)) # For lab (immediate, NOT for production): # Add-KdsRootKey -EffectiveTime ((Get-Date).AddHours(-10)) # Step 2: Create security group for computers that will use the gMSA New-ADGroup -Name "gMSA-SQLServers" ` -GroupScope Global ` -GroupCategory Security ` -Path "OU=Groups,DC=contoso,DC=com" # Add computer accounts that need to use this gMSA Add-ADGroupMember -Identity "gMSA-SQLServers" -Members "SQL01$", "SQL02$" # Step 3: Create the gMSA New-ADServiceAccount -Name "gmsa-sql-prod" ` -DNSHostName "gmsa-sql-prod.contoso.com" ` -PrincipalsAllowedToRetrieveManagedPassword "gMSA-SQLServers" ` -ServicePrincipalNames "MSSQLSvc/sql01.contoso.com:1433" ` -Description "SQL Server service account - auto-managed password" # Step 4: Install gMSA on target server (run on SQL server) Install-ADServiceAccount -Identity "gmsa-sql-prod" # Step 5: Test the gMSA Test-ADServiceAccount -Identity "gmsa-sql-prod"

gMSA Benefits

240-character auto-rotating passwords, no password management overhead, supports multiple servers, fully integrated with AD.

4

Remove Unnecessary SPNs

If an SPN is no longer needed (service decommissioned), remove it to reduce attack surface.

# List current SPNs for an account Get-ADUser -Identity "svc-legacy-app" -Properties ServicePrincipalName | Select-Object -ExpandProperty ServicePrincipalName # Remove specific SPN Set-ADUser -Identity "svc-legacy-app" -ServicePrincipalNames @{Remove="HTTP/legacyapp.contoso.com"} # Remove ALL SPNs (if service is decommissioned) Set-ADUser -Identity "svc-legacy-app" -ServicePrincipalNames $null
5

Enable AES Encryption Only

Disable RC4 encryption for Kerberos. AES-encrypted tickets are harder to crack.

# Set account to support only AES encryption Set-ADUser -Identity "svc-sql-prod" -KerberosEncryptionType AES128,AES256 # Verify the setting Get-ADUser -Identity "svc-sql-prod" -Properties msDS-SupportedEncryptionTypes | Select-Object SamAccountName, 'msDS-SupportedEncryptionTypes' # Domain-wide: Disable RC4 via Group Policy # Computer Configuration > Policies > Windows Settings > Security Settings # > Local Policies > Security Options # "Network security: Configure encryption types allowed for Kerberos" # Enable only: AES128_HMAC_SHA1, AES256_HMAC_SHA1, Future encryption types

Compatibility Check

Disabling RC4 may break legacy applications. Test thoroughly in non-production first. Monitor Event ID 4768/4769 for Kerberos failures after change.

6

Implement Detection

Monitor for Kerberoasting activity using Windows Event Logs.

# Event ID 4769: Kerberos Service Ticket Request # Indicator: High volume of TGS requests for different SPNs from single source # Filter: Ticket Encryption Type = 0x17 (RC4) when AES is expected # Create alert for suspicious TGS requests # SIEM Query (Splunk example): # index=windows EventCode=4769 Ticket_Encryption_Type=0x17 # | stats count by src_ip, Account_Name # | where count > 10

Implementation Timeline

Phase 1
Week 1
Discovery & inventory
Phase 2
Week 2
Password rotation (critical)
Phase 3
Week 3-4
gMSA migration
Phase 4
Ongoing
Detection & monitoring
Verification Checklist
All Kerberoastable accounts inventoried
Privileged service accounts rotated to 25+ char passwords
gMSA migration plan created for eligible accounts
Unnecessary SPNs removed
AES-only encryption tested and rolled out
Detection rules implemented in SIEM
Password rotation schedule established (90 days max)

AS-REP Roasting Remediation

High Risk
Effort: Low
Time: 1-2 days
T1558.004

Attack Overview

AS-REP Roasting targets accounts with "Do not require Kerberos preauthentication" enabled. Attackers can request AS-REP without knowing the password, then crack offline. This should NEVER be enabled unless absolutely required for legacy systems.

1

Identify AS-REP Roastable Accounts

# Find all accounts with pre-auth disabled Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} -Properties ` DoesNotRequirePreAuth, PasswordLastSet, Enabled, adminCount, Description | Select-Object SamAccountName, Enabled, @{N='PasswordAgeDays';E={((Get-Date) - $_.PasswordLastSet).Days}}, @{N='IsPrivileged';E={$_.adminCount -eq 1}}, Description | Export-Csv "ASREPRoastable-Accounts.csv" -NoTypeInformation # Count affected accounts $ASREPAccounts = Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true -and Enabled -eq $true} Write-Host "AS-REP Roastable accounts found: $($ASREPAccounts.Count)" -ForegroundColor Red
2

Enable Kerberos Pre-Authentication

For each identified account, enable pre-authentication. This is a simple attribute change with no service impact in most cases.

# Enable pre-auth for single account Set-ADAccountControl -Identity "vulnerable-user" -DoesNotRequirePreAuth $false # Bulk remediation for all affected accounts $ASREPAccounts = Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} foreach ($Account in $ASREPAccounts) { try { Set-ADAccountControl -Identity $Account -DoesNotRequirePreAuth $false Write-Host "[FIXED] $($Account.SamAccountName)" -ForegroundColor Green } catch { Write-Host "[ERROR] $($Account.SamAccountName): $_" -ForegroundColor Red } } # Verify fix $Remaining = Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true -and Enabled -eq $true} Write-Host "`nRemaining AS-REP Roastable: $($Remaining.Count)"

Low Risk Change

Enabling pre-authentication is generally safe. The only legitimate use case is ancient Kerberos clients that don't support pre-auth—extremely rare in modern environments.

3

Prevent Future Occurrences

Create a scheduled task or monitoring rule to detect if this setting is ever re-enabled.

# Create scheduled monitoring script $MonitorScript = @' $ASREPAccounts = Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true -and Enabled -eq $true} if ($ASREPAccounts.Count -gt 0) { # Send alert email $Body = "AS-REP Roastable accounts detected:`n" $Body += ($ASREPAccounts | Select-Object SamAccountName | Out-String) Send-MailMessage -To "security@contoso.com" -From "ad-monitor@contoso.com" ` -Subject "ALERT: AS-REP Roastable Accounts Detected" -Body $Body ` -SmtpServer "smtp.contoso.com" } '@ # Save and schedule $MonitorScript | Out-File "C:\Scripts\Monitor-ASREPRoast.ps1" # Schedule via Task Scheduler to run daily
Verification Checklist
All AS-REP Roastable accounts identified
Pre-authentication enabled on all accounts
Re-scan confirms zero AS-REP Roastable accounts
Monitoring implemented for future detection

Kerberos Delegation Remediation

Critical
Effort: High
Time: 2-4 weeks
T1558.001

Attack Overview

Unconstrained Delegation: Any user who authenticates to a server with unconstrained delegation has their TGT stored in memory. Attackers can steal these TGTs to impersonate ANY user, including Domain Admins.

Delegation TypeRisk LevelRemediation Priority
Unconstrained DelegationCRITICALImmediate
Constrained Delegation with Protocol TransitionHIGHHigh
Constrained Delegation (standard)MEDIUMReview
Resource-Based Constrained DelegationLOWModern approach
1

Identify All Delegation

# Find ALL delegation (unconstrained, constrained, RBCD) $Results = @() # Unconstrained Delegation (CRITICAL) $Unconstrained = Get-ADComputer -Filter {TrustedForDelegation -eq $true} -Properties TrustedForDelegation | Where-Object { $_.Name -notmatch "^DC" } # Exclude DCs $Unconstrained += Get-ADUser -Filter {TrustedForDelegation -eq $true} -Properties TrustedForDelegation foreach ($Item in $Unconstrained) { $Results += [PSCustomObject]@{ Name = $Item.Name Type = $Item.ObjectClass DelegationType = "Unconstrained" RiskLevel = "CRITICAL" Target = "ANY SERVICE" } } # Constrained Delegation $Constrained = Get-ADObject -Filter {msDS-AllowedToDelegateTo -like "*"} -Properties ` 'msDS-AllowedToDelegateTo', TrustedToAuthForDelegation foreach ($Item in $Constrained) { $DelegType = if ($Item.TrustedToAuthForDelegation) { "Constrained+ProtocolTransition" } else { "Constrained" } $Risk = if ($Item.TrustedToAuthForDelegation) { "HIGH" } else { "MEDIUM" } $Results += [PSCustomObject]@{ Name = $Item.Name Type = $Item.ObjectClass DelegationType = $DelegType RiskLevel = $Risk Target = ($Item.'msDS-AllowedToDelegateTo' -join "; ") } } $Results | Export-Csv "Delegation-Inventory.csv" -NoTypeInformation $Results | Format-Table -AutoSize
2

Remove Unconstrained Delegation

Convert to constrained or resource-based constrained delegation.

# Remove unconstrained delegation from computer Set-ADComputer -Identity "APPSERVER01" -TrustedForDelegation $false # Remove from user account Set-ADUser -Identity "svc-app" -TrustedForDelegation $false # Verify removal Get-ADComputer -Identity "APPSERVER01" -Properties TrustedForDelegation | Select-Object Name, TrustedForDelegation

Service Impact

Removing delegation will break applications that rely on it. Work with app owners to understand requirements and migrate to constrained delegation.

3

Implement Resource-Based Constrained Delegation (RBCD)

RBCD is the modern, more secure approach. Permission is configured on the target resource, not the source.

# Example: Allow web server to delegate to SQL server # Get the principals $WebServer = Get-ADComputer -Identity "WEBSERVER01" $SQLServer = Get-ADComputer -Identity "SQLSERVER01" # Configure RBCD on the target (SQL server) # This allows WEBSERVER01 to delegate to SQLSERVER01 Set-ADComputer -Identity $SQLServer ` -PrincipalsAllowedToDelegateToAccount $WebServer # Verify configuration Get-ADComputer -Identity $SQLServer -Properties PrincipalsAllowedToDelegateToAccount | Select-Object Name, PrincipalsAllowedToDelegateToAccount
4

Protect Sensitive Accounts

Add high-value accounts to "Protected Users" group to prevent delegation abuse.

# Add privileged users to Protected Users group # These accounts cannot be delegated, period $PrivilegedUsers = @("admin-john", "admin-sarah", "svc-critical") foreach ($User in $PrivilegedUsers) { Add-ADGroupMember -Identity "Protected Users" -Members $User Write-Host "[PROTECTED] $User added to Protected Users" -ForegroundColor Green } # Set "Account is sensitive and cannot be delegated" flag # Alternative for accounts that can't be in Protected Users Set-ADUser -Identity "svc-sensitive" -AccountNotDelegated $true
Verification Checklist
All delegation types inventoried
Unconstrained delegation removed from all non-DC systems
Applications migrated to constrained/RBCD
Privileged accounts added to Protected Users
Protocol transition reviewed and minimized

Stale Account Cleanup

Medium Risk
Effort: Medium
Time: Ongoing
1

Identify Stale Accounts

# Find accounts with no login in 90+ days $StaleDate = (Get-Date).AddDays(-90) $StaleAccounts = Get-ADUser -Filter {Enabled -eq $true} -Properties ` LastLogonDate, PasswordLastSet, Description, Manager, Department | Where-Object { $_.LastLogonDate -lt $StaleDate -or $_.LastLogonDate -eq $null } | Select-Object SamAccountName, Name, Department, LastLogonDate, PasswordLastSet, Description, @{N='Manager';E={(Get-ADUser $_.Manager -ErrorAction SilentlyContinue).Name}} $StaleAccounts | Export-Csv "Stale-Accounts.csv" -NoTypeInformation Write-Host "Stale accounts found: $($StaleAccounts.Count)"
2

Validate with Managers/HR

Send report to department managers and HR to verify employees are still active.

# Process: 1. Export stale accounts grouped by department/manager 2. Send validation requests with 2-week deadline 3. Track responses in spreadsheet 4. Escalate non-responses to HR 5. Document approval for each action
3

Disable Confirmed Stale Accounts

# Disable stale accounts (don't delete immediately) $AccountsToDisable = Import-Csv "Approved-Disable-List.csv" foreach ($Account in $AccountsToDisable) { try { # Disable the account Disable-ADAccount -Identity $Account.SamAccountName # Update description with disable reason and date $NewDesc = "DISABLED $(Get-Date -Format 'yyyy-MM-dd') - Stale account | Original: $((Get-ADUser $Account.SamAccountName -Properties Description).Description)" Set-ADUser -Identity $Account.SamAccountName -Description $NewDesc # Move to Disabled Users OU $DisabledOU = "OU=Disabled Users,DC=contoso,DC=com" Move-ADObject -Identity (Get-ADUser $Account.SamAccountName).DistinguishedName -TargetPath $DisabledOU Write-Host "[DISABLED] $($Account.SamAccountName)" -ForegroundColor Green } catch { Write-Host "[ERROR] $($Account.SamAccountName): $_" -ForegroundColor Red } } # Log the action $AccountsToDisable | Add-Member -NotePropertyName "DisabledDate" -NotePropertyValue (Get-Date) $AccountsToDisable | Export-Csv "Disabled-Accounts-Log.csv" -Append -NoTypeInformation
4

Delete After Retention Period

# Delete accounts disabled 90+ days ago $RetentionDays = 90 $DeleteDate = (Get-Date).AddDays(-$RetentionDays) $DisabledOU = "OU=Disabled Users,DC=contoso,DC=com" $ToDelete = Get-ADUser -SearchBase $DisabledOU -Filter {Enabled -eq $false} -Properties WhenChanged | Where-Object { $_.WhenChanged -lt $DeleteDate } Write-Host "Accounts to delete: $($ToDelete.Count)" foreach ($Account in $ToDelete) { # Final backup before deletion $Account | Export-Csv "Deleted-Accounts-Archive.csv" -Append -NoTypeInformation # Delete Remove-ADUser -Identity $Account -Confirm:$false Write-Host "[DELETED] $($Account.SamAccountName)" -ForegroundColor Yellow }
5

Automate Ongoing Cleanup

# Schedule weekly stale account report # Add to Task Scheduler to run every Monday $ReportScript = @' Import-Module ActiveDirectory $StaleDate = (Get-Date).AddDays(-90) $Report = Get-ADUser -Filter {Enabled -eq $true -and LastLogonDate -lt $StaleDate} -Properties LastLogonDate, Department | Select-Object SamAccountName, Department, LastLogonDate if ($Report.Count -gt 0) { $Report | Export-Csv "\\server\reports\Weekly-Stale-Accounts.csv" -NoTypeInformation Send-MailMessage -To "it-security@contoso.com" ` -From "ad-reports@contoso.com" ` -Subject "Weekly Stale Account Report - $($Report.Count) accounts" ` -Body "Please review attached stale account report." ` -Attachments "\\server\reports\Weekly-Stale-Accounts.csv" ` -SmtpServer "smtp.contoso.com" } '@ $ReportScript | Out-File "C:\Scripts\Weekly-StaleAccountReport.ps1"

Password Policy Hardening

Medium Risk
Effort: Low
Time: 1 week
SettingWeak PolicyRecommended
Minimum Length8 characters14+ characters
ComplexityEnabledEnabled + passphrases
Maximum Age90 days365 days (with MFA)
History5 passwords24 passwords
Lockout Threshold0 (disabled)10-15 attempts
Lockout DurationN/A15-30 minutes
1

Update Default Domain Policy

# Via Group Policy Management Console: 1. Open GPMC.msc 2. Edit "Default Domain Policy" 3. Navigate to: Computer Configuration > Policies > Windows Settings > Security Settings > Account Policies > Password Policy 4. Configure settings: - Minimum password length: 14 - Password must meet complexity: Enabled - Enforce password history: 24 - Maximum password age: 365 (if MFA enforced) or 90 - Minimum password age: 1 day 5. Account Lockout Policy: - Account lockout threshold: 10 invalid attempts - Account lockout duration: 30 minutes - Reset lockout counter after: 30 minutes 6. Run: gpupdate /force on DCs
2

Create Fine-Grained Policy for Privileged Accounts

# Create stricter policy for privileged users New-ADFineGrainedPasswordPolicy -Name "Privileged-Account-Policy" ` -Precedence 10 ` -MinPasswordLength 20 ` -PasswordHistoryCount 24 ` -MaxPasswordAge "90.00:00:00" ` -MinPasswordAge "1.00:00:00" ` -ComplexityEnabled $true ` -ReversibleEncryptionEnabled $false ` -LockoutThreshold 5 ` -LockoutDuration "00:30:00" ` -LockoutObservationWindow "00:30:00" # Apply to Domain Admins group Add-ADFineGrainedPasswordPolicySubject -Identity "Privileged-Account-Policy" ` -Subjects "Domain Admins", "Enterprise Admins", "Schema Admins" # Verify application Get-ADFineGrainedPasswordPolicy -Identity "Privileged-Account-Policy" | Select-Object Name, MinPasswordLength, MaxPasswordAge, LockoutThreshold
3

Implement Banned Password List

# Azure AD Password Protection (Hybrid) # Blocks common passwords and custom banned words 1. Download Azure AD Password Protection agents from Microsoft 2. Install DC Agent on all Domain Controllers 3. Install Proxy service on 2+ member servers 4. Configure in Azure AD: - Azure Portal > Azure AD > Security > Authentication Methods - Enable "Password Protection" - Add custom banned passwords (company name, products, etc.) # Benefits: - Blocks "Password123!", "Summer2024!", "CompanyName1!" - Syncs with Azure AD banned password list - No user training required - enforcement at password change

Service Account Hardening

High Risk
Effort: High
Time: 4-8 weeks
1

Inventory All Service Accounts

# Comprehensive service account discovery $ServiceAccounts = @() # Method 1: Accounts with SPNs $ServiceAccounts += Get-ADUser -Filter {ServicePrincipalName -like "*"} -Properties * # Method 2: Naming patterns $Patterns = @("svc-*", "*-svc", "service*", "sa-*", "sql*", "app-*") foreach ($Pattern in $Patterns) { $ServiceAccounts += Get-ADUser -Filter {SamAccountName -like $Pattern} -Properties * } # Method 3: PasswordNeverExpires $ServiceAccounts += Get-ADUser -Filter {PasswordNeverExpires -eq $true} -Properties * # Deduplicate and analyze $ServiceAccounts = $ServiceAccounts | Sort-Object SamAccountName -Unique $Inventory = foreach ($SA in $ServiceAccounts) { [PSCustomObject]@{ SamAccountName = $SA.SamAccountName DisplayName = $SA.DisplayName Enabled = $SA.Enabled HasSPN = [bool]$SA.ServicePrincipalName PasswordNeverExpires = $SA.PasswordNeverExpires PasswordLastSet = $SA.PasswordLastSet PasswordAgeDays = if ($SA.PasswordLastSet) { ((Get-Date) - $SA.PasswordLastSet).Days } else { "Never" } LastLogon = $SA.LastLogonDate Description = $SA.Description CanBegMSA = ($SA.ServicePrincipalName -and $SA.Enabled) # Candidate for gMSA } } $Inventory | Export-Csv "Service-Account-Inventory.csv" -NoTypeInformation
2

Document Account Usage

# For each service account, document: 1. Account Name: ________________________ 2. Owner (Application Team): ________________________ 3. Application/Service: ________________________ 4. Servers Used On: ________________________ 5. Current Password Age: ________________________ 6. Privileged Group Membership: ________________________ 7. Has SPN: Yes / No 8. Delegation Configured: Yes / No 9. Can Convert to gMSA: Yes / No 10. Business Criticality: High / Medium / Low # Store in CMDB or ServiceNow for ongoing management
3

Implement Password Rotation Policy

# For accounts that can't use gMSA, implement 90-day rotation # This script can be scheduled or integrated with your PAM solution function Rotate-ServiceAccountPassword { param( [string]$AccountName, [string]$PAMVaultPath # Path to store in CyberArk/HashiCorp Vault ) # Generate strong password Add-Type -AssemblyName System.Web $NewPassword = [System.Web.Security.Membership]::GeneratePassword(25, 5) $SecurePassword = ConvertTo-SecureString $NewPassword -AsPlainText -Force # Rotate in AD Set-ADAccountPassword -Identity $AccountName -NewPassword $SecurePassword -Reset # Update PAM vault # (Insert your PAM integration code here) # Log the rotation $Log = [PSCustomObject]@{ Account = $AccountName RotatedBy = "Automated" RotationDate = Get-Date NextRotation = (Get-Date).AddDays(90) } $Log | Export-Csv "Password-Rotation-Log.csv" -Append -NoTypeInformation return $NewPassword # Return to update application config }
4

Remove Unnecessary Privileges

# Audit service account group memberships $ServiceAccounts = Import-Csv "Service-Account-Inventory.csv" foreach ($SA in $ServiceAccounts) { $Groups = Get-ADPrincipalGroupMembership -Identity $SA.SamAccountName Write-Host "`n$($SA.SamAccountName) is member of:" -ForegroundColor Cyan foreach ($Group in $Groups) { $IsPrivileged = $Group.Name -match "Admin|Operator|Server|Account" $Color = if ($IsPrivileged) { "Red" } else { "Gray" } Write-Host " - $($Group.Name)" -ForegroundColor $Color } } # Remove from unnecessary groups # Example: Remove from Domain Admins if only local admin needed Remove-ADGroupMember -Identity "Domain Admins" -Members "svc-app-legacy" -Confirm:$false

AdminSDHolder Cleanup

High Risk
Effort: Medium
Time: 1-2 weeks

What is AdminSDHolder?

AdminSDHolder is a container that defines the ACL template for all protected accounts (Domain Admins, etc.). Every 60 minutes, SDProp applies this ACL to protected accounts. Attackers can add backdoor permissions here for persistence.

1

Audit Current AdminSDHolder ACL

# Get AdminSDHolder ACL $Domain = Get-ADDomain $AdminSDHolderDN = "CN=AdminSDHolder,CN=System,$($Domain.DistinguishedName)" $ACL = Get-Acl "AD:\$AdminSDHolderDN" # Expected trustees (should be limited to these) $Expected = @( "NT AUTHORITY\SYSTEM", "NT AUTHORITY\SELF", "BUILTIN\Administrators", "$($Domain.NetBIOSName)\Domain Admins", "$($Domain.NetBIOSName)\Enterprise Admins" ) Write-Host "AdminSDHolder ACL Analysis:" -ForegroundColor Cyan Write-Host "=" * 50 foreach ($ACE in $ACL.Access) { $Trustee = $ACE.IdentityReference.Value $IsExpected = $Expected -contains $Trustee $Color = if ($IsExpected) { "Green" } else { "Red" } Write-Host "`n[$($ACE.AccessControlType)] $Trustee" -ForegroundColor $Color Write-Host " Rights: $($ACE.ActiveDirectoryRights)" if (-not $IsExpected) { Write-Host " [!] NON-STANDARD - INVESTIGATE" -ForegroundColor Red } }
2

Remove Unauthorized ACEs

# Remove specific unauthorized ACE from AdminSDHolder $Domain = Get-ADDomain $AdminSDHolderDN = "CN=AdminSDHolder,CN=System,$($Domain.DistinguishedName)" # Get current ACL $ACL = Get-Acl "AD:\$AdminSDHolderDN" # Remove specific trustee (example: removing backdoor account) $TrusteeToRemove = "CONTOSO\BackdoorAccount" $ACL.Access | Where-Object { $_.IdentityReference -eq $TrusteeToRemove } | ForEach-Object { $ACL.RemoveAccessRule($_) } # Apply modified ACL Set-Acl "AD:\$AdminSDHolderDN" $ACL Write-Host "[REMOVED] $TrusteeToRemove from AdminSDHolder" -ForegroundColor Green # Force SDProp to run immediately (updates protected accounts) # This requires running on a DC $RootDSE = [ADSI]"LDAP://RootDSE" $RootDSE.Put("runProtectAdminGroupsTask", 1) $RootDSE.SetInfo() Write-Host "[TRIGGERED] SDProp task - protected accounts will be updated" -ForegroundColor Cyan
3

Clear Orphaned adminCount Flags

# Find accounts with adminCount=1 that aren't in privileged groups $PrivilegedGroups = @( "Domain Admins", "Enterprise Admins", "Schema Admins", "Administrators", "Account Operators", "Backup Operators", "Server Operators", "Print Operators" ) $OrphanedAccounts = Get-ADUser -Filter {adminCount -eq 1} -Properties adminCount, memberOf | Where-Object { $UserGroups = $_.memberOf | ForEach-Object { (Get-ADGroup $_).Name } -not ($PrivilegedGroups | Where-Object { $UserGroups -contains $_ }) } Write-Host "Orphaned adminCount accounts: $($OrphanedAccounts.Count)" -ForegroundColor Yellow # Clear the flag (allows inheritance to work again) foreach ($Account in $OrphanedAccounts) { Set-ADUser -Identity $Account -Clear adminCount # Re-enable inheritance $UserDN = $Account.DistinguishedName $ACL = Get-Acl "AD:\$UserDN" $ACL.SetAccessRuleProtection($false, $true) # Enable inheritance Set-Acl "AD:\$UserDN" $ACL Write-Host "[FIXED] $($Account.SamAccountName) - adminCount cleared, inheritance enabled" -ForegroundColor Green }

MFA Deployment Playbook

Critical
Effort: High
Time: 4-8 weeks

Why MFA is Critical

MFA blocks 99.9% of credential-based attacks. It's the single most impactful security control you can implement. Prioritize privileged accounts first, then expand to all users.

Recommended Rollout

Phase 1
Week 1-2
IT & Security Teams
Phase 2
Week 3-4
All Privileged Users
Phase 3
Week 5-6
Executives & Finance
Phase 4
Week 7-8
All Employees
1

Enable Security Defaults or Conditional Access

# Option A: Security Defaults (Simple, Free) Azure Portal > Azure Active Directory > Properties > Manage Security Defaults > Enable Security Defaults: Yes # Option B: Conditional Access (Recommended, requires P1 license) Azure Portal > Azure AD > Security > Conditional Access > New Policy Policy 1: "Require MFA for Admins" - Users: Directory roles > Select all admin roles - Cloud apps: All cloud apps - Grant: Require MFA - Enable: On Policy 2: "Require MFA for All Users" - Users: All users (exclude emergency access accounts) - Cloud apps: All cloud apps - Conditions: Exclude trusted locations (optional) - Grant: Require MFA - Enable: Report-only first, then On
2

Configure Authentication Methods

# Prioritize phishing-resistant methods Azure Portal > Azure AD > Security > Authentication Methods Recommended priority: 1. FIDO2 Security Keys (most secure) 2. Microsoft Authenticator (push notifications) 3. Windows Hello for Business 4. Authenticator TOTP codes (acceptable) 5. SMS (disable if possible - SIM swap vulnerable) 6. Voice call (disable if possible) For each method: - Enable for target users/groups - Configure settings (e.g., number matching for Authenticator)
3

Create Emergency Access Accounts

# Create break-glass accounts BEFORE enforcing MFA # These accounts bypass MFA for emergency access # In Azure AD: # 1. Create 2 cloud-only accounts (no sync from on-prem) # 2. Assign Global Administrator role # 3. Use 25+ character random passwords # 4. Store credentials in physical safe (split knowledge) # 5. Exclude from ALL Conditional Access policies # 6. Monitor sign-ins with alerts # Alert rule for break-glass usage: # Azure Portal > Azure AD > Sign-in logs > # Create alert for sign-ins from these accounts
4

User Communication & Training

# Communication Plan: Week -2: Announcement email - What: MFA is coming - Why: Security improvement - When: Rollout date - Action: Download Microsoft Authenticator app Week -1: Reminder + Instructions - Step-by-step setup guide - Video tutorial link - IT support contact for help Rollout Day: Final notice - MFA now required - Link to self-service setup - Support desk hours extended Week +1: Follow-up - Check registration stats - Reach out to non-compliant users - Address common issues
5

Monitor MFA Coverage

# PowerShell: Check MFA registration status Connect-MgGraph -Scopes "UserAuthenticationMethod.Read.All" $Users = Get-MgUser -All -Property Id, DisplayName, UserPrincipalName $MFAStatus = foreach ($User in $Users) { $Methods = Get-MgUserAuthenticationMethod -UserId $User.Id $HasMFA = ($Methods | Where-Object { $_.'@odata.type' -match "microsoft|fido2|phone" }).Count -gt 0 [PSCustomObject]@{ User = $User.UserPrincipalName MFARegistered = $HasMFA MethodCount = $Methods.Count } } $Coverage = ($MFAStatus | Where-Object MFARegistered).Count / $MFAStatus.Count * 100 Write-Host "MFA Coverage: $([math]::Round($Coverage,1))%" # Export users without MFA $MFAStatus | Where-Object { -not $_.MFARegistered } | Export-Csv "Users-Without-MFA.csv" -NoTypeInformation
MFA Deployment Checklist
Emergency access accounts created and tested
Authentication methods configured (prioritize passwordless)
Conditional Access policies created (report-only mode)
User communication sent
IT team trained on support procedures
Pilot group enrolled and tested
Policies switched from report-only to enforced
Coverage monitoring dashboard configured
Legacy authentication blocked