Better together: Login Enterprise & Hydra (Part 1)

Created by AI.

If you haven’t seen the news, Login VSI acquired a new technology: Hydra. Hydra is a powerful Azure Virtual Desktop management and orchestration platform. Its main use cases include image management, auto-scaling, and user session management– in addition to providing a sleeker UI than native Azure.

Hydra has the ability to execute PowerShell scripts against Session Hosts in host pools its managing. This means that custom images aren’t required, and Azure Marketplace images can be used with layered PowerShell customizations. This blog is Part One of a two part series that will highlight how this approach can be used to automate the deployment of Launchers and Session Hosts, all ready to test with Login Enterprise.

The workflow

To test Azure Virtual Desktop, there are two common approaches:

  • Using the Remote Desktop Client
  • Using the Windows App

Login VSI has created templates for both scenarios, but the RDC is preferred in my case. To test Azure Virtual Desktop with a Login Enterprise Launcher, one needs installed a) the Launcher b) the Universal Web Connector and c) the appropriate ‘Connection Scripts’ to automate the connection process via UI.

Hydra has a concept of “Collections” which is a group of PowerShell scripts that are executed in sequence, with built-in error-handling, and the ability to interact with the VM (e.g., Install something then restart the VM). So, Collections will be used to configure the Launcher and Session Host.

Installing the Launcher, UWC, and RDC

The Launcher, Universal Web Connector, and Remote Desktop Client can all be installed using an analogous approach, so I will describe here only the Launcher installation.

The PowerShell script automates the installation by downloading an MSI installer from a specified URL (in this case, hosted in Github) and executing it locally.

The script also creates a Windows Shortcut for the Launcher, which is stored in the “All Users” Startup folder. As you’ll see in the next section, we use a local account configured with SysInternals’ AutoLogon to act as a Launcher service account. When the Autologon user logs in, the Launcher will automatically start, ready for testing.

Here’s the code:

# Set Login Enterprise Details
$serverUrl = "https://<your_login_enterprise_fqdn_here>"
$secret = "<your_launcher_secret_here>" 

# Set Launcher Installation Defaults and Startup Folder Location
$launcherProgramFilesPath = "C:\Program Files\Login VSI\Login Enterprise Launcher"
$targetPath = Join-Path $launcherProgramFilesPath "LoginEnterprise.Launcher.UI.exe"
$shortcutPath = "$env:ALLUSERSPROFILE\Microsoft\Windows\Start Menu\Programs\Startup\LoginEnterpriseLauncherUI.lnk"
$startupFolder = Split-Path -Parent $shortcutPath


####################################################################################################
# Download and Install MSI from GitHub
####################################################################################################
$msiUrl        = "https://<URL_for_launcher_executable>" # E.g. add Setup.msi to public Github Repo
$msiName       = "Setup.msi"
$downloadDir   = "C:\Launcher\Installer"
$msiPath       = Join-Path $downloadDir $msiName

OutputWriter("Starting MSI download and install process.")
OutputWriter("Installer URL: $msiUrl")
OutputWriter("Installer will be saved to: $msiPath")

####################################################################################################
# Create download directory
####################################################################################################
if (-not (Test-Path $downloadDir)) {
    OutputWriter("Creating installer download directory: $downloadDir")
    try {
        New-Item -Path $downloadDir -ItemType Directory -Force | Out-Null
        LogWriter("Created directory $downloadDir")
    } catch {
        OutputWriter("Failed to create directory: $_")
        LogWriter("Directory creation failed: $_")
        exit 1
    }
} else {
    LogWriter("Download directory already exists: $downloadDir")
}

####################################################################################################
# Download MSI
####################################################################################################
OutputWriter("Downloading installer...")
try {
    Invoke-WebRequest -Uri $msiUrl -OutFile $msiPath -UseBasicParsing
    OutputWriter("Download completed.")
    LogWriter("Downloaded $msiName to $msiPath")
} catch {
    OutputWriter("Download failed: $_")
    LogWriter("Download error: $_")
    exit 1
}

