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}>" -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}>" -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 Reply
Want to join the discussion?Feel free to contribute!