Integrating LAPS with Citrix VDI

Overview

The Local Administrator Password Solution is a great tool to help you manage local passwords on your corporate network. However, due to the non-persistant nature of some environments, such as Citrix VDI, it can become a security vulnerability as the local password will always be set back the original password set when the VDI was persistent.

I created this script, Reset-LAPSPasswordExpiration.ps1, which aims to solve this problem by randomly generating a password upon either VDI startup or shutdown and synchronizing those results with the LAPS password store in Active Directory. This requires RSAT to be on the Citrix server because it uses Set-ADComputer.

How To Use This Script

The script can either be run remotely or on the Citrix machine. I recommend using the local start-up procedure as it is most effective. However, I did integrate other abilities if you wish to you modify its deployment in your environment. This script requires the proper version of RSAT be installed on the Citrix machine.

If run it locally on the Citrix server at startup (recommended way):


  Reset-LAPSPasswordExpiration -RunOnLocalHost -logic Startup 

If run local on the Citrix server at shutdown:


  Reset-LAPSPasswordExpiration -RunOnLocalHost -logic Shutdown 

To run against a batch of Citrix servers remotely:


  Reset-LAPSPasswordExpiration -CSVFile Reset-LAPSPasswordExpiration-Import.csv -Logic Shutdown 

To run against a single server remotely:


  Reset-LAPSPasswordExpiration -Hostname CITRIXSERVER001 -Logic Shutdown 

Sample Output:

Logging Output

The script has built in logging functionality which is fully compatible with CMTrace.exe

PowerShell Function: Reset-LAPSPasswordExpiration


<#
.Synopsis
   Reset the LAPS Password Expiration Date
.DESCRIPTION
   The tool, Reset-LAPSPasswordExpiration.ps1 was written by David Maiolo which will reset the LAPS Password expiration date. Useful on non-persistent VDIs
.EXAMPLE
   Reset-LAPSPasswordExpiration -CSVFile laps_computers_import.csv -Logic Startup
.EXAMPLE
   Reset-LAPSPasswordExpiration -Hostname LT061222 -Logic Shutdown
.EXAMPLE
   Reset-LAPSPasswordExpiration -RunOnLocalHost -logic Startup
#>


function New-DGMCMTraceLog
{
  param (
  [Parameter(Mandatory=$true)]
  $message,
  [Parameter(Mandatory=$true)]
  $component,
  [Parameter(Mandatory=$true)]
  $type )

  switch ($type)
  {
    1 { $type = "Info" }
    2 { $type = "Warning" }
    3 { $type = "Error" }
    4 { $type = "Verbose" }
  }

  if (($type -eq "Verbose") -and ($Global:Verbose))
  {
    $toLog = "{0} `$$<{1}><{2} {3}><thread={4}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    Write-Host $message
  }
  elseif ($type -ne "Verbose")
  {
    $toLog = "{0} `$$<{1}><{2} {3}><thread={4}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    if ($type -eq 'Info') { Write-Host $message }
    if ($type -eq 'Warning') { Write-Host $message -ForegroundColor Yellow}
    if ($type -eq 'Error') { Write-Host $message -ForegroundColor Red}
    

  }
  if (($type -eq 'Warning') -and ($Global:ScriptStatus -ne 'Error')) { $Global:ScriptStatus = $type }
  if ($type -eq 'Error') { $Global:ScriptStatus = $type }

  if ((Get-Item $Global:LogFile).Length/1KB -gt $Global:MaxLogSizeInKB)
  {
    $log = $Global:LogFile
    Remove-Item ($log.Replace(".log", ".lo_"))
    Rename-Item $Global:LogFile ($log.Replace(".log", ".lo_")) -Force
  }
} 

function GetScriptDirectory
{
  $invocation = (Get-Variable MyInvocation -Scope 1).Value
  Split-Path $invocation.MyCommand.Path
} 

