Stop Losing RDS Per-User CALs to Case Sensitivity: The Real Fix with msTS* LDAP Rights

Fix RDS CAL duplicates by delegating msTS* write rights to AD and enforcing lowercase usernames at NetScaler Gateway as a bonus.

4 min read
Stop Losing RDS Per-User CALs to Case Sensitivity: The Real Fix with msTS* LDAP Rights

If your Remote Desktop Services (RDS) Per-User CAL count keeps inflating because the same user appears as UserA, usera and USERA, you’re hitting a well-known trap: the licensing server’s local tracking is case-sensitive. The durable fix is to let the licensing server write CAL assignments to Active Directory (AD) which is case-insensitive by delegating write rights on the msTS* attributes.

Additionally, if you use Citrix NetScaler Gateway in front of your environment, you can enforce lowercase usernames before they reach the RDS farm, effectively preventing duplicates at the authentication source.

My problem

  • Symptom: Duplicate CALs for the same user when they log in with different casing.
  • Root cause: Licensing server falls back to its local EDB (TLSLic.edb), which is case-sensitive.
  • Fix: Delegate write permissions on msTSExpireDate, msTSLicenseVersion and msTSManagingLS to the licensing server computer account(s).
  • Bonus: If using NetScaler Gateway, enforce lowercase usernames with an advanced expression policy.
  • Result: Case-insensitive, centralized CAL tracking stored in AD and no duplicate usernames.

Why this works

When the RDS Licensing Server can write CAL assignment data into AD, the license is tied to the user’s Distinguished Name (DN) not the login string. Because AD usernames are case-insensitive, it doesn’t matter whether the user logs in as UserA, usera, or USERA. The same CAL is reused. No duplicates.

Attributes used

  • msTSLicenseVersion - Version of CAL assigned
  • msTSExpireDate - Expiration for temporary CALs
  • msTSManagingLS - GUID of the licensing server

If the server can’t write these, it falls back to local tracking in TLSLic.edb, which is case-sensitive.

Why SELF permission is not enough

A common misunderstanding is thinking:

“But SELF already has write access on these attributes, so it should work.”

Nope. SELF = the user object itself. When RDS assigns CALs, the licensing server, not the user, tries to update msTS*.

Principal Can write msTS* Result
SELF only ❌ Access Denied for licensing server Duplicate CALs
Licensing server computer account ✅ CAL written to AD One CAL per user

Conclusion: SELF doesn’t help. You must grant WriteProperty rights to the licensing server’s computer account(s).

How it works visually

        ┌──────────────────────────┐
        │   User logs in as        │
        │   USERA / UserA / usera  │
        └────────────┬─────────────┘
                     │
          Authentication succeeds (case-insensitive)
                     │
     ┌───────────────▼────────────────┐
     │ RDS Licensing Server           │
     │ tries to assign CAL in AD      │
     └───────────────┬────────────────┘
                     │
         ┌───────────┴────────────┐
         │ Can write msTS* ?      │
         ├─────────────┬──────────┤
         │ YES         │ NO       │
         ▼             ▼
  CAL stored in AD   CAL stored locally
 (case-insensitive)  (case-sensitive ❌)
         │             │
         ▼             ▼
 One CAL per user ✅   Duplicate CALs ❌

Prerequisites

  • RDS Per-User CAL licensing mode.
  • Domain Admin or delegated permissions on OU.
  • Licensing server computer account(s) (e.g. RDSLIC01$).
  • Healthy AD replication.

Step-by-Step (GUI)

  1. Open Active Directory Users and Computers (ADUC) and enable Advanced Features.
  2. Right-click the OU containing your user accounts → Properties → Security → Advanced.
  3. Add your licensing server computer account (e.g. RDSLIC01$).
  4. Grant Write permissions on:
    • msTSExpireDate
    • msTSLicenseVersion
    • msTSManagingLS
  5. Apply and close.
  6. Restart the Remote Desktop Licensing service on the licensing server.

Step-by-Step (PowerShell)

Import-Module ActiveDirectory

$OuDn = "OU=Users,DC=contoso,DC=com"      # OU or domain DN
$LicensingServers = @("CONTOSO\RDSLIC01$")

