In September 2025, I had the opportunity to attend AVD Techfest. That’s where I first heard about AaronLocker. And yes, it’s actually been around for quite a while.
For many years, I have worked in the EUC (End User Computing) field and was already familiar with Software Restrictions and later AppLocker. I have used both. Increasingly, we’re seeing situations where GPOs are no longer used for configuring and hardening session hosts. As a result, using AppLocker has become less practical.
In the session “Securing Your Session Hosts: Easy to implement security tips for Citrix, AVD, Windows365 and Omnissa” by my MVP colleagues Patrick van den Born and Stefan Dingemanse, they presented the use of AaronLocker for hardening images. It was immediately clear to me that I wanted to implement this for Nerdio Manager for Enterprise.
Although Windows Defender Application Control (WDAC) is now available – a solution that is continuously being expanded and improved – I was convinced by the simplicity of AaronLocker. Especially for customers who have not yet implemented WDAC, this can be an alternative to increase security on session hosts. So, I created a solution for Nerdio that can be easily integrated into the image creation process. There are plenty of blogs and articles on the internet that introduce AaronLocker itself, so I will focus on the implementation in Nerdio.
Implementation
The easiest way to provide AaronLocker is via Shell Apps. With Shell Apps, you can create a deployment using three scripts and additional data in a ZIP file.
A Shell App contains a Detect Script, which checks whether the application has already been installed, and at the end of the installation, verifies whether the installation was successful. The Install Script performs the actual installation. The Uninstall Script is practically irrelevant in our case, since we only install/execute AaronLocker during the image creation process.
Since AaronLocker is downloaded directly from the source, we need to be able to make adjustments. For this, there are files in the CustomizationInputs folder, which can be adapted to your needs. We copy these files into the ZIP file, so we can make adjustments as needed and update the ZIP file accordingly.
General