function Reset-LAPSPasswordExpiration
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0,
                   ParameterSetName='CSV File')]
                   [ValidateScript({(Test-Path $_)})]
                   $CSVFile,
        # Param2 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=1,
                   ParameterSetName='Single Computer')]
                   [ValidateScript({(Get-ADComputer -Identity $_).objectclass -eq 'computer' })]
                   [String]$Hostname,
        # Param3 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=2,
                   ParameterSetName='Local Host')]
                   [Switch]$RunOnLocalHost,
        # Param4 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=3)]
                   [ValidateSet("Shutdown","Startup")]
                   [String]$Logic
    )

    Begin
    {
        
        $path = (get-item -Path .).FullName 
        
        if ($RunOnLocalHost){
            $localhostname = $env:computername
            $csv = [PSCustomObject]@{
                Hostname = $localhostname}
        }
        elseif ($CSVFile -ne $null){
            Write-Host "Importing $CSVFile..."
            $csv = import-csv "$CSVFile"
        }else{
            $csv = [PSCustomObject]@{
                Hostname = $Hostname}
        }
        Write-Host "=========================================="
        Write-Host "LAPS Password Expiration Date Reset Tool"
        Write-Host "=========================================="
        Write-Host "v0.5 (2017-12-26) by dmaiolo"
        New-DGMCMTraceLog -message ("Starting Logging for Reset-LAPSPasswordExpiration") -component "Main()" -type 1 
    }
    Process
    {
    
    $computers = @();
        
    $csv | foreach-object {
        $h = $_.Hostname
    

        if(Test-Connection -ComputerName $h -Count 1 -Quiet){
            
            $comp = Get-ADComputer $h -Properties ms-MCS-AdmPwdExpirationTime
            
            try{
                $currentexpirationtime = $([datetime]::FromFileTime([convert]::ToInt64($comp.'ms-MCS-AdmPwdExpirationTime',10)))
                New-DGMCMTraceLog -message ("$h`: The current LAPS password expiration is $currentexpirationtime.") -component "Main()" -type 1
            }catch{
                New-DGMCMTraceLog -message ("$h`: The current LAPS password has an unknown expiration or is already clear.") -component "Main()" -type 2
            }

            Write-Host "$h`: Resetting ms-MCS-AdmPwdExpirationTime..."
            try{
                Set-ADComputer $h -Clear "ms-MCS-AdmPwdExpirationTime"
                New-DGMCMTraceLog -message ("$h`: ms-MCS-AdmPwdExpirationTime has been cleared succesfully.") -component "Main()" -type 1
            }catch{
                New-DGMCMTraceLog -message ("$h`: ms-MCS-AdmPwdExpirationTime could not be cleared.") -component "Main()" -type 3
            }
            if ($Logic -eq "Startup"){
                New-DGMCMTraceLog -message ("$h`: Startup Sequence Logic Was Initiated.") -component "Main()" -type 1
                Write-Host "$h`: Sleeping 3 seconds..."
                sleep 3

                Write-Host "$h`: Running GPUpdate /Force on $h ..."
                if ($RunOnLocalHost){
                    try{
                        $command = "GPUPdate /Target:Computer /Force"
                        Invoke-Expression -Command:"$command"
                        New-DGMCMTraceLog -message ("$h`: Group Policy Was Succesfully Updated.") -component "Main()" -type 1
                    }catch{
                        New-DGMCMTraceLog -message ("$h`: Group Policy Could Not Update.") -component "Main()" -type 3
                    }
                }else{
                    try{
                        Invoke-GPUpdate -Computer $h -Force
                        New-DGMCMTraceLog -message ("$h`: Group Policy Was Succesfully Updated.") -component "Main()" -type 1
                    }catch{
                        New-DGMCMTraceLog -message ("$h`: Group Policy Could Not Update.") -component "Main()" -type 3
                    }
                }
            }elseif ($Logic -eq "Shutdown"){
                New-DGMCMTraceLog -message ("$h`: Shutdown Sequence Logic Was Initiated. Skipping GPUpdate") -component "Main()" -type 1
            }
            Write-Host "$h`: Sleeping 5 Seconds..."
            sleep 5
            try{
                $currentexpirationtime = $([datetime]::FromFileTime([convert]::ToInt64($comp.'ms-MCS-AdmPwdExpirationTime',10)))
                New-DGMCMTraceLog -message ("$h`: The updated LAPS password expiration is $currentexpirationtime.") -component "Main()" -type 1
            }catch{
                New-DGMCMTraceLog -message ("$h`: The updated LAPS password has an unknown expiration.") -component "Main()" -type 3
            }
        }
        else{
            New-DGMCMTraceLog -message ("$h`: is offline.") -component "Main()" -type 2
        }
       }
    }
    End
    {
       Write-Host "==============================================================="
       Write-Host "Log File of Results Generated at $Global:LogFile VIEW WITH CMTRACE.EXE"
       New-DGMCMTraceLog -message ("Ending Logging for Reset-LAPSPasswordExpiration") -component "Main()" -type 1
    }
}

$VerboseLogging = "true"
[bool]$Global:Verbose = [System.Convert]::ToBoolean($VerboseLogging)
#$Global:LogFile = Join-Path (GetScriptDirectory) "Reset-LAPSPasswordExpiration_$(Get-Date -Format dd-MM-yyyy).log"
$Global:LogFile = "\\vendscr001prd\Scripts\Reset-LAPSPasswordExpiration\Reset-LAPSPasswordExpiration.log"
$Global:MaxLogSizeInKB = 10240
$Global:ScriptName = 'Reset-LAPSPasswordExpiration.ps1' 
$Global:ScriptStatus = 'Success'

Leave a Comment

Your email address will not be published.