Script to create a Kerberos Token Size Report

by Jeremy on December 20, 2013

This PowerShell script will enumerate all user accounts in a Domain, calculate their estimated Token Size and create a report of the top x users in CSV format.

However, before I talk about the script it’s important to provide some background information on Kerberos token size; how to calculate it; and how to manage it.

The Kerberos token size grows depending on the following facts:

  • Amount of direct and indirect (nested) group memberships.
    • Distribution groups are not included in the token, but all security groups are included.
    • All group scopes are included in the token evaluation.
  • Whether or not the user has a SID history, and if so, the number of entries.
  • Authentication method (username/password or multi-factor like Smart Cards).
  • The user is enabled for Kerberos delegation.
  • Local user rights assigned to the user.

If it grows beyond the default maximum allowed size…

  • Access and single sign-on (SSO) to Kerberos enabled services will fail, as well as causing unknown side-effects to other services.
  • You have the option of reducing the Kerberos Token Size, or modifying the MaxTokenSize setting on the computers. Now days increasing the  MaxTokenSize setting is a given, however reducing the Kerberos Token Size is a necessary management task and often focussed around the security group design and implementation, and perhaps the clean-up of SID history.

As per KB327825, use the following formula to determine whether it is necessary to modify the MaxTokenSize value or not:

TokenSize = 1200 + 40d + 8s

This formula uses the following values:

  • d: The number of domain local groups a user is a member of plus the number of universal groups outside the user’s account domain that the user is a member of plus the number of groups represented in security ID (SID) history.
    • Although it’s not documented in KB327825 or any other Microsoft references, I also add the number of global groups outside the user’s account domain that the user is a member of to the “d” calculation of the TokenSize. Whilst the Microsoft methodology is to add universal groups from other domains, it is possible to add global groups too. Therefore it’s important to capture this and correctly include it in the calculation.
  • s: The number of security global groups that a user is a member of plus the number of universal groups in a user’s account domain that the user is a member of.
  • 1200: The estimated value for ticket overhead. This value can vary, depending on factors such as DNS domain name length, client name, and other factors.
    • User rights include rights such as “Log on locally” or “Access this Computer from the network”. The only user rights that are added to an access token are those user rights that are configured on the server that hosts a secured resource. Most of the users are likely to have only two or three user rights on the Exchange server. Administrators may have dozens of user rights. Each user right requires 12 bytes to store it in the token.
    • Token overhead includes multiple fields such as the token source, expiration time, and impersonation information. For example, a typical domain user has no special access or restrictions; token overhead is likely to be between 400 and 500 bytes.
    • Estimated value for ticket overhead can vary depending on factors such as DNS domain name length, client name and other factors.
  • Each group membership adds the group SID to the token together with an additional 16 bytes for associated attributes and information. The maximum possible size for SID is 68 bytes. Therefore, each security group to which a user belongs typically adds 44 bytes to the user’s token size.
    • Domain Local group SIDs are 40 bytes each.
    • Domain Global and Universal groups are 8 bytes each.

You can see here that there is a higher tax to pay for using Domain Local groups. Hence the reason why you often find a greater use of Global and/or Universal groups in larger environments. The AGDLP group design and methodology is good in principle, but needs to be implemented sensibly.

You may also see the formula represented as:

TokenSize = [12 x number of user rights] + [token overhead] + 40d + 8s

…where [12 x number of user rights] + [token overhead] is typically estimated to be 1200 bytes.

The default value of the Kerberos MaxTokenSize will be different based on the Windows Operating System version:

  • 12000 bytes for Windows XP/2003/Vista/2008/7/2008R2
  • 48000 bytes for Windows 8/2012 and above

This can be increased by setting the Kerberos MaxTokenSize (the maximum Kerberos SSPI context token buffer size) registry parameter to a supported maximum value of up to 65335 bytes. Microsoft does NOT recommend increasing this beyond to 48000 bytes. However, blatantly increasing the MaxTokenSize can have further impacts on applications, such as Microsoft Internet Information Services (IIS). Best practice is to review methods to reduce the token size, such as reducing and consolidating group membership, ensuring there is no looping (circular nesting) in groups, and cleaning up SID History, before increasing the MaxTokenSize.

If the MaxTokenSize is to be increased, it’s still extremely important to manage the number of groups each user is a member of. Although 1,024 is the maximum number of security groups that a user can be a member of, it is a best practice to restrict the number to 1,015. This number makes sure that token generation will always succeed because it provides space for up to 9 SIDs of well-known groups that are inserted by the Local Security Authority (LSA) when a user logs onto a computer.

The MaxTokenSize registry value can be deployed via a Group Policy Object (GPO) as per KB938118.

Existing Tools:

  • There is a little known UI application called TokenMaster.exe, which was released back in 2000 by Jeffrey Richter and Jason Clark. It’s very hard to find, so I’ve zipped it up and included it here: TokenMaster.zip
  • Microsoft also has a tool called Tokensz.exe that could also be used in a login script.
  • A 3rd party Active Directory Audit Tool called Gold Finger lets you view the complete access token of any users Active Directory domain user account.

