Notice: Due to size constraints and loading performance considerations, scripts referenced in blog posts are not attached directly. To request access, please complete the following form: Script Request Form Note: A Google account is required to access the form.
Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

Maintaining FGPP Compliance

As organizations increasingly adopt Fine-Grained Password Policies (FGPP) to enforce stricter security standards for different user groups, a critical oversight often emerges: users slipping through the cracks and falling back to weaker default domain policies.

The FGPP Implementation Challenge

When you deploy Fine-Grained Password Policies in Active Directory, you're essentially creating a tiered security model where different user groups receive different password requirements. This is incredibly powerful for organizations that need to enforce stricter policies for privileged accounts while maintaining usability for standard users.

However, there's a fundamental flaw in how many organizations monitor FGPP compliance: they assume that once users are placed in the appropriate groups, they'll stay there.

Visual Runtime of the Script

This is the output on he console updates when the script is running:

What Happens When Users Fall Through the Cracks

Let me illustrate with a real-world scenario from our environment at bear.local:

# Example: Checking a user's effective password policy
Get-ADUserResultantPasswordPolicy -Identity "john.fluff"

# If this returns null, the user is subject to the default domain policy
# If it returns a policy object, they're covered by FGPP

Consider this: your company policy mandates that privileged accounts must change passwords every 30 days, implemented through FGPP. Your default domain policy, however, allows 100-day password changes for backwards compatibility with legacy systems.

What happens when a privileged user is accidentally removed from their FGPP group?

They silently fall back to the 100-day policy. No alerts, no notifications – just a critical compliance gap that could persist for months.

Understanding Fine-Grained Password Policies

Before diving into the monitoring solution, let's explore what FGPP can actually control and why these settings matter.

FGPP Configuration Options

Fine-Grained Password Policies offer granular control over multiple password and account lockout settings:

# Creating a secure FGPP for privileged accounts
New-ADFineGrainedPasswordPolicy -Name "SecurePrivilegedPolicy" `
    -Precedence 10 `
    -ComplexityEnabled $true `
    -Description "High-security policy for privileged accounts" `
    -DisplayName "Privileged Account Security Policy" `
    -LockoutDuration "00:30:00" `
    -LockoutObservationWindow "00:30:00" `
    -LockoutThreshold 3 `
    -MaxPasswordAge "30.00:00:00" `
    -MinPasswordAge "1.00:00:00" `
    -MinPasswordLength 14 `
    -PasswordHistoryCount 24 `
    -ReversibleEncryptionEnabled $false

Let's break down these critical settings:

Password Age Settings:

  • MaxPasswordAge: Maximum time before password must be changed (30 days for privileged accounts)
  • MinPasswordAge: Minimum time before password can be changed again (prevents rapid cycling)

Complexity Requirements:

  • MinPasswordLength: Minimum character count (14 for high-security environments)
  • ComplexityEnabled: Enforces Windows complexity rules
  • PasswordHistoryCount: Prevents reusing recent passwords (24 previous passwords)

Account Lockout Protection:

  • LockoutThreshold: Failed attempts before lockout (3 attempts)
  • LockoutDuration: How long accounts remain locked (30 minutes)
  • LockoutObservationWindow: Time window for counting failed attempts

When Users Fall Back to Default Policy

The real danger lies in the silent nature of FGPP failures. When a user is removed from their designated password policy group – whether through administrative error, group cleanup, or organizational changes – they don't receive a weaker password policy notification.

Domain Functional Level Limitation

Here's where the compliance gap becomes even more critical: traditional domain password policies in Windows Server 2012 R2 and earlier are limited to a maximum of 14 characters for minimum password length. This has been a longstanding limitation for many decades on all Microsoft platforms.

Consider this scenario in our bear.local environment:

  • Your FGPP enforces 30-character minimum passwords for privileged accounts
  • Your default domain policy can only enforce a maximum of 14 characters (due to Server 2012 R2 limitations)
  • When a privileged user falls out of their FGPP group, they drop from 30-character requirements to just 14 characters