Detect Script
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$basePath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $productCode = 'AaronLocker' $programRegistryPath = Join-Path $basePath $productCode # Check if the application is installed if (!(Test-Path $programRegistryPath)) { $Context.Log("RegistryKey does not exist: " + $programRegistryPath) return $false } # Check if the instaled version has the correct value $installedVersion = (Get-ItemProperty -Path $programRegistryPath -Name 'InstalledVersion').InstalledVersion $Context.Log("InstalledVersion: " + $Context.TargetVersion) $Context.Log("TargetVersion: " + $Context.TargetVersion) if ($installedVersion -eq $Context.TargetVersion) { $Context.Log("Installed version is identical to the target version") return $true } else { $Context.Log("Installed version is not the same as the target version") return $false } |
Install Script
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
#name: AaronLocker #description: Execute AaronLocker and apply configuration files #execution mode: Combined #tags: beckmann.ch, Preview <# Notes: Use this script to execute AaronLocker and apply configuration files. #> <# .SYNOPSIS Installs AaronLocker, creates AppLocker policies and applies them as a local GPO (AppLocker). .PARAMETER Mode 'Audit' or 'Enforce' (Default: 'Audit'). .PARAMETER AaronLockerPath Target path (Default: 'C:\Program Files\AaronLocker'). .NOTES - Requires AppLocker-capable edition (Enterprise/Education/Server). - Backup existing local AppLocker policy as XML backup. #> $ErrorActionPreference = 'Stop' [string]$Mode = 'Enforce' # -- Functions --------------------------------------------------------------- function Start-Unzip { param([string]$zipfile, [string]$outpath) $Null = Add-Type -AssemblyName System.IO.Compression.FileSystem $Null = [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath) } # -- Guardrails --------------------------------------------------------------- # AppLocker Cmdlets available? if (-not (Get-Command Set-AppLockerPolicy -ErrorAction SilentlyContinue)) { throw "AppLocker Cmdlets not found. OS edition must support AppLocker." } # -- Constants ---------------------------------------------------------------- $scriptRoot = $pwd [string]$AaronLockerPath = "$env:ProgramFiles\AaronLocker" $Context.Log("Mode: " + $Mode) $Context.Log("AaronLockerPath: " + $AaronLockerPath) $Context.Log("ScriptRoot: " + $scriptRoot.Path) [string]$aaronLockerUrl = 'https://github.com/microsoft/AaronLocker/archive/refs/heads/main.zip' [string]$aaronLockerZip = Join-Path $scriptRoot 'AaronLocker-main.zip' [string]$srcAaronLockerRoot = Join-Path $scriptRoot 'AaronLocker-main' [string]$srcAaronLockerTool = Join-Path $srcAaronLockerRoot 'AaronLocker' $Context.Log("AaronLockerUrl: " + $aaronLockerUrl) $Context.Log("AaronLockerZip: " + $aaronLockerZip) $Context.Log("SrcAaronLockerRoot: " + $srcAaronLockerRoot) $Context.Log("SrcAaronLockerTool: " + $srcAaronLockerTool) [string]$accessChkUrl = 'https://download.sysinternals.com/files/AccessChk.zip' [string]$accessChkZip = Join-Path $scriptRoot 'AccessChk.zip' [string]$srcAccessChkRoot = Join-Path $scriptRoot 'AccessChk' $Context.Log("AccessChkUrl: " + $accessChkUrl) $Context.Log("AccessChkZip: " + $accessChkZip) $Context.Log("SrcAccessChkRoot: " + $srcAccessChkRoot) [string]$customizationFolder = 'CustomizationInputs' [string]$customizationInputSource = Join-Path $scriptRoot $customizationFolder [string]$customizationInputTarget = Join-Path $AaronLockerPath $customizationFolder $Context.Log("CustomizationInputSource: " + $customizationInputSource) $Context.Log("CustomizationInputTarget: " + $customizationInputTarget) # -- Prepare directories ------------------------------------------------------ $Context.Log("Creating target directory: " + $AaronLockerPath) New-Item -Path $AaronLockerPath -ItemType Directory -Force | Out-Null # -- Download AaronLocker ----------------------------------------------------- $Context.Log("Downloading AaronLocker...") Invoke-WebRequest -Uri $aaronLockerUrl -OutFile $aaronLockerZip if (Test-Path $srcAaronLockerRoot) { Remove-Item -Path $srcAaronLockerRoot -Recurse -Force } # Unpack $Context.Log("Unpacking AaronLocker...") Start-Unzip "$aaronLockerZip" "$scriptRoot" # Copy the relevant folder to destination if (-not (Test-Path $srcAaronLockerTool)) { throw "AaronLocker content not found in the archive." } robocopy $srcAaronLockerTool $AaronLockerPath /E /NFL /NDL /NJH /NJS /NP | Out-Null # -- Download AccessChk (used by AaronLocker) ------------------- $Context.Log("Downloading Sysinternals AccessChk...") Invoke-WebRequest -Uri $accessChkUrl -OutFile $accessChkZip if (Test-Path $srcAccessChkRoot) { Remove-Item -Path $srcAccessChkRoot -Recurse -Force } # Unpack $Context.Log("Unpacking AccessChk...") Start-Unzip "$accessChkZip" "$srcAccessChkRoot" $Context.Log("Copying AccessChk to AaronLocker folder...") $accessExes = Get-ChildItem -Path $srcAccessChkRoot -Include 'AccessChk64a.exe', 'AccessChk64.exe', 'AccessChk.exe' -Recurse -ErrorAction SilentlyContinue if ($accessExes) { foreach ($accessExe in $accessExes) { Copy-Item $accessExe.FullName (Join-Path $AaronLockerPath $accessExe.Name) -Force } } else { $Context.Log("ERROR: AccessChk could not be extracted. AaronLocker may work less accurately without it.") } # -- Copy CustomizationInput files ------------------------------------------ $Context.Log("Copying CustomizationInput files...") $customizationFiles = Get-ChildItem -Path $customizationInputSource -Recurse -File foreach ($file in $customizationFiles) { Copy-Item -Path $file.FullName -Destination $customizationInputTarget -Force } # -- Policies generate -------------------------------------------------------- $policyOutDir = Join-Path $AaronLockerPath 'Outputs' New-Item -Path $policyOutDir -ItemType Directory -Force | Out-Null # Ermittel das Create-Policies-Skript (Namensvariante je nach Repo-Stand) $createScript = Get-ChildItem -Path $AaronLockerPath -Filter '*Create*Policies*.ps1' -Recurse | Select-Object -First 1 if (-not $createScript) { $Context.Log("ERROR: Create-Policies-Skript in $AaronLockerPath not found."); throw "Create-Policies-Skript in $AaronLockerPath not found." } $Context.Log("Generating AppLocker-Policies with $($createScript.Name)...") # Many AaronLocker scripts operate from the working directory - switch to it Push-Location $AaronLockerPath try { # Execute in the current process context & $createScript.FullName -ErrorAction Stop } finally { Pop-Location } # -- Output files detection ------------------------------------------------ # AaronLocker typically creates: AppLockerRules-<Date>-Audit.xml / -Enforce.xml $policyAudit = Get-ChildItem -Path $AaronLockerPath -Filter '*AppLockerRules*-Audit.xml' -Recurse | Sort-Object LastWriteTime -Descending | Select-Object -First 1 $policyEnf = Get-ChildItem -Path $AaronLockerPath -Filter '*AppLockerRules*-Enforce.xml' -Recurse | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $policyAudit -or -not $policyEnf) { # Fallback: any audit/enforce file in the outputs $policyAudit = Get-ChildItem $policyOutDir -Filter '*Audit*.xml' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 $policyEnf = Get-ChildItem $policyOutDir -Filter '*Enforce*.xml' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 } if (($Mode -eq 'Audit' -and -not $policyAudit) -or ($Mode -eq 'Enforce' -and -not $policyEnf)) { throw "Could not find the generated ${Mode}-Policy XML." } $targetPolicy = if ($Mode -eq 'Audit') { $policyAudit.FullName } else { $policyEnf.FullName } $Context.Log("Using Policy: $targetPolicy") # -- Backup of the existing local policy ----------------------------------- $backupDir = Join-Path $AaronLockerPath 'Backups' New-Item -Path $backupDir -ItemType Directory -Force | Out-Null $backupFile = Join-Path $backupDir ("Local-AppLocker-Backup-{0:yyyyMMdd-HHmmss}.xml" -f (Get-Date)) try { (Get-AppLockerPolicy -Local -Xml) | Out-File -FilePath $backupFile -Encoding utf8 -Force $Context.Log("Backup of the local AppLocker policy: $backupFile") } catch { $Context.Log("ERROR: Could not backup existing local policy: $($_.Exception.Message)") } # -- Application Identity Service activation ---------------------------------- try { sc.exe config AppIDSvc start= auto if ((Get-Service AppIDSvc).Status -ne 'Running') { Start-Service AppIDSvc } } catch { $Context.Log("WARNING: Could not start 'Application Identity' service: $($_.Exception.Message)") } # -- Apply policy (local) -------------------------------------------------- # Hinweis: Without -LDAP, Set-AppLockerPolicy acts on the local policy. $Context.Log("Applying AppLocker-${Mode}-Policy locally...") Set-AppLockerPolicy -XmlPolicy $targetPolicy -ErrorAction Stop # Optional: Trigger gpupdate (not mandatory) try { gpupdate.exe /target:computer /force | Out-Null } catch {} $Context.Log("Done. AppLocker ($Mode) is set. Restart recommended.") # Create the registry key and set the version $basePath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $productCode = 'AaronLocker' $programRegistryPath = Join-Path $basePath $productCode New-Item -Path $programRegistryPath -Force -ErrorAction SilentlyContinue New-ItemProperty -Path $programRegistryPath -Name 'InstalledVersion' -Value $Context.TargetVersion -PropertyType String -Force |
Uninstall Script
|
1 |
Write-Host "Uninstall is not required" |
Versions with zip file


Conclusion
Even though AppLocker is no longer state-of-the-art, there are still security updates available at the moment. And to better protect an image and thus session hosts, AppLocker can be an alternative. In this case, AaronLocker can significantly simplify the process of creating and applying rules.
I hope this article helps you implement AaronLocker in your environment.