References:

Now we get to the fun part…the script icon smile Script to create a Kerberos Token Size Report

The script is fully documented and shows that I’ve been extremely thorough in all aspects of testing to produce the most accurate and error free results. I’ve had some great feedback so far that’s helped me tweak this for different environments. But like anything, my knowledge is based on experience and research, so I welcome your feedback if there is something you feel I could improve on. Refer to the end of this article for information on how to tune your PowerShell environment to avoid the “OutOfMemory Exception” error.

The script has some variables that can be set:

  • In a large environment you don’t need to export the whole user list and their token size, etc, but rather focus on the top x users with potential issues. So set the $TopUsers variable to the number of users with large tokens that you want to report on. For example: $TopUsers = 200
  • In a large environment with tens of thousands of users the $array of objects will grow very large and may cause memory issues, so we use the $TokensSizeThreshold variable to set a threshold size in bytes that you want to start to capture the user information into the $array for the report.
  • In large environments the script can take hours to complete, so I’ve implemented a progress bar and a count with percent complete calculation. This is so that you can monitor it and know where it’s up to. These are controlled by the $ProgressBar and $ConsoleOutput variables. Once you’re comfortable with the script, you could turn these off and run the script as a scheduled task on a regular basis as part of your administrative reporting tasks.
  • When the script completes it will write a summary to the console. This is controlled by the $OutputSummary variable.
  • Leave the $UseTokenGroups and $UseGetAuthorizationGroups as they are. Make sure you read the documentation in the script before you make changes to these variables.

IMPORTANT: The current release of this script does not report on cross-forest/domain group memberships as neither the tokenGroups attribute or GetAuthorizationGroups() method can achieve this. Therefore reporting on Global and Universal security groups outside the users Domain will always report as 0.

The screen shots shown in this post are from a recent health check I completed in a large environment with 8228 enabled user accounts. Disabled user accounts are excluded. Review the LDAP filter in the script.

The screen shot below shows the progress bar and the information calculated per user. As mentioned, this output can be turned on and off by the $ProgressBar and $ConsoleOutput variables.

KerberosTokenSize Script to create a Kerberos Token Size Report

The screen show below shows the summary report produced when the script completes. As mentioned, this output can also be turned on and off by the $OutputSummary variable.

KerberosTokenSizeSummary Script to create a Kerberos Token Size Report

The screen shot below shows the output of the csv file that has been imported into Excel, with an overlay from the report I completed for the customer in a Word table format. As you can see here their issue was group memberships of Domain Local groups, so I didn’t need to include all columns when presenting this to the customer, just enough to demonstrate the root cause of their Kerberos Token issues.

KerberosTokenSizeReport Script to create a Kerberos Token Size Report

Here is the Get-TokenSizeReport.ps1 script:

<#
  This script will enumerate all user accounts in a Domain, calculate their estimated Token Size and create
  a report of the top x users in CSV format.

  Script Name: Get-TokenSizeReport.ps1
  Release 1.8
  Modified by Jeremy@jhouseconsulting.com 31/12/2013
  Written by Jeremy@jhouseconsulting.com 02/12/2013

  Original script was derived from the CheckMaxTokenSize.ps1 written by Tim Springston [MS] on 7/19/2013

http://gallery.technet.microsoft.com/scriptcenter/Check-for-MaxTokenSize-520e51e5

  Re-wrote the script to be more efficient and provide a report for all users in the
  Domain.

  References:
  - Microsoft KB327825: Problems with Kerberos authentication when a user belongs to many groups

http://support.microsoft.com/kb/327825

  - Microsoft KB243330: Well-known security identifiers in Windows operating systems

http://support.microsoft.com/kb/243330

  - Microsoft KB328889: Users who are members of more than 1,015 groups may fail logon authentication

http://support.microsoft.com/kb/328889

  - Microsoft KB938118: How to use Group Policy to add the MaxTokenSize registry entry to multiple computers

http://support.microsoft.com/kb/938118

  - Microsoft Blog: Managing Token Bloat:

http://blogs.dirteam.com/blogs/sanderberkouwer/archive/2013/05/22/common-challenges-when-managing-active-directory-domain-services-part-2-unnecessary-complexity-and-token-bloat.aspx

  Although it's not documented in KB327825 or any other Microsoft references, I also add the number of
  global groups outside the user's account domain that the user is a member of to the "d" calculation of
  the TokenSize. Whilst the Microsoft methodology is to add universal groups from other domains, it is
  possible to add global groups too. Therefore it's important to capture this and correctly include it in
  the calculation.

  For users with large tokens consider reducing direct and transitive (nested) group memberships.
  Larger environments that have evolved over time also have a tendancy to suffer from Circular Group
  Nesting and sIDHistory.

  On the odd ocasion I was receiving the following error:
  - Exception calling "FindByIdentity" with "2" argument(s): "Multiple principals contain
    a matching Identity."
  - There seemed to be a known bug in .NET 4.x when passing two arguments to the FindByIdentity() method.
  - The solution was to either use a machine with .NET 3.5 or re-write the script to pass
    three arguments as per the Get-UserPrincipal function provided in the following Scripting Guy article:
    - http://blogs.technet.com/b/heyscriptingguy/archive/2009/10/08/hey-scripting-guy-october-8-2009.aspx
    This function passes the Context Type, FQDN Domain Name and Parent OU/Container.
  - Other references:
    - http://richardspowershellblog.wordpress.com/2008/05/27/account-management-member-of/
    - http://www.powergui.org/thread.jspa?threadID=20194

  I have also seen the following error:
  - Exception calling "GetAuthorizationGroups" with "0" argument(s): "An error (1301) occurred while
    enumerating the groups. The group's SID could not be resolved."
  - Other references:
    - http://richardspowershellblog.wordpress.com/2008/05/27/account-management-member-of/
    - https://groups.google.com/forum/#!topic/microsoft.public.adsi.general/jX3wGd0JPOo
    - http://lucidcode.com/2013/02/18/foreign-security-groups-in-active-directory/

  Added the tokenGroups attribute to get all nested groups as I could not achieve 100% reliability using
  the GetAuthorizationGroups() method. Could not afford for it to start failing after running for hours
  in large environments.
  - References:
    - http://www.msxfaq.de/code/tokengroup.htm
    - http://www.msxfaq.de/tools/dumpticketsize.htm

  There are important differences between using the GetAuthorizationGroups() method versus the tokenGroups
  attribute that need to be understood. Aside from the unreliability of GetAuthorizationGroups(), when push
  comes to shove you get different results depending on which method you use, and what you want to achieve.
    - The tokenGroups attribute only contains the actual "Active Directory" principals, which are groups and
      siDHistory.
    - However, tokenGroups does not reveal cross-forest/domain group memberships. The tokenGroups attribute
      is constructed by Active Directory on request, and this depends on the availability of a Global Catalog
      server: http://msdn.microsoft.com/en-us/library/ms680275(VS.85).aspx
    - The GetAuthorizationGroups() method also returns the well-known security identifiers of the local
      system (LSALogonUser) for the user running the script, which will include groups such as:
      - Everyone (S-1-1-0)
      - Authenticated Users (S-1-5-11)
      - This Organization (S-1-5-15)
      - Low Mandatory Level (S-1-16-4096)
      This will vary depending on where you're running the script from and in what user context. The result
      is still consistent, as it adds the same overhead to each user. But this is misleading.
    - GetAuthorizationGroups() will return cross-forest/domain group memberships, but cannot resolve them
      because they contain a ForeignSecurityPrincipal. It therefore fails as documented above.
    - GetAuthorizationGroups() does not contain siDHistory.

  In my view you would use the tokenGroups attribute to collate a consistent and accurate user report across
  the environment, whereas the GetAuthorizationGroups() method could be used in a logon script to calucate
  the token of the user together with the system they are logging on to. The actual calculation of the token
  size adds the estimated value for ticket overhead anyway, hence the reason why using the tokenGroups
  attribute provides a consistent result for all users.
  If you wanted an accurate token size per user per system and GetAuthorizationGroups() method continues to
  prove to be unreliable, you could use the tokenGroups attribute together with the addition of the output
  from the "whoami /groups" command to get all the well-known groups and label needed to calculate the
  complete local token.

  Microsoft also has a tool called Tokensz.exe that could also be used in a logon script. It can be downloded
  from here: http://www.microsoft.com/download/en/details.aspx?id=1448

  To be completed:
  - Some further research and testing needs to be completed with the code that retrieves the tokenGroups
    attribute to validate performance between the GetInfoEx method or RefreshCache method.
  - Work out how to report on cross-forest/domain group memberships as neither tokenGroups or
    GetAuthorizationGroups() can achieve this.
#>

#-------------------------------------------------------------

# Set this value to the number of users with large tokens that
# you want to report on.
$TopUsers = 200

# Set this to the size in bytes that you want to start to capture
# the user information for the report.
$TokensSizeThreshold = 6000
# Note that if we don't set a threshold high enough for large
# environments the $array of objects will grow too large and may
# cause memory issues.

# Set this value to true if you want to see the progress bar.
$ProgressBar = $True

# Set this value to true if you want to output to the console
$ConsoleOutput = $True

# Set this value to true if you want a summary output to the
# console when the script has completed.
$OutputSummary = $True

# Set this value to true to use the tokenGroups attribute
$UseTokenGroups = $True

# Set this value to true to use the GetAuthorizationGroups() method
$UseGetAuthorizationGroups = $False

#-------------------------------------------------------------

# Get the script path
$ScriptPath = {Split-Path $MyInvocation.ScriptName}
$ReferenceFile = $(&$ScriptPath) + "\KerberosTokenSizeReport.csv"

