Public/Test-FslConfiguration.ps1
|
function Test-FslConfiguration { <# .SYNOPSIS Sanity-checks the FSLogix configuration of a session host and flags the misconfigurations that cause temp profiles, login storms and data loss. .DESCRIPTION Evaluates the FSLogix install state and the Profiles/ODFC registry settings against a curated rule set distilled from real troubleshooting: unreachable VHDLocations, masked failures (temp profiles), local-profile collisions, double-attach conflicts between Profile and ODFC containers, missing antivirus exclusions and more. All checks are read-only. Returns FSLogixDoctor.Finding objects. .PARAMETER ConfigSnapshot A configuration snapshot hashtable. Defaults to the live local configuration; pass a fixture snapshot for testing or offline analysis. .EXAMPLE Test-FslConfiguration Checks the local session host. .EXAMPLE Test-FslConfiguration | Where-Object Severity -in 'Critical','Warning' #> [CmdletBinding()] [OutputType([pscustomobject])] param( [hashtable]$ConfigSnapshot ) if (-not $ConfigSnapshot) { $ConfigSnapshot = Get-FslConfigSnapshot } $target = $ConfigSnapshot.ComputerName $configHelp = 'https://learn.microsoft.com/en-us/fslogix/reference-configuration-settings' $profiles = $ConfigSnapshot.Profiles $odfc = $ConfigSnapshot.Odfc # --- Install state ------------------------------------------------------- $install = $ConfigSnapshot.Install if (-not $install -or -not $install.Installed) { New-FslFinding -Category Environment -Check 'FSLogix installed' -Severity Critical -Target $target ` -Message 'The FSLogix service (frxsvc) is not present on this host.' ` -Recommendation 'Install the FSLogix Apps agent. Download: https://aka.ms/fslogix/download' ` -HelpUri 'https://learn.microsoft.com/en-us/fslogix/how-to-install-fslogix' return } if ($install.ServiceStatus -ne 'Running') { New-FslFinding -Category Environment -Check 'FSLogix service running' -Severity Critical -Target $target ` -Message ("The FSLogix service (frxsvc) is installed but its state is '{0}'." -f $install.ServiceStatus) ` -Recommendation 'Start the service and check the Application event log for service crashes: Start-Service frxsvc' } else { New-FslFinding -Category Environment -Check 'FSLogix service running' -Severity Pass -Target $target ` -Message 'The FSLogix service (frxsvc) is running.' ` -Evidence ("Version: {0}" -f $install.Version) } if ($install.Version) { # Judge the version against the curated release table instead of # 'compare manually'. The table is a shipped snapshot - the finding # always names its AsOf date so an aging table stays honest. $releaseTable = Get-FslDataTable -Name Releases $newestCurated = $null try { $newestCurated = @($releaseTable.Releases | ForEach-Object { [version]$_.Version } | Sort-Object -Descending)[0] } catch { $newestCurated = $null } $installedVersion = $null try { $installedVersion = [version]$install.Version } catch { $installedVersion = $null } if ($newestCurated -and $installedVersion -and $installedVersion -lt $newestCurated) { New-FslFinding -Category Environment -Check 'FSLogix version' -Severity Warning -Target $target ` -Message ("Installed FSLogix version {0} is older than the newest curated release {1} (release table as of {2})." -f $install.Version, $newestCurated, $releaseTable.AsOf) ` -Recommendation 'Update FSLogix during the next maintenance window and review the release notes for fixed issues: https://learn.microsoft.com/en-us/fslogix/overview-release-notes' ` -HelpUri 'https://learn.microsoft.com/en-us/fslogix/overview-release-notes' } elseif ($newestCurated -and $installedVersion) { New-FslFinding -Category Environment -Check 'FSLogix version' -Severity Info -Target $target ` -Message ("Installed FSLogix version {0} matches or exceeds the newest curated release (release table as of {1})." -f $install.Version, $releaseTable.AsOf) ` -Recommendation 'Spot-check the release notes for issues newer than the curated table: https://learn.microsoft.com/en-us/fslogix/overview-release-notes' } else { New-FslFinding -Category Environment -Check 'FSLogix version' -Severity Info -Target $target ` -Message ("Installed FSLogix version: {0}." -f $install.Version) ` -Recommendation 'Compare against the current release and known issues: https://learn.microsoft.com/en-us/fslogix/overview-release-notes' } } # --- Profiles configuration ---------------------------------------------- if (-not $profiles) { New-FslFinding -Category Configuration -Check 'Profiles configured' -Severity Critical -Target $target ` -Message 'No FSLogix Profiles configuration found (HKLM:\SOFTWARE\FSLogix\Profiles is missing or empty).' ` -Recommendation 'Configure at least Enabled=1 and VHDLocations via GPO, Intune or registry.' ` -HelpUri $configHelp return } if ($profiles['Enabled'] -ne 1) { $enabledValue = '<not set>' if ($null -ne $profiles['Enabled']) { $enabledValue = [string]$profiles['Enabled'] } New-FslFinding -Category Configuration -Check 'Profiles enabled' -Severity Critical -Target $target ` -Message 'FSLogix profile containers are not enabled on this host.' ` -Evidence ("Enabled = {0}" -f $enabledValue) ` -Recommendation 'Set HKLM:\SOFTWARE\FSLogix\Profiles\Enabled (DWORD) = 1.' ` -HelpUri $configHelp } else { New-FslFinding -Category Configuration -Check 'Profiles enabled' -Severity Pass -Target $target ` -Message 'FSLogix profile containers are enabled.' } # --- VHDLocations / CCDLocations ------------------------------------------- # Handles REG_MULTI_SZ and semicolon-delimited REG_SZ alike. $locations = @(ConvertTo-FslLocationList -Value $profiles['VHDLocations']) $usesCloudCache = (@(ConvertTo-FslLocationList -Value $profiles['CCDLocations']).Count -gt 0) if ($usesCloudCache -and $locations.Count -gt 0) { New-FslFinding -Category Configuration -Check 'VHDLocations vs CCDLocations' -Severity Critical -Target $target ` -Message 'Both VHDLocations and CCDLocations are set. Per Microsoft the two must not be present at the same time.' ` -Recommendation 'Remove one of the two: VHDLocations for classic SMB, CCDLocations for Cloud Cache.' ` -HelpUri $configHelp } if ($locations.Count -eq 0 -and -not $usesCloudCache) { New-FslFinding -Category Configuration -Check 'VHDLocations defined' -Severity Critical -Target $target ` -Message 'Neither VHDLocations nor CCDLocations is configured - FSLogix has nowhere to store profile containers.' ` -Recommendation 'Set VHDLocations to the UNC path of your profile share (or CCDLocations for Cloud Cache).' ` -HelpUri $configHelp } else { foreach ($location in $locations) { $online = $null if ($ConfigSnapshot.VhdLocationsOnline -is [hashtable] -and $ConfigSnapshot.VhdLocationsOnline.ContainsKey($location)) { $online = [bool]$ConfigSnapshot.VhdLocationsOnline[$location] } else { try { $online = Test-Path -Path $location -ErrorAction Stop } catch { $online = $false } } if ($online) { New-FslFinding -Category Configuration -Check 'VHDLocations reachable' -Severity Pass -Target $target ` -Message ("Profile location '{0}' is reachable." -f $location) continue } # Separate 'network path down' from 'the share denies the probing # user': a share that denies THIS account still answers on TCP 445. # Typical false-alarm scenario: Azure Files with identity-based auth, # where only the AVD users hold the share-level RBAC role and the # admin running the diagnostic cannot browse the share at all. $shareHost = $null if ($location -match '^\\\\(?<sharehost>[^\\]+)') { $shareHost = $Matches['sharehost'] } $portOpen = $null if ($shareHost) { if ($ConfigSnapshot.SmbPortOpen -is [hashtable] -and $ConfigSnapshot.SmbPortOpen.ContainsKey($location)) { $portOpen = [bool]$ConfigSnapshot.SmbPortOpen[$location] } else { $portOpen = Test-FslSmbPort -ComputerName $shareHost } } $evidence = 'Note: the check runs in the current user context; the computer account or user may still have differing access.' if ($portOpen -eq $true) { $evidence = ("TCP 445 to '{0}' is open - the endpoint answers, so this looks like missing share permissions for the probing account '{1}', not a network problem. {2}" -f $shareHost, $env:USERNAME, $evidence) } elseif ($portOpen -eq $false) { $evidence = ("TCP 445 to '{0}' is NOT answering (blocked or offline) - this is a network/endpoint problem, not a permissions issue. {1}" -f $shareHost, $evidence) } $recommendation = 'Verify DNS, SMB connectivity, share and NTFS permissions for the profile share.' $reachHelpUri = 'https://learn.microsoft.com/en-us/fslogix/how-to-configure-storage-permissions' if ($shareHost -like '*.file.core.windows.net') { $recommendation = 'Azure Files: users need the ''Storage File Data SMB Share Contributor'' RBAC role on the storage account (admins ''Storage File Data SMB Share Elevated Contributor'' to browse), plus working identity-based auth (AD DS or Entra Kerberos) and NTFS ACLs. The probing account needs its own role assignment before this check can succeed.' $reachHelpUri = 'https://learn.microsoft.com/en-us/azure/storage/files/storage-files-identity-auth-active-directory-enable' } New-FslFinding -Category Configuration -Check 'VHDLocations reachable' -Severity Critical -Target $target ` -Message ("Profile location '{0}' is NOT reachable from this host (as the probing user)." -f $location) ` -Evidence $evidence ` -Recommendation $recommendation ` -HelpUri $reachHelpUri } } # --- Size and disk format ------------------------------------------------- if ($null -eq $profiles['SizeInMBs']) { New-FslFinding -Category Configuration -Check 'SizeInMBs' -Severity Info -Target $target ` -Message 'SizeInMBs is not set; containers use the FSLogix default maximum size.' ` -Recommendation 'Set an explicit SizeInMBs that matches your storage planning.' -HelpUri $configHelp } elseif ([int]$profiles['SizeInMBs'] -lt 5120) { New-FslFinding -Category Configuration -Check 'SizeInMBs' -Severity Warning -Target $target ` -Message ("SizeInMBs is only {0} MB - profiles fill up quickly and a full container behaves like a full C: drive for the user." -f $profiles['SizeInMBs']) ` -Recommendation 'Plan at least 10-30 GB per user for Office workloads.' -HelpUri $configHelp } if ($null -ne $profiles['IsDynamic'] -and [int]$profiles['IsDynamic'] -eq 0) { New-FslFinding -Category Configuration -Check 'IsDynamic' -Severity Warning -Target $target ` -Message 'IsDynamic=0: every container is fully allocated at maximum size on the share.' ` -Recommendation 'Set IsDynamic=1 unless you have a specific storage reason for fixed-size disks.' -HelpUri $configHelp } # Per Microsoft the DEFAULT VolumeType is vhd, so an unset value also means vhd. $volumeType = 'vhd (default)' if ($profiles['VolumeType']) { $volumeType = ([string]$profiles['VolumeType']).ToLowerInvariant() } if ($volumeType -ne 'vhdx') { New-FslFinding -Category Configuration -Check 'VolumeType' -Severity Warning -Target $target ` -Message ("New containers are created as '{0}'; VHDX is the recommended modern format (more resilient, larger maximum size)." -f $volumeType) ` -Recommendation 'Set VolumeType=vhdx explicitly. Existing VHD containers can be converted with frx migrate-vhd.' ` -HelpUri $configHelp } # --- Failure masking and profile collisions -------------------------------- if ($null -eq $profiles['DeleteLocalProfileWhenVHDShouldApply'] -or [int]$profiles['DeleteLocalProfileWhenVHDShouldApply'] -eq 0) { New-FslFinding -Category Configuration -Check 'Local profile collision' -Severity Warning -Target $target ` -Message 'DeleteLocalProfileWhenVHDShouldApply=0: an existing local profile blocks the container attach (session Reason=3) and the user silently keeps the local profile.' ` -Evidence 'This is one of the most common causes of "my settings are gone" tickets on rebuilt or reused hosts.' ` -Recommendation 'Set DeleteLocalProfileWhenVHDShouldApply=1 after verifying no needed data lives in local profiles.' ` -HelpUri $configHelp } else { New-FslFinding -Category Configuration -Check 'Local profile collision' -Severity Info -Target $target ` -Message 'DeleteLocalProfileWhenVHDShouldApply=1: existing local profiles are PERMANENTLY deleted when the container should apply.' ` -Recommendation 'Correct for most deployments - just be aware of the data-loss semantics when reusing hosts that held real local profiles.' ` -HelpUri $configHelp } $preventFailure = ($null -ne $profiles['PreventLoginWithFailure'] -and [int]$profiles['PreventLoginWithFailure'] -eq 1) $preventTemp = ($null -ne $profiles['PreventLoginWithTempProfile'] -and [int]$profiles['PreventLoginWithTempProfile'] -eq 1) if (-not $preventFailure -and -not $preventTemp) { New-FslFinding -Category Configuration -Check 'Failure masking' -Severity Warning -Target $target ` -Message 'PreventLoginWithFailure=0 and PreventLoginWithTempProfile=0: when the container attach fails, users silently work in a temp/local profile and changes are lost at logoff.' ` -Evidence 'Correlate with session Reason=7 (temp profile) and [ERROR:...] log lines to find affected users.' ` -Recommendation 'Set both to 1 in production so attach failures surface immediately instead of costing user data.' ` -HelpUri $configHelp } # --- Profile/ODFC double configuration ------------------------------------- if ($odfc -and $odfc['Enabled'] -eq 1 -and $profiles['Enabled'] -eq 1) { $odfcLocations = @(ConvertTo-FslLocationList -Value $odfc['VHDLocations']) $overlap = @($locations | Where-Object { $odfcLocations -contains $_ }) if ($overlap.Count -gt 0) { New-FslFinding -Category Configuration -Check 'Profile/ODFC overlap' -Severity Warning -Target $target ` -Message 'Profile container and ODFC container are both enabled and share the same VHDLocations.' ` -Evidence ("Overlapping location(s): {0}" -f ($overlap -join ', ')) ` -Recommendation 'Running both is only supported in specific designs; ODFC data is already inside the profile container by default. Verify this is intentional.' ` -HelpUri 'https://learn.microsoft.com/en-us/fslogix/concepts-container-types' } } # --- Login-hang tuning ------------------------------------------------------ # Defaults per Microsoft: LockedRetryCount=12, LockedRetryInterval=5 (60s worst case). $retryCount = 12 if ($null -ne $profiles['LockedRetryCount']) { $retryCount = [int]$profiles['LockedRetryCount'] } $retryInterval = 5 if ($null -ne $profiles['LockedRetryInterval']) { $retryInterval = [int]$profiles['LockedRetryInterval'] } $waitSeconds = $retryCount * $retryInterval if ($waitSeconds -gt 120) { New-FslFinding -Category Configuration -Check 'Locked-container retry time' -Severity Warning -Target $target ` -Message ("A locked container makes logins hang for up to {0} seconds (LockedRetryCount={1} x LockedRetryInterval={2}s) before failing." -f $waitSeconds, $retryCount, $retryInterval) ` -Recommendation 'Reduce the retry window so users fail fast instead of staring at a hung login screen.' ` -HelpUri $configHelp } elseif ($retryCount -eq 0) { New-FslFinding -Category Configuration -Check 'Locked-container retry time' -Severity Warning -Target $target ` -Message 'LockedRetryCount=0: a transient lock on the container fails the attach immediately (no retries).' ` -Recommendation 'Keep a small retry budget (default 12 x 5s) to ride out transient SMB locks.' ` -HelpUri $configHelp } # --- Stale/obsolete settings -------------------------------------------------- if ($profiles.ContainsKey('ConcurrentUserSessions')) { New-FslFinding -Category Configuration -Check 'Obsolete setting' -Severity Info -Target $target ` -Message 'ConcurrentUserSessions is set, but Microsoft removed this setting from the product - it has no effect.' ` -Evidence 'Its presence usually means configuration carried over from an old gold image or GPO.' ` -Recommendation 'Remove the value to keep the configuration honest.' ` -HelpUri 'https://learn.microsoft.com/en-us/fslogix/concepts-multi-concurrent-connections' } # --- Logging configuration ----------------------------------------------------- $logging = $ConfigSnapshot.Logging if ($logging) { if ($null -ne $logging['LoggingEnabled'] -and [int]$logging['LoggingEnabled'] -eq 0) { New-FslFinding -Category Configuration -Check 'Text logging' -Severity Warning -Target $target ` -Message 'LoggingEnabled=0: FSLogix writes no text logs, which removes the most important diagnostic source on this host.' ` -Recommendation 'Set LoggingEnabled=2 (all components).' -HelpUri $configHelp } if ($null -ne $logging['LogFileKeepingPeriod'] -and [int]$logging['LogFileKeepingPeriod'] -lt 7) { New-FslFinding -Category Configuration -Check 'Log retention' -Severity Info -Target $target ` -Message ("LogFileKeepingPeriod={0} days (default 2) - often too short to investigate incidents reported after a weekend." -f $logging['LogFileKeepingPeriod']) ` -Recommendation 'Raise to at least 7-14 days (maximum 180).' -HelpUri $configHelp } } # --- Antivirus exclusions ---------------------------------------------------- if ($ConfigSnapshot.DefenderExclusions -is [array] -and $locations.Count -gt 0) { $exclusions = @($ConfigSnapshot.DefenderExclusions | Where-Object { $_ }) $unexcluded = @() foreach ($location in $locations) { $covered = $false foreach ($exclusion in $exclusions) { # Covered only when the location IS the exclusion or lies UNDER it # (with a path boundary). An exclusion inside the location covers a # subfolder only, and '\\fs01\prof' must not cover '\\fs01\profiles'. $exclusionRoot = ([string]$exclusion).TrimEnd('\') $locationRoot = $location.TrimEnd('\') if ($locationRoot -eq $exclusionRoot -or $locationRoot -like ($exclusionRoot + '\*')) { $covered = $true break } } if (-not $covered) { $unexcluded += $location } } if ($unexcluded.Count -gt 0) { New-FslFinding -Category Configuration -Check 'Antivirus exclusions' -Severity Warning -Target $target ` -Message 'Profile share paths are not in the Microsoft Defender path exclusions; on-access scanning of container disks degrades logon performance and can cause locks.' ` -Evidence ("Not excluded: {0}" -f ($unexcluded -join ', ')) ` -Recommendation 'Add the Microsoft-recommended FSLogix exclusions.' ` -HelpUri 'https://learn.microsoft.com/en-us/fslogix/overview-prerequisites#configure-antivirus-file-and-folder-exclusions' } else { New-FslFinding -Category Configuration -Check 'Antivirus exclusions' -Severity Pass -Target $target ` -Message 'All configured profile locations are covered by Defender path exclusions.' } } } |