$schemaNc = (Get-ADRootDSE).schemaNamingContext
$attrs = "msTSExpireDate","msTSLicenseVersion","msTSManagingLS" | ForEach-Object {
    Get-ADObject -LDAPFilter "(lDAPDisplayName=$_)"
                 -SearchBase $schemaNc -Properties schemaIDGUID
}

$ou = [ADSI]"LDAP://$OuDn"
$acl = $ou.ObjectSecurity

foreach ($server in $LicensingServers) {
    $ntAccount = New-Object System.Security.Principal.NTAccount($server)
    $sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier])

    foreach ($attr in $attrs) {
        $guid = New-Object Guid ($attr.schemaIDGUID)
        $rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
            $sid,
            [System.DirectoryServices.ActiveDirectoryRights]::WriteProperty,
            [System.Security.AccessControl.AccessControlType]::Allow,
            $guid,
            [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
        )
        $acl.AddAccessRule($rule) | Out-Null
    }
}

$ou.ObjectSecurity = $acl
$ou.CommitChanges()
Write-Host "Delegation applied to $OuDn for: $($LicensingServers -join ', ')"

ACL example in GUI

To verify in ADUC or ADSI Edit:

  1. Go to the OU or user object.
  2. Open Security → Advanced.
  3. You should see something like:
Principal Type Permission Applies to
RDSLIC01$ Allow Write msTSLicenseVersion Descendant User objects
RDSLIC01$ Allow Write msTSExpireDate Descendant User objects
RDSLIC01$ Allow Write msTSManagingLS Descendant User objects
SELF Allow (optional)

This confirms your licensing server can update the attributes directly in AD.

Enforcing lowercase at NetScaler Gateway (optional)

If your users connect through Citrix NetScaler Gateway, you can enforce lowercase usernames before authentication using an Advanced Policy. This ensures usernames are always sent in a normalized format to your RDS/AD environment even if the user types USERA or UserA.

Example using LOWER() function

  1. Go to AppExpert → Policies in the NetScaler GUI.
  2. Create a Responder or Rewrite Policy.
  3. Use the LOWER() text function to transform the HTTP.REQ.USER.NAME to lowercase.
HTTP.REQ.USER.NAME.SET_TEXT_MODE(LOWER(HTTP.REQ.USER.NAME))
  1. Bind the policy globally or to your Gateway vServer.
  2. Test with various case combinations authentication requests will always be sent in lowercase to AD.

Reference: https://docs.netscaler.com/en-us/citrix-adc/current-release/appexpert/policies-and-expressions/advanced-policy-exp-evaluation-text/basic-text-operations.html

This can dramatically reduce duplicate CAL entries, especially in environments with many user devices or SSO variations.

Verification checklist

  • Restart Remote Desktop Licensing service.
  • Connect with a test user.
  • Check the AD object:
    • msTSLicenseVersion and msTSManagingLS populated.
  • Login with multiple casings only one CAL assigned.
  • No more duplicate entries in Licensing Manager.
  • (Optional) Confirm in NetScaler logs that usernames are lowercase.

Optional: clean up old duplicates

  1. Stop Remote Desktop Licensing service.
  2. Backup and remove C:\Windows\System32\lserver\TLSLic.edb.
  3. Restart and reactivate licensing server.
  4. Future assignments are now stored in AD.

Best Practices

  • Delegate msTS* write rights to your licensing server computer accounts.
  • Enforce lowercase usernames at the NetScaler Gateway level (if applicable).
  • Use Per-Device CALs for kiosk-type devices.
  • Audit CAL usage regularly with PowerShell or LSREPORT.
  • If you rebuild the licensing server, re-add its permissions.

Summary

Method Result
SELF only ❌ Licensing server can’t write → duplicates remain
Delegating to licensing server ✅ Writes CALs to AD → case-insensitive tracking
NetScaler enforced lowercase 🚀 Prevents duplicates at the source
Manual cleanup only ⚠️ Temporary relief, problem comes back
Proper delegation + lowercase + cleanup 💯 Permanent, clean and stable CAL management

Note

Many admins waste time deleting their licensing database over and over again because of duplicate CALs.
The real fix isn’t just cleaning up it’s:

  • Delegating CAL assignment into AD
  • Enforcing lowercase usernames upstream (NetScaler Gateway)
  • Cleaning up old duplicates one last time

Do this once and your CAL reports will stay clean.