Function Get-UserPrincipal($cName, $cContainer, $userName)
{
  $dsam = "System.DirectoryServices.AccountManagement"
  $rtn = [reflection.assembly]::LoadWithPartialName($dsam)
  $cType = "domain" #context type
  $iType = "SamAccountName"
  $dsamUserPrincipal = "$dsam.userPrincipal" -as [type]
  $principalContext = new-object "$dsam.PrincipalContext"($cType,$cName,$cContainer)
  $dsamUserPrincipal::FindByIdentity($principalContext,$iType,$userName)
} # end Get-UserPrincipal

Function Test-DotNetFrameWork35
{
 Test-path -path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5'
} #end Test-DotNetFrameWork35

If(-not(Test-DotNetFrameWork35)) { "Requires .NET Framework 3.5" ; exit }

$array = @()
$TotalUsersProcessed = 0
$UserCount = 0
$GroupCount = 0
$LargestTokenSize = 0
$TotalGoodTokens = 0
$TotalTokensBetween8and12K = 0
$TotalLargeTokens = 0
$TotalVeryLargeTokens = 0

$ADRoot = ([System.DirectoryServices.DirectoryEntry]"LDAP://RootDSE")
$DefaultNamingContext = $ADRoot.defaultNamingContext

# Derive FQDN Domain Name
$TempDefaultNamingContext = $DefaultNamingContext.ToString().ToUpper()
$DomainName = $TempDefaultNamingContext.Replace(",DC=",".")
$DomainName = $DomainName.Replace("DC=","")

# Create an LDAP search for all enabled users not marked as criticalsystemobjects to avoid system accounts
$ADFilter = "(&(objectClass=user)(objectcategory=person)(!userAccountControl:1.2.840.113556.1.4.803:=2)(!(isCriticalSystemObject=TRUE))(!name=IUSR*)(!name=IWAM*)(!name=ASPNET))"
$ADPropertyList = @("distinguishedname","samAccountName","userAccountControl","objectsid","sidhistory","primaryGroupID","primarygrouptoken","lastlogontimestamp","memberof")
$ADScope = "SUBTREE"
$ADPageSize = 1000
$ADSearchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$($DefaultNamingContext)")
$ADSearcher = New-Object System.DirectoryServices.DirectorySearcher
$ADSearcher.SearchRoot = $ADSearchRoot
$ADSearcher.PageSize = $ADPageSize
$ADSearcher.Filter = $ADFilter
$ADSearcher.SearchScope = $ADScope
if ($ADPropertyList) {
  foreach ($ADProperty in $ADPropertyList) {
    [Void]$ADSearcher.PropertiesToLoad.Add($ADProperty)
  }
}
$Users = $ADSearcher.Findall()
$UserCount = $users.Count