This creates a massive security gap – users aren't just falling back to a "slightly weaker" policy, they're potentially falling back to passwords that are less than half the required length.

Domain Functional Level vs. Password Length Capabilities

The password length limitations are not tied to domain functional level increases but rather to specific operating system versions and updates:

Windows Server 2012 R2 and Earlier:

  • Maximum configurable minimum password length: 14 characters
  • No workaround available through standard Group Policy
  • FGPP is the only way to enforce longer passwords

Windows Server 2016/2019:

  • Initially introduced support for longer passwords in 2018 updates
  • Support was later removed in January 2019 updates due to compatibility issues
  • Still limited to 14 characters in most deployments

Windows Server 2022 and Windows 10 2004+:

  • Introduction of RelaxMinimumPasswordLengthLimits policy setting
  • Allows minimum password length up to 255 characters
  • Requires three coordinated Group Policy settings to function

The Hidden Compliance Trap

# Example: What happens when FGPP fails in older domains
Get-ADUserResultantPasswordPolicy -Identity "privileged.admin"
# Returns: null (using default domain policy)

Get-ADDefaultDomainPasswordPolicy | Select-Object MinPasswordLength
# Returns: MinPasswordLength : 14  (maximum possible in Server 2012 R2)

# Meanwhile, your FGPP was enforcing:
Get-ADFineGrainedPasswordPolicy -Identity "BEAR-PrivilegedPolicy" | 
Select-Object MinPasswordLength
# Returns: MinPasswordLength : 30

This creates a scenario where:

  1. FGPP enforces 30 characters for privileged accounts
  2. Default domain policy enforces only 14 characters (maximum possible)
  3. The gap is 16 characters – more than doubling the attack surface

Checking Current Policy Assignment

You can verify a user's current password policy assignment:

# Method 1: Check resultant password policy
$user = "privileged.admin"
$policy = Get-ADUserResultantPasswordPolicy -Identity $user

if ($policy) {
    Write-Host "User $user is subject to FGPP: $($policy.Name)"
} else {
    Write-Host "WARNING: User $user is using DEFAULT domain policy"
}

# Method 2: Check group membership
$policyGroups = @("BEAR-DomainAdmins-Policy", "BEAR-PrivilegedUsers-Policy")
$userGroups = (Get-ADUser -Identity $user -Properties MemberOf).MemberOf