####################################################################################################
# Install MSI
####################################################################################################
if (Test-Path $msiPath) {
    OutputWriter("Starting MSI installation...")
    try {
        $arguments = "/i `"$msiPath`" /qn serverurl=$serverUrl secret=$secret"
        LogWriter("Executing: msiexec.exe $arguments")
        $process = Start-Process -FilePath "msiexec.exe" -ArgumentList $arguments -Wait -PassThru

        if ($process.ExitCode -eq 0) {
            OutputWriter("MSI installation succeeded.")
            LogWriter("Installer exit code: 0 (Success)")
        } else {
            OutputWriter("MSI installation failed with exit code: $($process.ExitCode)")
            LogWriter("Installer exit code: $($process.ExitCode)")
            exit $process.ExitCode
        }
    } catch {
        OutputWriter("Installation process failed: $_")
        LogWriter("Installer exception: $_")
        exit 1
    }
} else {
    OutputWriter("MSI file not found at expected path: $msiPath")
    LogWriter("Installer missing: $msiPath")
    exit 1
}
OutputWriter("MSI process completed.")

if (Test-Path $launcherProgramFilesPath) {
    OutputWriter("Launcher installation deemed successful based on installation folder in %PROGRAMFILES%.")
    # exit 0
}

##################################################
# Add Launcher to Startup folder
##################################################
OutputWriter("Starting shortcut creation and Startup placement process.")
OutputWriter("Creating shortcut from: $targetPath")
OutputWriter("Shortcut will be added to $startupFolder")

try {
    
    # Verify the target executable exists
    if (-not (Test-Path -Path $targetPath -PathType Leaf)) {
        throw "Target executable not found: $targetPath"
    }

    # Ensure the Startup folder exists (it should, but just in case)
    if (-not (Test-Path -Path $startupFolder -PathType Container)) {
        throw "Startup folder does not exist: $startupFolder"
    }

    # Create the WScript.Shell COM object
    try {
        $WshShell = New-Object -ComObject WScript.Shell
    }
    catch {
        throw "Unable to create WScript.Shell COM object: $_"
    }

    # Create the shortcut
    $shortcut = $WshShell.CreateShortcut($shortcutPath)

    # Assign properties to the shortcut
    $shortcut.TargetPath       = $targetPath
    $shortcut.Arguments        = $arguments
    $shortcut.WorkingDirectory = Split-Path -Parent $targetPath

    # Save the shortcut to disk
    $shortcut.Save()

    OutputWriter("Shortcut successfully created at: $shortcutPath")
    # LogWriter("Shortcut created!")
}
catch {
    # Write-Error "Failed to create shortcut: $_"
    OutputWriter("Failed to create shortcut! $_")
}

Configuring SysInternals Autologon

The PowerShell script below automates the setup of Windows AutoLogon for a local user account. It downloads the SysInternals’ AutoLogon utility, then checks there is a local user account with the specified $autoLogonUsername exists. Otherwise, it creates one with a randomly generated password. The $autoLogonCount variable controls the number of automatic logons that are configured–each restart of the VM will decrement this value until its zero and no further auto logons will occur.

Here’s the code:

####################################################################################################
####################################################################################################
# Configure AutoLogon
####################################################################################################
####################################################################################################
$autoLogonCount               = "7" # Configure the number of automatic logins here. Currently, this will configure 7 automatic logins.
$autologonDownloadUrl         = "https://download.sysinternals.com/files/AutoLogon.zip"
$autologonDownloadDestination = "C:\Launcher\AutoLogon"
$autologonZipDestination      = Join-Path $autologonDownloadDestination "AutoLogon.zip" # C:\Launcher\AutoLogon\AutoLogon.zip
$autologonUnzipDestination    = Join-Path $autologonDownloadDestination "AutoLogon"     # C:\Launcher\AutoLogon\AutoLogon\
$autologonExePath             = Join-Path $autologonUnzipDestination "AutoLogon64.exe"  # C:\Launcher\AutoLogon\AutoLogon\AutoLogon64.exe

$autologonUsername            = "autologin" # This is the username of the local user account, used for AutoLogon. You may configure this value.
Add-Type -AssemblyName System.Web
$password = [System.Web.Security.Membership]::GeneratePassword(20, 4) # A randomized password is created
$securePass = ConvertTo-SecureString $password -AsPlainText -Force

OutputWriter("Downloading SysInternals' AutoLogon from: $autologonDownloadUrl")
OutputWriter("Archive will be downloaded to: $$autologonUnzipDestination")

OutputWriter("Archive will be extracted to: $autologonUnzipDestination")
OutputWriter("Target executable should be in: $autologonExePath")

##################################################
# Prepare for download and extraction
##################################################
if (-not (Test-Path $autologonDownloadDestination)) {
    OutputWriter("Creating folder to store Autologon download")
    New-Item -Path $autologonDownloadDestination -ItemType Directory -Force | Out-Null
}
else { 
    # OutputWriter("Folder already exists.")
    LogWriter("Autologon download folder already exists.")
}

##################################################
# Download AutoLogon and Extract
##################################################
OutputWriter("Downloading SysInternals' AutoLogon")
if (-not (Test-Path $autologonExePath)) {
    OutputWriter("AutoLogon64.exe not found. Proceeding to download and extract...")
    
    try {
        Invoke-WebRequest -Uri $autologonDownloadUrl -OutFile $autologonZipDestination -UseBasicParsing
        Expand-Archive -Path $autologonZipDestination -DestinationPath $autologonUnzipDestination -Force
        OutputWriter("Download and extraction complete.")
    }
    catch {
        OutputWriter("Failed to download or extract AutoLogon: $_")
        exit 1
    }
} else {
    OutputWriter("AutoLogon already downloaded and extracted.")
}


####################################################################################################
# Create autologon user (if not exists)
####################################################################################################
try {
    if (-not (Get-LocalUser -Name $autologonUsername -ErrorAction SilentlyContinue)) {
        OutputWriter("Creating local user '$autologonUsername'")
        New-LocalUser -Name $autologonUsername -Password $securePass -FullName $autologonUsername -PasswordNeverExpires:$true -UserMayNotChangePassword:$true
        OutputWriter("User '$autologonUsername' created.")
    } else {
        OutputWriter("User '$autologonUsername' already exists.")
    }
}
catch {
    OutputWriter("Failed to create or check user: $_")
    throw "Failed to create or check for user existence: $_"
}

####################################################################################################
# Configure AutoLogon using AutoLogon64.exe
####################################################################################################
if (Test-Path $autologonExePath) {
    try {
        OutputWriter "Running AutoLogon64.exe configuration..."
        Start-Process $autologonExePath -ArgumentList $autologonUsername,$env:COMPUTERNAME,$password,"-accepteula" -Wait
        OutputWriter("AutoLogon configured.")
    }
    catch {
        OutputWriter("Failed to configure AutoLogon: $_")
        exit 1
    }
} else {
    OutputWriter("AutoLogon64.exe not found at expected path: $autologonExePath")
    exit 1
}

####################################################################################################
# Registry configuration
####################################################################################################
OutputWriter("Configuring registry values for AutoLogon...")

$winlogonPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
$policyPath   = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"

# Ensure required policies
try {
    $regSettings = @{
        "$policyPath\dontdisplaylastusername"           = 0
        "$policyPath\DisableAutomaticRestartSignOn"     = 0
        "$winlogonPath\AutoLogonCount"                  = $autoLogonCount
    }

    foreach ($key in $regSettings.Keys) {
        $pathParts = $key.Split('\')
        $regPath = ($pathParts[0..($pathParts.Length - 2)] -join '\')
        $regName = $pathParts[-1]
        $desiredValue = $regSettings[$key]

        $existing = Get-ItemProperty -Path $regPath -Name $regName -ErrorAction SilentlyContinue
        if ($existing.$regName -ne $desiredValue) {
            Set-ItemProperty -Path $regPath -Name $regName -Value $desiredValue
            OutputWriter("Set registry '$regName' to '$desiredValue'")
        } else {
            OutputWriter("Registry '$regName' already set to '$desiredValue'")
        }
    }
}
catch {
    OutputWriter("Failed to update registry keys: $_")
    exit 1
}
OutputWriter("AutoLogon setup complete")

Don’t say I didn’t warn you…

Using AutoLogon as above means that the user’s password is stored in the Windows registry. If an attacker gains access to the system and can read the registry, they could retrieve this password and gain unauthorized access to the account. There is a trade-off between convenience and security when using AutoLogon.

However, all of that said, there is no traditional console access to Azure VMs, and retrieving this password would require admin-level access to the Windows OS. If someone has this access, the system is already compromised, and the local user account is the least of your worries.

Look out!

This was the first installment of this series. In part two I will walkthrough how similar approach can be used to prepare a Session Host for testing in a fully automated, hands-off manner. See you there.

Comments

Leave a comment