if ($UserCount -ne 0) {
  foreach($user in $users) {
    #$user.Properties
    #$user.Properties.propertynames
    $lastLogonTimeStamp = ""
    $lastLogon = ""
    $UserDN = $user.Properties.distinguishedname[0]
    $samAccountName = $user.Properties.samaccountname[0]
    If (($user.Properties.lastlogontimestamp | Measure-Object).Count -gt 0) {
      $lastLogonTimeStamp = $user.Properties.lastlogontimestamp[0]
      $lastLogon = [System.DateTime]::FromFileTime($lastLogonTimeStamp)
      if ($lastLogon -match "1/01/1601") {$lastLogon = "Never logged on before"}
    } else {
      $lastLogon = "Never logged on before"
    }
    $OU = $user.GetDirectoryEntry().Parent
    $OU = $OU -replace ("LDAP:\/\/","")

    # Get user SID
    $arruserSID = New-Object System.Security.Principal.SecurityIdentifier($user.Properties.objectsid[0], 0)
    $userSID = $arruserSID.Value

    # Get the SID of the Domain the account is in
    $AccountDomainSid = $arruserSID.AccountDomainSid.Value

    # Get User Account Control & Primary Group by binding to the user account
    $objUser = [ADSI]("LDAP://" + $UserDN)
    $UACValue = $objUser.useraccountcontrol[0]
    $primarygroupID = $objUser.PrimaryGroupID
    # Primary group can be calculated by merging the account domain SID and primary group ID
    $primarygroupSID = $AccountDomainSid + "-" + $primarygroupID.ToString()
    $primarygroup = [adsi]("LDAP://<SID=$primarygroupSID>")
    $primarygroupname = $primarygroup.name
    $objUser = $null

    # Get SID history
    $SIDCounter = 0
    if ($user.Properties.sidhistory -ne $null) {
      foreach ($sidhistory in $user.Properties.sidhistory) {
        $SIDHistObj = New-Object System.Security.Principal.SecurityIdentifier($sidhistory, 0)
        #Write-Host -ForegroundColor green $SIDHistObj.Value "is in the SIDHistory."
        $SIDCounter++
      }
    }
    $SIDHistObj = $null

    $TotalUsersProcessed ++
    If ($ProgressBar) {
      Write-Progress -Activity 'Processing Users' -Status ("Username: {0}" -f $samAccountName) -PercentComplete (($TotalUsersProcessed/$UserCount)*100)
    }

    # Use TokenGroups Attribute
    If ($UseTokenGroups) {
      $UserAccount = [ADSI]"$($User.Path)"
      $UserAccount.GetInfoEx(@("tokenGroups"),0) | Out-Null
      $ErrorActionPreference = "continue"
      $error.Clear()
      $groups = $UserAccount.GetEx("tokengroups")
      if ($Error) {
        Write-Warning "  Tokengroups not readable"
        $Groups=@()   #empty enumeration
      }
      $GroupCount = 0
      # Note that the tokengroups includes all principals, which includes siDHistory, so we need
      # to subtract the sIDHistory count to correctly report on the number of groups in the token.
      $GroupCount = $groups.count - $SIDCounter

      $SecurityDomainLocalScope = 0
      $SecurityGlobalInternalScope = 0
      $SecurityGlobalExternalScope = 0
      $SecurityUniversalInternalScope = 0
      $SecurityUniversalExternalScope = 0

      foreach($token in $groups) {
        $principal = New-Object System.Security.Principal.SecurityIdentifier($token,0)
        $GroupSid = $principal.value
        #$group = $principal.Translate([System.Security.Principal.NTAccount])
        #$group.value
        $grp = [ADSI]"LDAP://<SID=$GroupSid>"
        if ($grp.Path -ne $null) {
          $grpdn = $grp.distinguishedName.tostring().ToLower()
          $grouptype = $grp.groupType.psbase.value

          switch -exact ($GroupType) {
            "-2147483646"   {
                            # Global security scope
                            if ($GroupSid -match $DomainSID)
                            {
                              $SecurityGlobalInternalScope++
                            } else {
                              # Global groups from others.
                              $SecurityGlobalExternalScope++
                            }
                            }
            "-2147483644"   {
                            # Domain Local scope
                            $SecurityDomainLocalScope++
                            }
            "-2147483643"   {
                            # Domain Local BuildIn scope
                            $SecurityDomainLocalScope++
                            }
            "-2147483640"   {
                            # Universal security scope
                            if ($GroupSid -match $AccountDomainSid)
                            {
                              $SecurityUniversalInternalScope++
                            } else {
                              # Universal groups from others.
                              $SecurityUniversalExternalScope++
                            }
                            }
          }
        }
      }
    }

    # Use GetAuthorizationGroups() Method
    If ($UseGetAuthorizationGroups) {

      $userPrincipal = Get-UserPrincipal -userName $SamAccountName -cName $DomainName -cContainer "$OU"

      $GroupCount = 0
      $SecurityDomainLocalScope = 0
      $SecurityGlobalInternalScope = 0
      $SecurityGlobalExternalScope = 0
      $SecurityUniversalInternalScope = 0
      $SecurityUniversalExternalScope = 0

      # Use GetAuthorizationGroups() for Indirect Group MemberShip, which includes all Nested groups and the Primary group
      Try {
        $groups = $userPrincipal.GetAuthorizationGroups() | select SamAccountName, GroupScope, SID

        $GroupCount = $groups.count

        foreach ($group in $groups) {
          $GroupSid = $group.SID.value
          #$group

          switch ($group.GroupScope)
            {
              "Local" {
                # Domain Local & Domain Local BuildIn scope
                $SecurityDomainLocalScope++
                }
              "Global" {
                # Global security scope
                if ($GroupSid -match $DomainSID) {
                  $SecurityGlobalInternalScope++
                } else {
                  # Global groups from others.
                  $SecurityGlobalExternalScope++
                }
              }
                "Universal" {
                # Universal security scope
                if ($GroupSid -match $AccountDomainSid) {
                  $SecurityUniversalInternalScope++
                } else {
                  # Universal groups from others.
                  $SecurityUniversalExternalScope++
                }
              }
            }
        }
      }
      Catch {
        write-host "Error with the GetAuthorizationGroups() method: $($_.Exception.Message)" -ForegroundColor Red
      }
    }

    If ($ConsoleOutput) {
      Write-Host -ForegroundColor green "Checking the token of user $SamAccountName in domain $DomainName"
      Write-Host -ForegroundColor green "There are $GroupCount groups in the token."
      Write-Host -ForegroundColor green "- $SecurityDomainLocalScope are domain local security groups."
      Write-Host -ForegroundColor green "- $SecurityGlobalInternalScope are domain global scope security groups inside the users domain."
      Write-Host -ForegroundColor green "- $SecurityGlobalExternalScope are domain global scope security groups outside the users domain."
      Write-Host -ForegroundColor green "- $SecurityUniversalInternalScope are universal security groups inside the users domain."
      Write-Host -ForegroundColor green "- $SecurityUniversalExternalScope are universal security groups outside the users domain."
      Write-host -ForegroundColor green "The primary group is $primarygroupname."
      Write-host -ForegroundColor green "There are $SIDCounter SIDs in the users SIDHistory."
      Write-Host -ForegroundColor green "The current userAccountControl value is $UACValue."
    }

    $TrustedforDelegation = $false
    if ((($UACValue -bor 0x80000) -eq $UACValue) -OR (($UACValue -bor 0x1000000) -eq $UACValue)) {
      $TrustedforDelegation = $true
    }

    # Calculate the current token size, taking into account whether or not the account is trusted for delegation or not.
    $TokenSize = 1200 + (40 * ($SecurityDomainLocalScope + $SecurityGlobalExternalScope + $SecurityUniversalExternalScope + $SIDCounter)) + (8 * ($SecurityGlobalInternalScope  + $SecurityUniversalInternalScope))
    if ($TrustedforDelegation -eq $false) {
      If ($ConsoleOutput) {
        Write-Host -ForegroundColor green "Token size is $Tokensize and the user is not trusted for delegation."
      }
    } else {
      $TokenSize = 2 * $TokenSize
      If ($ConsoleOutput) {
        Write-Host -ForegroundColor green "Token size is $Tokensize and the user is trusted for delegation."
      }
    }

    If ($TokenSize -le 12000) {
      $TotalGoodTokens ++
      If ($TokenSize -gt 8192) {
        $TotalTokensBetween8and12K ++
      }
    } elseIf ($TokenSize -le 48000) {
      $TotalLargeTokens ++
    } else {
      $TotalVeryLargeTokens ++
    }

    If ($TokenSize -gt $LargestTokenSize) {
      $LargestTokenSize = $TokenSize
      $LargestTokenUser = $SamAccountName
    }

    If ($TokenSize -ge $TokensSizeThreshold) {
      $obj = New-Object -TypeName PSObject
      $obj | Add-Member -MemberType NoteProperty -Name "Domain" -value $DomainName
      $obj | Add-Member -MemberType NoteProperty -Name "SamAccountName" -value $SamAccountName
      $obj | Add-Member -MemberType NoteProperty -Name "TokenSize" -value $TokenSize
      $obj | Add-Member -MemberType NoteProperty -Name "Memberships" -value $GroupCount
      $obj | Add-Member -MemberType NoteProperty -Name "DomainLocal" -value $SecurityDomainLocalScope
      $obj | Add-Member -MemberType NoteProperty -Name "GlobalInternal" -value $SecurityGlobalInternalScope
      $obj | Add-Member -MemberType NoteProperty -Name "GlobalExternal" -value $SecurityGlobalExternalScope
      $obj | Add-Member -MemberType NoteProperty -Name "UniversalInternal" -value $SecurityUniversalInternalScope
      $obj | Add-Member -MemberType NoteProperty -Name "UniversalExternal" -value $SecurityUniversalExternalScope
      $obj | Add-Member -MemberType NoteProperty -Name "SIDHistory" -value $SIDCounter
      $obj | Add-Member -MemberType NoteProperty -Name "UACValue" -value $UACValue
      $obj | Add-Member -MemberType NoteProperty -Name "TrustedforDelegation" -value $TrustedforDelegation
      $obj | Add-Member -MemberType NoteProperty -Name "LastLogon" -value $lastLogon
      $array += $obj
    }

    If ($ConsoleOutput) {
      $percent = "{0:P}" -f ($TotalUsersProcessed/$UserCount)
      write-host -ForegroundColor green "Processed $TotalUsersProcessed of $UserCount user accounts = $percent complete."
      Write-host " "
    }
  }

  If ($OutputSummary) {
    Write-Host -ForegroundColor green "Summary:"
    Write-Host -ForegroundColor green "- Processed $UserCount user accounts."
    Write-Host -ForegroundColor green "- $TotalGoodTokens have a calculated token size of less than or equal to 12000 bytes."
    If ($TotalGoodTokens -gt 0) {
      Write-Host -ForegroundColor green "  - These users are good."
    }
    If ($TotalTokensBetween8and12K -gt 0) {
      Write-Host -ForegroundColor green "  - Although $TotalTokensBetween8and12K of these user accounts have tokens above 8K and should therefore be reviewed."
    }
    Write-Host -ForegroundColor green "- $TotalLargeTokens have a calculated token size larger than 12000 bytes."
    If ($TotalLargeTokens -gt 0) {
      Write-Host -ForegroundColor green "  - These users will be okay if you have increased the MaxTokenSize to 48000 bytes.`n  - Consider reducing direct and transitive (nested) group memberships."
    }
    Write-Host -ForegroundColor red "- $TotalVeryLargeTokens have a calculated token size larger than 48000 bytes."
    If ($TotalVeryLargeTokens -gt 0) {
      Write-Host -ForegroundColor red "  - These users will have problems. Do NOT increase the MaxTokenSize beyond 48000 bytes.`n  - Reduce the direct and transitive (nested) group memberships."
    }
    Write-Host -ForegroundColor green "- $LargestTokenUser has the largest calculated token size of $LargestTokenSize bytes in the $DomainName domain."
  }

  # Write-Output $array | Format-Table
  $array | Sort-Object TokenSize -descending | select-object -first $TopUsers | export-csv -notype -path "$ReferenceFile" -Delimiter ';'

  # Remove the quotes
  (get-content "$ReferenceFile") |% {$_ -replace '"',""} | out-file "$ReferenceFile" -Fo -En ascii
}