foreach ($group in $policyGroups) {
    $groupDN = (Get-ADGroup -Identity $group).DistinguishedName
    if ($userGroups -contains $groupDN) {
        Write-Host "User is member of policy group: $group"
    }

Upgrading Domain Password Capabilities

For organizations running newer domain controllers, you can extend the default domain policy beyond 14 characters:

# Available in Windows Server 2022 and Windows 10 2004+
# Three coordinated settings required:

# 1. Enable relaxed length limits
Set-GPRegistryValue -Name "Default Domain Policy" -Key 
"HKLM\System\CurrentControlSet\Control\SAM" -ValueName "RelaxMinimumPasswordLengthLimits" 
-Type DWord -Value 1

# 2. Set audit length (for monitoring)
# Navigate to: Computer Configuration > Windows Settings > Security Settings 
> Account Policies > Password Policy
# Set "Minimum password length audit" to desired value

# 3. Set actual minimum length  
# Set "Minimum password length" to desired value (can now exceed 14)

Important caveat: Even with these newer capabilities, FGPP remains essential for:

  • Granular group-based policies (different requirements for different user types)
  • Backwards compatibility with mixed-version environments
  • Centralized policy management for complex organizational structures

Automating Compliance

To address this challenge, I've developed a comprehensive PowerShell script that monitors FGPP group membership and generates detailed compliance reports.

The script follows this logical flow:

# 1. Load required modules and validate environment
Import-Module ActiveDirectory

# 2. Define policy group structure
$PolicyGroups = @(
    "BEAR-ExecutivePolicy-Users",
    "BEAR-AdminPolicy-Standard", 
    "BEAR-AdminPolicy-Privileged",
    "BEAR-ServicePolicy-Critical",
    "BEAR-UserPolicy-Marketing",
    "BEAR-UserPolicy-Finance",
    "BEAR-UserPolicy-IT",
    "BEAR-UserPolicy-HR",
    "BEAR-UserPolicy-Sales",
    "BEAR-UserPolicy-Operations"
)

# 3. Query all enabled users
$AllUsers = Get-ADUser -Filter {Enabled -eq $true} -Properties MemberOf

# 4. Analyze group memberships
# 5. Generate compliance report

Key Monitoring Features

The script provides several critical monitoring capabilities that account for domain functional level limitations:

Domain Version Detection:

# Detect domain functional level and warn about password length limitations
$DomainLevel = (Get-ADDomain).DomainMode
$MaxDomainPolicyLength = switch ($DomainLevel) {
    "Windows2012R2Domain" { 14 }
    "Windows2016Domain" { 14 }  # Still limited without RelaxMinimumPasswordLengthLimits
    "Windows2019Domain" { 14 }  # Still limited without RelaxMinimumPasswordLengthLimits  
    "Windows2022Domain" { 255 } # If RelaxMinimumPasswordLengthLimits is enabled
    default { 14 }
}
Write-Warning "Domain functional level: $DomainLevel"
Write-Warning "Maximum enforceable password length in default domain policy: 
$MaxDomainPolicyLength characters"

FGPP Gap Analysis:

# Compare FGPP requirements vs domain policy maximums
$FGPPPolicies = Get-ADFineGrainedPasswordPolicy -Filter *
foreach ($Policy in $FGPPPolicies) {
    $FGPPLength = $Policy.MinPasswordLength
    if ($FGPPLength -gt $MaxDomainPolicyLength) {
        $SecurityGap = $FGPPLength - $MaxDomainPolicyLength
        Write-Warning "CRITICAL: Policy '$($Policy.Name)' requires $FGPPLength chars, 
        but domain fallback only enforces $MaxDomainPolicyLength chars 
        (gap: $SecurityGap characters)"
    }
}

Group Validation:

# Verify all policy groups exist before processing
foreach ($GroupName in $PolicyGroups) {
    try {
        $Group = Get-ADGroup -Identity $GroupName -ErrorAction Stop
        Write-Host "✓ Policy group validated: $GroupName" -ForegroundColor Green
    }
    catch {
        Write-Warning "✗ Missing policy group: $GroupName"
        $MissingGroups += $GroupName
    }
}

User Analysis:

# Check each user's policy group membership
foreach ($User in $AllUsers) {
    $InPolicyGroup = $false
    $UserGroups = $User.MemberOf
    
    if ($UserGroups) {
        foreach ($GroupDN in $UserGroups) {
            $GroupName = (Get-ADGroup -Identity $GroupDN).Name
            if ($PolicyGroups -contains $GroupName) {
                $InPolicyGroup = $true
                break
            }
        }
    }
    
    if (-not $InPolicyGroup) {
        # User is non-compliant - add to report
        $NonCompliantUsers += $User
    }
}

Report Generation and Analysis

The script generates comprehensive CSV reports that include:

  • User Identification: Username, display name, email
  • Organizational Data: Department, job title
  • Account Status: Enabled status, last logon date
  • Technical Details: Distinguished name for easy AD management

Sample Report Output

Username,DisplayName,Email,Department,Title,Enabled,LastLogonDate,DistinguishedName
j.smith,John Smith,j.smith@bear.local,IT,System Administrator,True,2024-06-20,CN=John Smith,OU=IT,DC=bear,DC=local
m.jones,Mary Jones,m.jones@bear.local,Finance,Controller,True,2024-06-19,CN=Mary Jones,OU=Finance,DC=bear,DC=local

Scheduled Monitoring

Implement the script as a scheduled task for regular compliance checks:

# Create scheduled task to run weekly
$Action = New-ScheduledTaskAction -Execute 'PowerShell.exe' -Argument '-File 
"C:\Scripts\FGPP-Compliance-Monitor.ps1"'
$Trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 6:00AM
Register-ScheduledTask -Action $Action -Trigger $Trigger -TaskName 
"FGPP Compliance Monitor"

Integration with SIEM/Logging

# Write compliance events to Windows Event Log
New-EventLog -LogName "Application" -Source "FGPP Monitor" -ErrorAction SilentlyContinue

if ($NonCompliantUsers.Count -gt 0) {
    Write-EventLog -LogName "Application" -Source "FGPP Monitor" -EventId 1001 -EntryType 
Warning -Message "FGPP Compliance Issue: $($NonCompliantUsers.Count) users not in 
policy groups"
}

Script : FGPP-Compliance.ps1

# Fine-Grained Password Policy Compliance Monitor
# Domain: bear.local
# Purpose: Identify users not assigned to any password policy groups

# Import Active Directory module
try {
    Import-Module ActiveDirectory -ErrorAction Stop
    Write-Host "Active Directory module loaded successfully." -ForegroundColor Green
}
catch {
    Write-Error "Failed to load Active Directory module. Please ensure 
    RSAT-AD-PowerShell feature is installed."
    exit 1
}

# Define password policy groups for bear.local domain
$PasswordPolicyGroups = @(
    "BEAR-ExecutivePolicy-Users",
    "BEAR-AdminPolicy-Standard", 
    "BEAR-AdminPolicy-Privileged",
    "BEAR-ServicePolicy-Critical",
    "BEAR-UserPolicy-Marketing",
    "BEAR-UserPolicy-Finance",
    "BEAR-UserPolicy-IT",
    "BEAR-UserPolicy-HR",
    "BEAR-UserPolicy-Sales",
    "BEAR-UserPolicy-Operations"
)

# Get script directory for output file
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$ReportPath = Join-Path $ScriptPath "FGPP_Compliance_Report_$
(Get-Date -Format 'yyyyMMdd_HHmmss').csv"

Write-Host "Starting FGPP Compliance Audit for bear.local domain..." -ForegroundColor Yellow
Write-Host "Monitoring for users not assigned to password policy groups..." 
-ForegroundColor Yellow

try {
    # Retrieve all enabled user accounts
    Write-Host "Querying enabled user accounts from bear.local..." -ForegroundColor Cyan
    $AllUsers = Get-ADUser -Filter {Enabled -eq $true} -Properties MemberOf, Department, 
    Title, LastLogonDate, PasswordLastSet
    Write-Host "Located $($AllUsers.Count) enabled user accounts." -ForegroundColor Cyan
    
    # Validate password policy groups exist
    Write-Host "Validating password policy group structure..." -ForegroundColor Cyan
    $ValidatedGroups = @()
    $MissingGroups = @()
    
    foreach ($GroupName in $PasswordPolicyGroups) {
        try {
            $Group = Get-ADGroup -Identity $GroupName -ErrorAction Stop
            $ValidatedGroups += $GroupName
            Write-Host "✓ Validated policy group: $GroupName" -ForegroundColor Green
        }
        catch {
            $MissingGroups += $GroupName
            Write-Warning "✗ Policy group not found: $GroupName"
        }
    }
    
    if ($MissingGroups.Count -gt 0) {
        Write-Warning "ATTENTION: The following policy groups are missing from bear.local:"
        $MissingGroups | ForEach-Object { Write-Warning "  - $_" }
        Write-Host "Continuing with available groups..." -ForegroundColor Yellow
    }
    
    # Analyze user compliance with password policy groups
    Write-Host "Analyzing FGPP compliance for all users..." -ForegroundColor Cyan
    $NonCompliantUsers = @()
    $CompliantUsers = @()
    $ProcessedCount = 0
    
    foreach ($User in $AllUsers) {
        $ProcessedCount++
        if ($ProcessedCount % 50 -eq 0) {
            Write-Progress -Activity "Analyzing FGPP Compliance" -Status 
            "Processed $ProcessedCount of $($AllUsers.Count) users" -PercentComplete 
            (($ProcessedCount / $AllUsers.Count) * 100)
        }
        
        # Determine if user belongs to any password policy group
        $UserGroups = $User.MemberOf
        $AssignedToPolicyGroup = $false
        $PolicyGroupMembership = @()
        
        if ($UserGroups) {
            foreach ($GroupDN in $UserGroups) {
                try {
                    $GroupName = (Get-ADGroup -Identity $GroupDN).Name
                    if ($ValidatedGroups -contains $GroupName) {
                        $AssignedToPolicyGroup = $true
                        $PolicyGroupMembership += $GroupName
                    }
                }
                catch {
                    # Skip groups that can't be resolved
                    continue
                }
            }
        }
        
        # Categorize user based on policy group assignment
        if ($AssignedToPolicyGroup) {
            $CompliantUsers += [PSCustomObject]@{
                'Username' = $User.SamAccountName
                'DisplayName' = $User.Name
                'PolicyGroups' = ($PolicyGroupMembership -join "; ")
            }
        } else {
            # User is non-compliant - not in any password policy group
            $NonCompliantUsers += [PSCustomObject]@{
                'Username' = $User.SamAccountName
                'DisplayName' = $User.Name
                'Email' = $User.UserPrincipalName
                'Department' = $User.Department
                'Title' = $User.Title
                'Enabled' = $User.Enabled
                'LastLogonDate' = $User.LastLogonDate
                'PasswordLastSet' = $User.PasswordLastSet
                'DistinguishedName' = $User.DistinguishedName
                'ComplianceStatus' = 'NON-COMPLIANT: Using Default Domain Policy'
            }
        }
    }
    
    Write-Progress -Activity "Analyzing FGPP Compliance" -Completed
    
    # Generate comprehensive compliance report
    Write-Host "Generating FGPP compliance report..." -ForegroundColor Cyan
    
    # Create summary report header
    $ReportSummary = @"
# FGPP Compliance Report for bear.local Domain
# Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
# 
# SUMMARY:
# Total Users Analyzed: $($AllUsers.Count)
# Compliant Users: $($CompliantUsers.Count)
# Non-Compliant Users: $($NonCompliantUsers.Count)
# Compliance Rate: $(if($AllUsers.Count -gt 0){[math]::Round(($CompliantUsers.Count / $AllUsers.Count) * 100, 2)}else{0})%
#
# POLICY GROUPS MONITORED:
$(foreach($group in $ValidatedGroups){"# - $group"})
#
# NON-COMPLIANT USERS (Subject to Default Domain Policy):
"@
    
    if ($NonCompliantUsers.Count -gt 0) {
        # Export non-compliant users to CSV
        $NonCompliantUsers | Export-Csv -Path $ReportPath -NoTypeInformation -Encoding UTF8
        
        # Display compliance summary
        Write-Host "`n=== FGPP COMPLIANCE SUMMARY FOR BEAR.LOCAL ===" -ForegroundColor Yellow
        Write-Host "Total enabled users analyzed: $($AllUsers.Count)" -ForegroundColor White
        Write-Host "Users with FGPP assignment: $($CompliantUsers.Count)" -ForegroundColor Green
        Write-Host "Users without FGPP assignment: $($NonCompliantUsers.Count)" -ForegroundColor Red
        Write-Host "Compliance rate: $(if($AllUsers.Count -gt 0){[math]::Round(($CompliantUsers.Count / $AllUsers.Count) * 100, 2)}else{0})%" -ForegroundColor White
        Write-Host "Detailed report saved to: $ReportPath" -ForegroundColor Green
        
        # Show preview of non-compliant users
        Write-Host "`n=== NON-COMPLIANT USERS (Preview) ===" -ForegroundColor Red
        $NonCompliantUsers | Select-Object -First 10 | Format-Table -AutoSize Username, DisplayName, Department, LastLogonDate, PasswordLastSet
        
        if ($NonCompliantUsers.Count -gt 10) {
            Write-Host "... plus $($NonCompliantUsers.Count - 10) additional non-compliant users. See complete report: $ReportPath" -ForegroundColor Cyan
        }
        
        # Security recommendations
        Write-Host "`n=== SECURITY RECOMMENDATIONS ===" -ForegroundColor Yellow
        Write-Host "1. Review non-compliant users and assign to appropriate policy groups" -ForegroundColor White
        Write-Host "2. Verify that high-privilege accounts are not using default domain policy" -ForegroundColor White
        Write-Host "3. Implement automated monitoring to prevent future compliance gaps" -ForegroundColor White
        Write-Host "4. Consider service accounts that may need specialized password policies" -ForegroundColor White
        
    } else {
        Write-Host "`n=== FGPP COMPLIANCE SUMMARY FOR BEAR.LOCAL ===" -ForegroundColor Yellow
        Write-Host "Total enabled users analyzed: $($AllUsers.Count)" -ForegroundColor White
        Write-Host "✓ EXCELLENT: All users are assigned to password policy groups!" -ForegroundColor Green
        Write-Host "✓ No users are falling back to default domain policy" -ForegroundColor Green
        
        # Create empty report file with summary
        $ReportSummary | Out-File -FilePath $ReportPath -Encoding UTF8
        Write-Host "Summary report saved to: $ReportPath" -ForegroundColor Green
    }
    
    # Display monitored policy groups
    Write-Host "`n=== MONITORED PASSWORD POLICY GROUPS ===" -ForegroundColor Yellow
    $ValidatedGroups | ForEach-Object { 
        $GroupMembers = (Get-ADGroupMember -Identity $_ | Measure-Object).Count
        Write-Host "✓ $_ ($GroupMembers members)" -ForegroundColor Green 
    }
    if ($MissingGroups.Count -gt 0) {
        Write-Host "`n=== MISSING POLICY GROUPS ===" -ForegroundColor Red
        $MissingGroups | ForEach-Object { Write-Host "✗ $_ (not found in bear.local)" -ForegroundColor Red }
    }
    
    # Log event for monitoring systems
    try {
        New-EventLog -LogName "Application" -Source "FGPP Monitor" -ErrorAction SilentlyContinue
        $EventMessage = "FGPP Compliance Check: $($AllUsers.Count) users analyzed, $($NonCompliantUsers.Count) non-compliant"
        $EventType = if ($NonCompliantUsers.Count -gt 0) { "Warning" } else { "Information" }
        $EventID = if ($NonCompliantUsers.Count -gt 0) { 1001 } else { 1000 }
        
        Write-EventLog -LogName "Application" -Source "FGPP Monitor" -EventId $EventID -EntryType $EventType -Message $EventMessage
        Write-Host "Compliance event logged to Windows Event Log" -ForegroundColor Cyan
    }
    catch {
        Write-Warning "Could not write to Windows Event Log: $($_.Exception.Message)"
    }
    
} catch {
    Write-Error "Critical error during FGPP compliance audit: $($_.Exception.Message)"
    Write-Error "Stack trace: $($_.ScriptStackTrace)"
    exit 1
}

Write-Host "`nFGPP Compliance Audit completed successfully!" -ForegroundColor Green
Write-Host "For ongoing compliance, consider scheduling this script to run weekly." -ForegroundColor Cyan

Conclusion

Fine-Grained Password Policies are only as strong as your ability to monitor and maintain them. Without proper oversight, users can silently fall back to weaker default policies, creating significant security gaps that persist undetected.

The domain functional level limitation makes this even more critical – in Windows Server 2012 R2 environments, users falling back from FGPP requirements of 30+ characters to domain policy maximums of just 14 characters represents security degradation of I am sure what is already a weak password.

Previous Post Next Post

نموذج الاتصال