# provision-kiosk.ps1 — Windows kiosk provisioning wizard # # Turns a Windows Pro/Enterprise/Education machine into a Vestibule # kiosk without manual OS configuration: # # 1. Stage install files to C:\Program Files\Vestibule (if not already # installed there by the Inno Setup installer) # 2. Create the dedicated kiosk local account # 3. Build the extension XPI and deploy a merged policies.json to the # browser's distribution directory (policy force-installs the # extension, so no about:debugging step on the kiosk) — works for # LibreWolf and Firefox alike # 4. Write C:\ProgramData\Vestibule\kiosk.env (home URL + browser) # 5. Create the Start Menu shortcut with a stable AppUserModelID # 6. Apply AssignedAccess single-app kiosk config via the MDM WMI # bridge; on Enterprise/Education, step down to Shell Launcher if # the bridge rejects the XML # 7. Configure automatic logon for the kiosk account # # Exit codes: # 0 success (no reboot needed) # 10 success, reboot required to activate # 2 unsupported platform (not Windows / Home edition / not elevated) # 3 prerequisite missing (browser, usher binary, install files) # 4 kiosk account error # 5 lockdown apply failed (AssignedAccess AND Shell Launcher) # 6 invalid parameters # # Unattended example: # powershell -ExecutionPolicy Bypass -File provision-kiosk.ps1 ` # -KioskUser Kiosk -KioskPassword 'S3cure!' ` # -HomeUrl https://checkin.example.org -Quiet -Restart # # Interactive (wizard prompts): # powershell -ExecutionPolicy Bypass -File provision-kiosk.ps1 param( [ValidateSet("auto", "librewolf", "firefox")] [string]$Browser = "auto", # auto: step-down order LibreWolf -> Firefox [string]$KioskUser = "VestibuleKiosk", [string]$KioskPassword, # generated + printed if omitted [string]$HomeUrl, # default about:blank [string]$InstallRoot, # default: detected below [switch]$Quiet, # no prompts (unattended) [switch]$Restart, # auto-reboot when required [switch]$Check, # validate only, change nothing [switch]$ShellLauncher, # force Shell Launcher (skip bridge) [switch]$NoAutoLogon, # skip automatic logon config [switch]$InstallOverridesCfg # also deploy librewolf.overrides.cfg ) $ErrorActionPreference = "Stop" $AUMID = "Vestibule.Kiosk" $ExtId = "vestibule@vestibule.kiosk" $ProgramDataDir = Join-Path $env:ProgramData "Vestibule" $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path # ─── Small helpers ──────────────────────────────────────────────────── function Fail([int]$code, [string]$msg) { Write-Host "" Write-Host "ERROR: $msg" -ForegroundColor Red Write-Host " exiting with code $code" exit $code } function Info($msg) { Write-Host $msg } function Ok($msg) { Write-Host " [ok] $msg" -ForegroundColor Green } function Step($msg) { Write-Host ""; Write-Host "==> $msg" -ForegroundColor Cyan } function Test-Elevated { $id = [Security.Principal.WindowsIdentity]::GetCurrent() return ([Security.Principal.WindowsPrincipal]$id).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) } function Get-WindowsEdition { return (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").EditionID } # ─── Browser discovery (pipeline, first hit wins) ─────────────────── $librewolfSearchPaths = @( "${env:ProgramFiles}\LibreWolf\librewolf.exe", "${env:ProgramFiles(x86)}\LibreWolf\librewolf.exe", "${env:LOCALAPPDATA}\Programs\LibreWolf\librewolf.exe" ) $firefoxSearchPaths = @( "${env:ProgramFiles}\Mozilla Firefox\firefox.exe", "${env:ProgramFiles}\Mozilla Firefox ESR\firefox.exe", "${env:ProgramFiles(x86)}\Mozilla Firefox\firefox.exe", "${env:ProgramFiles(x86)}\Mozilla Firefox ESR\firefox.exe", "${env:LOCALAPPDATA}\Mozilla Firefox\firefox.exe" ) function Find-InPaths($paths, $exeName) { $hit = $paths | Where-Object { Test-Path $_ } | Select-Object -First 1 if ($hit) { return $hit } # Registry App Paths step-down: per-machine first, then per-user. $regRoots = @("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths", "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths") return $regRoots | ForEach-Object { (Get-ItemProperty (Join-Path $_ $exeName) -ErrorAction SilentlyContinue)."(default)" } | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1 } function Find-LibreWolf { Find-InPaths $librewolfSearchPaths "librewolf.exe" } function Find-Firefox { # Firefox and Firefox ESR. Both read the same distribution/policies # mechanism; ESR is recommended for kiosks (slower release cadence). Find-InPaths $firefoxSearchPaths "firefox.exe" } function Resolve-Browser { # Returns @{ Kind = 'librewolf'|'firefox'; Exe = path } or $null. # Explicit -Browser wins; auto steps down LibreWolf -> Firefox # (privacy defaults + trademark-safe, then fully supported Firefox). $lw = Find-LibreWolf $ff = Find-Firefox switch ($Browser) { "librewolf" { if ($lw) { return @{ Kind = "librewolf"; Exe = $lw } } return $null } "firefox" { if ($ff) { return @{ Kind = "firefox"; Exe = $ff } } return $null } default { if ($lw) { return @{ Kind = "librewolf"; Exe = $lw } } if ($ff) { return @{ Kind = "firefox"; Exe = $ff } } return $null } } } function New-RandomPassword { # 20 chars, unambiguous classes — strong enough for a locked-down # kiosk account that is never typed by a human. $chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!#%+" $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() $bytes = New-Object byte[] 20 $rng.GetBytes($bytes) return (-join ($bytes | ForEach-Object { $chars[$_ % $chars.Length] })) } # ─── Pre-flight checks ──────────────────────────────────────────────── Step "Pre-flight checks" if ($env:OS -ne "Windows_NT") { Fail 2 "this script configures Windows kiosk lockdown; on Linux use scripts/provision-kiosk.sh" } if (-not (Test-Elevated)) { Fail 2 "must run elevated (right-click PowerShell -> Run as administrator)" } $edition = Get-WindowsEdition Info "Windows edition: $edition" if ($edition -like "Core*") { Fail 2 ("Windows Home (edition '$edition') does not support AssignedAccess or " + "Shell Launcher. Use Windows Pro, Enterprise, or Education, or deploy " + "the Linux (cage) variant.") } $shellLauncherCapable = ($edition -like "Enterprise*" -or $edition -like "Education*" -or $edition -like "IoTEnterprise*") # Install root: explicit param > already-staged install > repo checkout. if (-not $InstallRoot) { if (Test-Path (Join-Path $scriptDir "..\bin\usher.exe")) { $InstallRoot = (Resolve-Path (Join-Path $scriptDir "..")).Path } else { $InstallRoot = "C:\Program Files\Vestibule" } } Info "install root: $InstallRoot" $browser = Resolve-Browser if ($browser) { Ok "browser ($($browser.Kind)): $($browser.Exe)" $browserExe = $browser.Exe } else { $browserExe = $null Write-Host " [!!] No LibreWolf or Firefox found in standard locations" -ForegroundColor Yellow } $repoRoot = if (Test-Path (Join-Path $scriptDir "..\extension\manifest.json")) { (Resolve-Path (Join-Path $scriptDir "..")).Path } else { $null } $usherStaged = Test-Path (Join-Path $InstallRoot "bin\usher.exe") # ─── Interactive prompts (skipped with -Quiet) ──────────────────────── if (-not $Quiet -and -not $Check) { if (-not $HomeUrl) { $HomeUrl = Read-Host "Kiosk home URL [about:blank]" if (-not $HomeUrl) { $HomeUrl = "about:blank" } } if (-not $KioskPassword) { $KioskPassword = New-RandomPassword Write-Host "" Write-Host "Generated kiosk account password (needed for auto-logon; save it now):" -ForegroundColor Yellow Write-Host " $KioskUser / $KioskPassword" -ForegroundColor Yellow Write-Host "" } } if (-not $HomeUrl) { $HomeUrl = "about:blank" } if ($Check) { Step "Check complete (no changes made)" Info "edition : $edition ($( if ($shellLauncherCapable) {'AssignedAccess + Shell Launcher step-down'} else {'AssignedAccess only'}))" Info "browser : $(if ($browser) {"$($browser.Kind) ($($browser.Exe))"} else {'MISSING -> would exit 3'})" Info "install root : $InstallRoot (usher staged: $usherStaged)" Info "kiosk user : $KioskUser" Info "home URL : $HomeUrl" Info "auto-logon : $(if ($NoAutoLogon) {'disabled'} else {'enabled'})" if (-not $browser) { Fail 3 "LibreWolf/Firefox not found" } if (-not $usherStaged -and -not $repoRoot) { Fail 3 "no staged install and no repo checkout with a built usher" } Ok "all prerequisites satisfied — re-run without -Check to provision" exit 0 } if (-not $KioskPassword) { $KioskPassword = New-RandomPassword Write-Host "Generated kiosk account password (save it now): $KioskUser / $KioskPassword" -ForegroundColor Yellow } if (-not $browser) { Fail 3 "No supported browser found — install LibreWolf (librewolf.net) or Firefox (mozilla.org), then re-run" } # ─── 1. Stage install files ─────────────────────────────────────────── Step "Stage install files ($InstallRoot)" if (-not $usherStaged) { if (-not $repoRoot) { Fail 3 ("usher.exe not found at '$InstallRoot\bin'. Install via the " + "Vestibule Setup .exe, or build from source: cd helper; cargo build --release") } New-Item -ItemType Directory -Force -Path "$InstallRoot\bin" | Out-Null New-Item -ItemType Directory -Force -Path "$InstallRoot\scripts" | Out-Null Copy-Item (Join-Path $repoRoot "helper\target\release\usher.exe") "$InstallRoot\bin\usher.exe" -Force Copy-Item (Join-Path $repoRoot "scripts\*.ps1") "$InstallRoot\scripts\" -Force Ok "staged usher.exe + scripts" } if (-not (Test-Path "$InstallRoot\extension\manifest.json") -and $repoRoot) { New-Item -ItemType Directory -Force -Path "$InstallRoot\extension" | Out-Null Copy-Item (Join-Path $repoRoot "extension\*") "$InstallRoot\extension\" -Recurse -Force Ok "staged extension source" } if (-not (Test-Path "$InstallRoot\extension\manifest.json")) { Fail 3 "extension files missing under '$InstallRoot\extension'" } # ─── 2. Kiosk account ───────────────────────────────────────────────── Step "Kiosk account '$KioskUser'" $secure = ConvertTo-SecureString $KioskPassword -AsPlainText -Force if (Get-LocalUser -Name $KioskUser -ErrorAction SilentlyContinue) { Set-LocalUser -Name $KioskUser -Password $secure -PasswordNeverExpires $true Ok "account exists — password reset, never expires" } else { try { New-LocalUser -Name $KioskUser -Password $secure ` -AccountNeverExpires -PasswordNeverExpires ` -PasswordChangeNotAllowed ` -Description "Vestibule kiosk account (auto-provisioned)" | Out-Null Ok "created" } catch { Fail 4 "could not create kiosk account: $($_.Exception.Message)" } } # ─── 3. XPI + policies.json ─────────────────────────────────────────── Step "Extension XPI + $($browser.Kind) policies" $xpiPath = Join-Path $InstallRoot "extension\vestibule.xpi" $xpiTmp = Join-Path $env:TEMP "vestibule-xpi.zip" if (Test-Path $xpiTmp) { Remove-Item $xpiTmp -Force } Compress-Archive -Path (Join-Path $InstallRoot "extension\*") ` -DestinationPath $xpiTmp -Force Move-Item $xpiTmp $xpiPath -Force Ok "built $(Split-Path -Leaf $xpiPath)" # Distribution dir sits next to the browser executable (LibreWolf and # Firefox share the mechanism). $browserDir = Split-Path -Parent $browserExe $distDir = Join-Path $browserDir "distribution" New-Item -ItemType Directory -Force -Path $distDir | Out-Null $policiesPath = Join-Path $distDir "policies.json" # The shipped distribution/policies.json is backed up once, then ours # is deep-merged on top so the browser's own hardening survives. if (Test-Path $policiesPath) { $bak = "$policiesPath.vestibule-bak" if (-not (Test-Path $bak)) { Copy-Item $policiesPath $bak -Force } Ok "backed up existing policies.json -> vestibule-bak" } # ConvertFrom-Json in Windows PowerShell 5.1 yields PSCustomObjects and # has no -AsHashtable; walk the tree into real hashtables so the deep # merge below can mutate in place. function ConvertTo-HashtableDeep($node) { if ($node -is [System.Management.Automation.PSCustomObject]) { $h = @{} foreach ($p in $node.PSObject.Properties) { $h[$p.Name] = ConvertTo-HashtableDeep $p.Value } return $h } if ($node -is [System.Collections.IEnumerable] -and $node -isnot [string]) { $arr = @() foreach ($item in $node) { $arr += ,(ConvertTo-HashtableDeep $item) } return $arr } return $node } function Merge-Policy([hashtable]$base, [hashtable]$overlay) { foreach ($k in $overlay.Keys) { if ($base.ContainsKey($k) -and $base[$k] -is [hashtable] -and $overlay[$k] -is [hashtable]) { Merge-Policy $base[$k] $overlay[$k] } else { $base[$k] = $overlay[$k] } } } $policies = @{ policies = @{} } if (Test-Path $policiesPath) { try { $existing = ConvertTo-HashtableDeep (Get-Content $policiesPath -Raw | ConvertFrom-Json) if ($existing -is [hashtable] -and $existing.Count -gt 0) { $policies = $existing } } catch { $policies = @{ policies = @{} } } } $canonical = ConvertTo-HashtableDeep (Get-Content (Join-Path $scriptDir "..\config\policies.json") -Raw | ConvertFrom-Json) Merge-Policy $policies $canonical # Policy-install the extension: force_installed survives the "*" blocked # wildcard in ExtensionSettings and re-installs itself on every startup. $xpiUrl = ([uri]$xpiPath).AbsoluteUri $policies.policies.ExtensionSettings = @{ "*" = @{ blocked_install_message = "Extensions are not allowed on this kiosk." install_sources = @() installation_mode = "blocked" } $ExtId = @{ installation_mode = "force_installed" install_url = $xpiUrl } } # WriteAllText = UTF-8 without BOM. Set-Content -Encoding UTF8 in # Windows PowerShell 5.1 emits a BOM, which Gecko's policy loader is not # guaranteed to tolerate. [System.IO.File]::WriteAllText($policiesPath, ($policies | ConvertTo-Json -Depth 10)) Ok "deployed policies.json (extension force-installed from $xpiUrl)" if ($InstallOverridesCfg) { # librewolf.overrides.cfg is a LibreWolf-specific autoconfig file; it # has no effect on Firefox (Firefox ignores it safely). if ($browser.Kind -eq "librewolf") { $src = Join-Path $scriptDir "..\config\librewolf.overrides.cfg" if (Test-Path $src) { Copy-Item $src (Join-Path $browserDir "librewolf.overrides.cfg") -Force Ok "deployed librewolf.overrides.cfg (SSO/telehealth compat)" } } else { Info "skipped librewolf.overrides.cfg (LibreWolf-only; browser is $($browser.Kind))" } } # ─── 4. ProgramData config ──────────────────────────────────────────── Step "Deployment config" New-Item -ItemType Directory -Force -Path $ProgramDataDir | Out-Null @" VESTIBULE_HOME_URL=$HomeUrl VESTIBULE_BROWSER=$($browser.Kind) "@ | Set-Content (Join-Path $ProgramDataDir "kiosk.env") -Encoding UTF8 Ok "kiosk.env written (home URL: $HomeUrl, browser: $($browser.Kind))" # ─── 5. Start Menu shortcut with AUMID ──────────────────────────────── Step "Kiosk shortcut (AUMID: $AUMID)" Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; // Sets the System.AppUserModel.ID property on a .lnk file. AssignedAccess // single-app kiosk mode launches desktop apps by AUMID, so the shortcut // must carry a stable explicit AUMID. public static class ShortcutAumid { [ComImport, Guid("00021401-0000-0000-C000-000000000046")] private class ShellLinkCoClass {} [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("0000010B-0000-0000-C000-000000000046")] private interface IPersistFile { void GetClassID(out Guid pClassID); [PreserveSig] int IsDirty(); void Load([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, uint dwMode); void Save([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, [MarshalAs(UnmanagedType.Bool)] bool fRemember); void SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string pszFileName); void GetCurFile([MarshalAs(UnmanagedType.LPWStr)] out string ppszFileName); } [ComImport, Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IPropertyStore { int GetCount(out uint cProps); int GetAt(uint iProp, out PropertyKey pkey); int GetValue(ref PropertyKey key, out PropVariant pv); int SetValue(ref PropertyKey key, ref PropVariant pv); int Commit(); } [StructLayout(LayoutKind.Sequential)] private struct PropertyKey { public Guid fmtid; public uint pid; } [StructLayout(LayoutKind.Explicit)] private struct PropVariant { [FieldOffset(0)] public ushort vt; [FieldOffset(8)] public IntPtr pointerValue; } [DllImport("ole32.dll")] private static extern int CoInitialize(IntPtr reserved); [DllImport("ole32.dll")] private static extern void CoUninitialize(); [DllImport("ole32.dll")] private static extern int CoCreateInstance(ref Guid clsid, IntPtr outer, uint context, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown)] out object obj); [DllImport("ole32.dll")] private static extern IntPtr CoTaskMemAlloc(uint bytes); [DllImport("ole32.dll")] private static extern void CoTaskMemFree(IntPtr p); private const ushort VT_LPWSTR = 31; private const uint STGM_READWRITE = 2; private static readonly Guid ClsidShellLink = new Guid("00021401-0000-0000-C000-000000000046"); private static readonly Guid IidPersistFile = new Guid("0000010B-0000-0000-C000-000000000046"); private static readonly Guid IidPropertyStore = new Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"); private static readonly PropertyKey PkeyAppUserModelId = new PropertyKey { fmtid = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), pid = 5 }; public static int SetLnkAumid(string lnkPath, string aumid) { int initHr = CoInitialize(IntPtr.Zero); // S_OK (0) or S_FALSE (1) try { Guid clsid = ClsidShellLink, iidPf = IidPersistFile; object pfObj; int hr = CoCreateInstance(ref clsid, IntPtr.Zero, 1 /*CLSCTX_INPROC_SERVER*/, ref iidPf, out pfObj); if (hr != 0) return hr; IPersistFile persist = (IPersistFile)pfObj; persist.Load(lnkPath, STGM_READWRITE); IPropertyStore store = (IPropertyStore)pfObj; PropVariant pv = new PropVariant(); pv.vt = VT_LPWSTR; pv.pointerValue = Marshal.StringToCoTaskMemUni(aumid); try { hr = store.SetValue(ref PkeyAppUserModelId, ref pv); if (hr != 0) return hr; hr = store.Commit(); if (hr != 0) return hr; } finally { CoTaskMemFree(pv.pointerValue); } persist.Save(lnkPath, true); return 0; } finally { if (initHr == 0) CoUninitialize(); } } } "@ $startMenuDir = Join-Path $env:ProgramData "Microsoft\Windows\Start Menu\Programs" $lnkPath = Join-Path $startMenuDir "Vestibule Kiosk.lnk" $launcher = Join-Path $InstallRoot "scripts\kiosk-launch.ps1" $ws = New-Object -ComObject WScript.Shell $sc = $ws.CreateShortcut($lnkPath) $sc.TargetPath = "powershell.exe" $sc.Arguments = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$launcher`"" $sc.WorkingDirectory = $InstallRoot $sc.IconLocation = "$browserExe,0" $sc.Description = "Vestibule kiosk (AssignedAccess shell)" $sc.Save() $hr = [ShortcutAumid]::SetLnkAumid($lnkPath, $AUMID) if ($hr -ne 0) { Fail 5 "could not set AUMID on shortcut (HRESULT 0x$($hr.ToString('X8')))" } Ok "shortcut: $lnkPath" # Verify the shell can resolve the AUMID (Get-StartApps indexes the # Start Menu; retry briefly because indexing is asynchronous). $aumidFound = $false for ($i = 0; $i -lt 3 -and -not $aumidFound; $i++) { Start-Sleep -Seconds 2 $aumidFound = [bool](Get-StartApps | Where-Object { $_.AppID -eq $AUMID }) } if (-not $aumidFound) { Fail 5 "AUMID '$AUMID' not visible to Get-StartApps — AssignedAccess would reject it. Reboot and re-run." } Ok "AUMID resolves via Get-StartApps" # ─── 6. Lockdown: AssignedAccess (Shell Launcher step-down) ──────── $shellLauncherArgs = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$launcher`"" $lockdownApplied = $false $rebootNeeded = $false function Test-ShellLauncherClass { return [bool](Get-CimClass -Namespace "root\standardcimv2\embedded" ` -ClassName "WESL_UserSetting" -ErrorAction SilentlyContinue) } function Enable-ShellLauncherFeature { try { Enable-WindowsOptionalFeature -Online ` -FeatureName "Client-DeviceLockdown" -All -NoRestart -ErrorAction Stop | Out-Null return $true } catch { try { Enable-WindowsOptionalFeature -Online ` -FeatureName "Client-EmbeddedShellLauncher" -All -NoRestart -ErrorAction Stop | Out-Null return $true } catch { return $false } } } function Invoke-ShellLauncher { if (-not (Test-ShellLauncherClass)) { if (-not (Enable-ShellLauncherFeature)) { return $false } if (-not (Test-ShellLauncherClass)) { # Feature enabled but class appears after reboot. $script:rebootNeeded = $true return $false } } $wesl = [wmiclass]"\\.\root\standardcimv2\embedded:WESL_UserSetting" # SetCustomShell's parameter list has drifted across Windows builds # (4-arg v1 and 5-arg variants with a custom return-code map). Try each # known shape; the first that returns 0 wins. $r = $null foreach ($call in @( { $wesl.SetCustomShell($KioskUser, "powershell.exe", $shellLauncherArgs, $null, 0) }, { $wesl.SetCustomShell($KioskUser, "powershell.exe", $shellLauncherArgs, 0) }, { $wesl.SetCustomShell($KioskUser, "powershell.exe", $shellLauncherArgs) } )) { try { $r = & $call if ($r.ReturnValue -eq 0) { break } } catch { $r = $null } } if ($r -and $r.ReturnValue -eq 0) { $script:lockdownApplied = $true $script:shellLauncherMode = $true Ok "Shell Launcher custom shell set for '$KioskUser'" return $true } Write-Host " [!!] SetCustomShell failed (last result: $(if ($r) {$r.ReturnValue} else {'exception'}))" -ForegroundColor Yellow return $false } function Invoke-AssignedAccess { $profileId = "{$([guid]::NewGuid().ToString())}" $xml = @" $KioskUser "@ try { $instances = @(Get-CimInstance -Namespace "root\cimv2\mdm\dmmap" ` -ClassName "MDM_AssignedAccess" -ErrorAction Stop) } catch { Write-Host " [!!] MDM WMI bridge unavailable: $($_.Exception.Message)" -ForegroundColor Yellow return $false } foreach ($inst in $instances) { try { $res = Invoke-CimMethod -InputObject $inst ` -MethodName "SetSingleAppKiosk" ` -Arguments @{ AssignedAccessConfiguration = $xml } -ErrorAction Stop if ($res.ReturnValue -eq 0) { $script:lockdownApplied = $true $script:shellLauncherMode = $false Ok "AssignedAccess single-app kiosk configured via MDM bridge" return $true } Write-Host " [!!] SetSingleAppKiosk returned $($res.ReturnValue) on one enrollment" -ForegroundColor Yellow } catch { Write-Host " [!!] bridge call failed: $($_.Exception.Message)" -ForegroundColor Yellow } } return $false } Step "OS-level lockdown" if ($ShellLauncher) { Info "mode: Shell Launcher (forced by parameter)" [void](Invoke-ShellLauncher) } else { Info "mode: AssignedAccess via MDM WMI bridge" if (-not (Invoke-AssignedAccess)) { if ($shellLauncherCapable) { Info "bridge failed — stepping down to Shell Launcher (supported on $edition)" [void](Invoke-ShellLauncher) } } } if (-not $lockdownApplied) { Fail 5 ("could not apply AssignedAccess or Shell Launcher. Apply manually: " + "Settings > Accounts > Other users > Set up kiosk, or use -ShellLauncher on Enterprise/Education.") } # ─── 7. Automatic logon ─────────────────────────────────────────────── Step "Automatic logon" if ($NoAutoLogon) { Info "skipped (-NoAutoLogon) — kiosk starts after manual logon as '$KioskUser'" } else { $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" Set-ItemProperty $wl -Name "AutoAdminLogon" -Value "1" -Type String Set-ItemProperty $wl -Name "DefaultUserName" -Value $KioskUser -Type String Set-ItemProperty $wl -Name "DefaultDomainName" -Value $env:COMPUTERNAME -Type String # NOTE: DefaultPassword is stored in plaintext in the registry. On a # locked-down kiosk appliance this is an accepted trade-off (documented # in DEPLOYMENT.md); hold Shift during boot to bypass auto-logon. Set-ItemProperty $wl -Name "DefaultPassword" -Value $KioskPassword -Type String Ok "auto-logon configured for '$KioskUser' (Shift at logon bypasses it)" } # ─── Summary ────────────────────────────────────────────────────────── Step "Provisioning complete" Info "kiosk user : $KioskUser" Info "home URL : $HomeUrl" Info "lockdown : $(if ($shellLauncherMode) {'Shell Launcher'} else {'AssignedAccess'})" Info "browser : $($browser.Kind) ($($browser.Exe))" Info "policies : $policiesPath" $rebootNeeded = $rebootNeeded -or (-not $NoAutoLogon) if ($rebootNeeded) { Info "reboot required to activate the kiosk." if ($Restart) { Info "restarting in 10 seconds (-Restart)..." shutdown.exe /r /t 10 /c "Vestibule kiosk activation" exit 0 } exit 10 } Ok "kiosk will start the next time '$KioskUser' logs on" exit 0