Dealing with the “OutOfMemory Exception” error:

If you’re running the script on a Virtual Machine with only 2GB of RAM, you should first increase this to at least 4GB.

When running this script in very large environments, or when a large proportion of your users have a calculated token greater than the $TokensSizeThreshold, the $array of objects grows and consumes all available memory for the shell. When this happens you will receive the “OutOfMemory Exception” error and the script will fail. This is because the PowerShell MaxMemoryPerShellMB quota defaults to only 150MB in versions 1.0 and 2.0, and 1024MB in version 3.0. But even under PowerShell 3.0 large objects can consume all this memory.

The MaxMemoryPerShellMB quota is the maximum amount of memory allocated per shell, including the shell’s child processes.

If you’re still running PowerShell version 2.0, increase it to 1024MB by running the following command from PowerShell as an Admin:

set-item wsman:localhost\Shell\MaxMemoryPerShellMB 1024

If the script continues to fail, increase it to 2048MB.

If you’re running PowerShell version 3.0, increase it to 2048MB by running the following command from PowerShell as an Admin:

set-item wsman:localhost\Shell\MaxMemoryPerShellMB 2048

Then just restart the WinRM service for the change to take effect.

You must also be aware of Microsoft hotfixes such as KB2842230.

This is a typical script you would run from a management or administrative server, so you really should be looking at tuning the PowerShell quota settings that are best for your environment and the needs of the IT Pros you work with.

Enjoy!

Jeremy Professional Shot Sept 2010 white Script to create a Kerberos Token Size Report

Jeremy

Independent Consultant | Contractor | Microsoft & Citrix Specialist | Desktop Virtualization Specialist at J House Consulting
Jeremy is a highly respected, IT geek, with over 28 years’ experience in the industry. He is an independent IT consultant providing expertise to enterprise, corporate, higher education and government clients. His skillset, high ethical standards, integrity, morals and attention to detail, coupled with his friendly nature and exceptional design and problem solving skills, makes him one of the most highly respected and sought after Microsoft and Citrix technical resources in Australia. His alignment with industry and vendor best practices puts him amongst the leaders of his field.
Jeremy Professional Shot Sept 2010 white Script to create a Kerberos Token Size Report
Jeremy Professional Shot Sept 2010 white Script to create a Kerberos Token Size Report
Jeremy Professional Shot Sept 2010 white Script to create a Kerberos Token Size Report
  • Doug Neely

    This script looks great. The only issue I ran into was when it found a user with SID history. When that happened, it returned the following error:

    New-Object : Specified cast is not valid.
    At C:\Proxy\Test\Get-TokenSize.ps1:211 char:21
    + $SIDHistObj = New-Object PSObject -Property $ADObject.Properties
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [New-Object], InvalidCastException
    + FullyQualifiedErrorId : System.InvalidCastException,Microsoft.PowerShell.Commands.NewObjectCommand
    It would then fail to update the $SIDHistObj and report it as zero. Do you know what might be causing this?

    Thanks.

    • http://www.jhouseconsulting.com/ Jeremy

      Hi Doug,

      Thanks for the feedback.

      I have only tested the sIDHistory in my lab environment where I have several accounts with multiple SIDs in their sIDHistory, and it works as expected. From the error you posted it looks like there is an issue with $ADObject. Can you write-host the $AccountDomainSid variable for each user? Is it okay for those users with sIDHistory?

      My lab environment is all 2008 R2 Domain Controllers to match my current customers environment. Did you copy the code or download the text file via the hyperlink? I’ve uploaded an updated version of the script. Could you download that and trying again? If it fails again, I’d like to know more about your environment so that I can track down the issue.

      Cheers,
      Jeremy

  • Al Mulnick

    Thanks for doing putting this together and publishing it. Just a quick note to let you know that while using it, it overran the memory on the system I ran it on. So I made a few changes to the script to be more memory friendly in a large-ish environment. About 40K active accounts and many groups. Changing to have it append the spreadsheet for every pass (about line 394) made it more memory friendly.
    I also really appreciate the detailed notes on your thinking and choices. That has been incredibly helpful.

    Cheers,
    Al

    • http://www.jhouseconsulting.com/ Jeremy

      Hi Al,

      Thanks for your great feedback and idea about appending to the CSV file. I have only just updated this article with the latest version which seems to be more memory friendly, but would love to have a look at integrating your suggestions too. Would you mind sending me a copy?

      Cheers,
      Jeremy

  • CarlWebster

    Feature request: In an extremely large AD this script can take a very long time to run. Would be nice to specify an individual user or security group or OU to process instead of every user in AD. When there are in excess of 250,000 users, this script can take a long time.

    Thanks

    Webster

    • http://wwww.jhouseconsulting.com/ Jeremy Saunders

      Hi Webster,

      Sure, that’s pretty easy to do.

      To run it for a particular user you would change the filter…
      $Username = “jeremy”
      $ADFilter = “(&(samAccountName=$username))”

      To change it to search for all users based on an OU you would set the LDAP path
      $ADSearchRoot = New-Object System.DirectoryServices.DirectoryEntry(“LDAP://OU=Finance,$($DefaultNamingContext)”)

      You may also want to change the search scope from subtree to onelevel
      $ADScope = “OneLevel”

      I’ll play around with some ideas to add optional parameters and let you know.

      Cheers,

      Jeremy

      • CarlWebster

        Thanks. It looks like it is going to take just over nine hours to process 266,069 accounts!

        • http://wwww.jhouseconsulting.com/ Jeremy Saunders

          That’s actually not too bad for that number of users. If Microsoft were doing an ADRAP, they would have the same challenge. It’s typically something you leave running overnight.

          Cheers,
          Jeremy

      • CarlWebster

        The change for one user worked like a champ.

        • http://wwww.jhouseconsulting.com/ Jeremy Saunders

          Awesome. Thanks for testing that.

  • Falko Dohse

    The script is consuming all available memory. I’m running the script on a 16GB Win2k8R2 server and it consumes the complete memory until it fails. The AD consists of only 22.000 users. Any idea what the cause could be?

    • http://wwww.jhouseconsulting.com/ Jeremy Saunders

      Hi Falko,

      The $array object must either be large, or your PowerShell environment may need to be sized correctly.

      To manage the $array object try increasing the value of the $TokensSizeThreshold variable in the script to start with and then work back from there.

      To manage the PowerShell environment you need to understand…
      - What version of PowerShell are you using?
      - What’s the MaxMemoryPerShellMB value set to?
      - Does KB2842230 apply to your environment?

      Hope that helps.

      Cheers,
      Jeremy

      • Falko Dohse

        I’m using PowerShell Version 3.0. The MaxMemoryPerShellMB is set to 4096. The KB2842230 is not installed. I’ve already adjusted the $TokenSizeThreashold to 10000 with no effect at all. The PowerShell process consumes ALL (16GB) available memory until it fails.

        • http://wwww.jhouseconsulting.com/ Jeremy Saunders

          Hi Falko,

          Either KB2842230 applies to your environment or you have another issue that’s causing it to run out of memory. This script has been run on much larger environments without issue. Do you have the same problem If you run it from a different host as the PowerShell process should never consume all that memory?

          Or maybe most of your users have a very large token size? I don’t know anything about your environment, but perhaps most of them have a token size of greater than 10000. Set the $TokenSizeThreashold higher.

          Cheers,
          Jeremy

  • Pingback: Active Directory Health Check, Audit and Remediation Scripts

  • James Kendig

    Jeremy, One issue I see with your script is that the $groups = $UserAccount.GetEx(“tokengroups”) command will only return a max of 1000 groups so the results are not actually accurate.

    • http://wwww.jhouseconsulting.com/ Jeremy Saunders

      Hi James,

      This is not a limitation with my script, but a system limitation with Group Membership as per http://support.microsoft.com/kb/328889. 1,015 or thereabouts is the maximum number of groups a user should be a member of. This is documented in my article. I can’t code around that!

      I have never seen an environment with users in so many groups. That is crazy group design and asking for trouble as Microsoft points out. The screen shot in my article is of an environment at its worst. I would suggest you have bigger issues to address with the group design in your environment.

      You should also look to run a script to assess if you have an issue with circular nested groups. This will add to your problem. I’ve posted a link to a script by Richard Mueller on my blog here:
      http://www.jhouseconsulting.com/2014/05/15/active-directory-health-check-audit-and-remediation-scripts-1301

      What I can do though is create another column in the csv to flag that it’s reached its maximum and wrap more of an explanation around it.

      Typically I only see large group membership issues for Sys Admins and Power Users, so I wouldn’t imagine an environment hitting this limitation for standard users.

      Interested to know more about your environment.

      Thanks for the feedback.

      Cheers,
      Jeremy

  • Mike Crampton

    Hey, thanks for this script, it ran perfectly on the first try. We had several users who were getting a kerberos-security token size error over 12000 earlier in the day. We got them logged on and working by adding the token size registry key as Microsoft documents, but wanted to see if there were any other users likely to have the same issue. Hence your script. However, your script reports that no users in the domain have a token size over 12000, and the largest is 5952 (one of the users who had the issue earlier). Any guesses on why the local computer might report a token size issue if the AD does not think there is?

    • http://wwww.jhouseconsulting.com/ Jeremy Saunders

      Hi Mike,

      Thanks for the feedback.

      Is this a single domain flat forest, or do users belong to groups from other domains/forests? My script only calculates the groups the user belongs to from the current domain. In cases where there is cross-domain/forest group membership, Microsoft suggests doubling my calculated token size, which would pretty much make sense.

      My script doesn’t calculate the token at the local computer level. The Microsoft formula used makes an assumption about that. It’s not accurate, but I can’t see this being an issue.

      So of the users that your computers are saying have a token size of over 12000, what is the break down of their group membership? ie. How many Domain Local groups are they a member of?

      What OS are your DC’s and what the Domain Level they are running at?

      What’s your client OS that’s reporting the token size of over 12000?

      Cheers,
      Jeremy

Previous post:

Next post: