# RCMS Kiosk Post-Install Setup Script # Runs automatically on first login after Windows install # Installs: Tailscale, RustDesk, RCMS app, configures kiosk mode $ErrorActionPreference = "Continue" # Log to ProgramData if writable (admin), else TEMP. Never C:\ root. $LogDir = Join-Path $env:ProgramData 'RCMS' try { New-Item -ItemType Directory -Path $LogDir -Force -ErrorAction Stop | Out-Null $LogFile = Join-Path $LogDir 'rcms-setup.log' } catch { $LogFile = Join-Path $env:TEMP 'rcms-setup.log' } # Tailscale / Headscale pre-auth key (MUST be reusable for multi-pod flashing) # Generate in Headplane: https://headplane.unveiledsoftwaresolutions.com $HeadscaleAuthKey = "hskey-auth-XHBjMMcy7tq9-zm-x3wK5qi8zNaQneUr6Vlyt7l6LPDywABR_-cUJOfbwEGWuWSrbx87ISNzBpuTS" function Log($msg) { $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss" "$ts - $msg" | Tee-Object -FilePath $LogFile -Append } Log "=========================================" Log "RCMS Kiosk Setup Starting" Log "=========================================" # --- Prompt operator for Pod number --- # Pod ID is fixed-format: RCMS-KP#### (4 digit code). # The operator only enters the 4 digits; the script builds the full Pod ID. Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName Microsoft.VisualBasic $PodPrefix = 'RCMS-KP' $ShortName = $null while ([string]::IsNullOrWhiteSpace($ShortName)) { $entered = [Microsoft.VisualBasic.Interaction]::InputBox( "Enter the 4-digit Pod number for this device.`n`nThe full Pod ID will be: $PodPrefix####`n`nExamples:`n Enter 0001 -> Pod ID becomes ${PodPrefix}0001`n Enter 0042 -> Pod ID becomes ${PodPrefix}0042`n Enter 1234 -> Pod ID becomes ${PodPrefix}1234`n`nRules:`n - Exactly 4 digits (0-9)`n - Leading zeros are kept`n`nThis Pod ID becomes:`n - Windows computer name`n - Tailscale / Headscale hostname`n - RustDesk device alias", "RCMS Pod Setup - Enter 4-digit code", "" ) if ([string]::IsNullOrWhiteSpace($entered)) { [System.Windows.Forms.MessageBox]::Show( "A 4-digit Pod number is required to continue.", "Pod Number Required", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Warning ) | Out-Null continue } $code = $entered.Trim() # Pad with leading zeros if 1-3 digits entered (e.g. "42" -> "0042") if ($code -match '^\d{1,4}$') { $code = $code.PadLeft(4, '0') } else { [System.Windows.Forms.MessageBox]::Show( "Invalid Pod number: '$entered'`n`nMust be 1-4 numeric digits. Letters, hyphens, and spaces are not allowed.", "Invalid Pod Number", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error ) | Out-Null Log "Invalid Pod number rejected: '$entered'" continue } $candidate = "$PodPrefix$code" $confirm = [System.Windows.Forms.MessageBox]::Show( "Pod ID will be set to:`n`n $candidate`n`nThis will be the Windows name, Tailscale hostname, and RustDesk alias.`n`nContinue?", "Confirm Pod ID", [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question ) if ($confirm -eq [System.Windows.Forms.DialogResult]::Yes) { $ShortName = $candidate } } Log "Pod ID confirmed: $ShortName" try { Set-Content -Path (Join-Path $LogDir 'pod-id.txt') -Value $ShortName -Force -ErrorAction Stop } catch { Set-Content -Path (Join-Path $env:TEMP 'rcms-pod-id.txt') -Value $ShortName -Force } # --- Enable Remote Desktop --- Log "Enabling Remote Desktop..." Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name 'fDenyTSConnections' -Value 0 Enable-NetFirewallRule -DisplayGroup "Remote Desktop" Log "Remote Desktop enabled" # --- Enable auto-login for rcms user --- Log "Configuring auto-login for rcms user..." $RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" Set-ItemProperty -Path $RegPath -Name "AutoAdminLogon" -Value "1" Set-ItemProperty -Path $RegPath -Name "DefaultUserName" -Value "rcms" Set-ItemProperty -Path $RegPath -Name "DefaultPassword" -Value "" Log "Auto-login configured" # --- Disable sleep/screen timeout --- Log "Disabling sleep and screen timeout..." powercfg /change monitor-timeout-ac 0 powercfg /change monitor-timeout-dc 0 powercfg /change standby-timeout-ac 0 powercfg /change standby-timeout-dc 0 powercfg /change hibernate-timeout-ac 0 powercfg /change hibernate-timeout-dc 0 Log "Power management configured" # --- Set computer name --- Log "Setting computer name to: $ShortName" Rename-Computer -NewName $ShortName -Force 2>$null Log "Computer name set to: $ShortName" # --- Install Tailscale (connects to Headscale) --- Log "Installing Tailscale..." $TailscaleInstaller = "$env:TEMP\tailscale-setup.exe" try { Invoke-WebRequest -Uri "https://pkgs.tailscale.com/stable/tailscale-setup-latest.exe" -OutFile $TailscaleInstaller -UseBasicParsing Start-Process -FilePath $TailscaleInstaller -ArgumentList "/install", "/quiet", "TS_UNATTENDED_MODE=true" -Wait Log "Tailscale installed" Start-Sleep -Seconds 5 Log "Connecting to Headscale as $ShortName..." if ([string]::IsNullOrWhiteSpace($HeadscaleAuthKey)) { Log "WARNING: No auth key set - Tailscale will require manual approval in Headplane" & "C:\Program Files\Tailscale\tailscale.exe" up --login-server https://headscale.unveiledsoftwaresolutions.com --hostname $ShortName --unattended } else { & "C:\Program Files\Tailscale\tailscale.exe" up --login-server https://headscale.unveiledsoftwaresolutions.com --hostname $ShortName --authkey $HeadscaleAuthKey --unattended Log "Tailscale enrolled with auth key" } Log "Headplane: https://headplane.unveiledsoftwaresolutions.com" } catch { Log "ERROR: Tailscale install failed: $_" } # --- Install RustDesk --- # IMPORTANT: RustDesk service runs as LocalSystem but spawns child workers as # LocalService. The actual config the service reads is at: # C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\ # NOT at C:\Windows\System32\config\systemprofile\... (that's only used for the # direct service process, not the worker that does the rendezvous connection). # # Also: RustDesk's silent installer can hang on completion popup, so use a # timeout-based wait rather than -Wait blocking forever. Log "Installing RustDesk..." $RustDeskVersion = "1.4.1" $RustDeskInstaller = "$env:TEMP\rustdesk-setup.exe" try { Invoke-WebRequest -Uri "https://github.com/rustdesk/rustdesk/releases/download/$RustDeskVersion/rustdesk-$RustDeskVersion-x86_64.exe" -OutFile $RustDeskInstaller -UseBasicParsing Log "Running RustDesk silent install (max 90s)..." $proc = Start-Process -FilePath $RustDeskInstaller -ArgumentList "--silent-install" -PassThru if (-not $proc.WaitForExit(90000)) { Log "RustDesk installer did not exit in 90s — killing (this is expected, install completed but installer hung)" Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue Get-Process | Where-Object { $_.Path -and $_.Path -like '*rustdesk*' -and $_.Path -notlike '*Program Files*' } | Stop-Process -Force -ErrorAction SilentlyContinue } Start-Sleep -Seconds 3 Log "RustDesk installed" $RustDeskConfig = @" rendezvous_server = "rustdesk.unveiledsoftwaresolutions.com:21116" nat_type = 1 serial = 0 unlock_pin = "" trusted_devices = "" [options] key = "GGBR2CgKOzGMdqto74NYbvFThiNts4FdYS3vLunTLzo=" custom-rendezvous-server = "rustdesk.unveiledsoftwaresolutions.com" api-server = "https://rustdesk-panel.unveiledsoftwaresolutions.com" "@ # Write config to ALL THREE locations (the LocalService one is the critical one): $LocalServiceConfigDir = "C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config" $SystemConfigDir = "C:\Windows\System32\config\systemprofile\AppData\Roaming\RustDesk\config" $UserConfigDir = "$env:APPDATA\RustDesk\config" foreach ($dir in @($LocalServiceConfigDir, $SystemConfigDir, $UserConfigDir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null Set-Content -Path "$dir\RustDesk2.toml" -Value $RustDeskConfig -Force } # Delete the cached RustDesk.toml at LocalService — it has the public RustDesk # server's UUID/key cached. Removing it forces the service to re-register with # OUR server fresh on next start. $StaleToml = "$LocalServiceConfigDir\RustDesk.toml" if (Test-Path $StaleToml) { Remove-Item $StaleToml -Force -ErrorAction SilentlyContinue } # Make sure LocalService account can read its config dir icacls "C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk" /grant "NT AUTHORITY\LOCAL SERVICE:(OI)(CI)F" /T 2>&1 | Out-Null Log "RustDesk config written to LocalService + SYSTEM + user APPDATA" # Restart the service so it picks up the new config and re-registers with OUR server Log "Restarting RustDesk service..." Stop-Service -Name "rustdesk" -Force -ErrorAction SilentlyContinue Get-Process rustdesk -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 3 Start-Service -Name "rustdesk" -ErrorAction SilentlyContinue Start-Sleep -Seconds 8 # Note: We do NOT call --set-id here. The user's self-hosted RustDesk panel # only supports server-assigned numeric IDs (returns server_not_support for # custom strings). The pod will register with a numeric ID assigned by the # server. Use the panel's alias/name field to map that numeric ID to the # Pod ID for human-readable identification. $RustDeskAssignedId = (& "C:\Program Files\RustDesk\rustdesk.exe" --get-id 2>&1) -join '' Log "RustDesk registered with server-assigned ID: $RustDeskAssignedId" Log " -> Map this ID to '$ShortName' in the RustDesk panel: https://rustdesk-panel.unveiledsoftwaresolutions.com" # Save the mapping for reference $idMapFile = Join-Path $LogDir 'rustdesk-id-mapping.txt' "$ShortName=$RustDeskAssignedId" | Set-Content -Path $idMapFile -Force -ErrorAction SilentlyContinue Start-Process "C:\Program Files\RustDesk\rustdesk.exe" -ErrorAction SilentlyContinue Log "RustDesk restarted with Pod ID as device ID" } catch { Log "ERROR: RustDesk install failed: $_" } # --- Install RCMS Remote Portals --- Log "Installing RCMS Remote Portals..." $RCMSDir = "C:\RCMS" New-Item -ItemType Directory -Path $RCMSDir -Force | Out-Null try { # Download the latest Windows installer from the release proxy. # The proxy returns JSON with `assets` (filenames) and `download_urls` (a # filename -> URL map). NOT a per-asset `browser_download_url` like GitHub's API. Log "Fetching latest release info..." $ReleaseInfo = Invoke-RestMethod -Uri "https://downloads.unveiledsoftwaresolutions.com/rcms-app/release/latest" -UseBasicParsing $setupAssetName = ($ReleaseInfo.assets | Where-Object { $_.name -match '^RCMS-Setup.*\.exe$' } | Select-Object -First 1).name if ($setupAssetName) { $SetupUrl = $ReleaseInfo.download_urls.$setupAssetName if (-not $SetupUrl) { throw "release proxy returned no download_url for $setupAssetName" } $SetupFile = "$env:TEMP\$setupAssetName" Log "Downloading: $setupAssetName" Invoke-WebRequest -Uri $SetupUrl -OutFile $SetupFile -UseBasicParsing Log "Download complete, installing..." # Run NSIS installer silently Start-Process -FilePath $SetupFile -ArgumentList "/S" -Wait Log "RCMS Remote Portals installed" } else { Log "WARNING: No RCMS-Setup .exe asset found in release" } } catch { Log "ERROR: RCMS install failed: $_ -- install manually via RDP after setup" } # --- Configure RCMS to start on login --- # The Electron NSIS installer puts the binary at: # $LOCALAPPDATA\Programs\rcms-source\RC Medical Specialist.exe # (folder name = npm package name 'rcms-source', NOT the friendly 'RC Medical Specialist') Log "Configuring RCMS auto-start..." $RCMSCandidates = @( "$env:LOCALAPPDATA\Programs\rcms-source\RC Medical Specialist.exe", "$env:LOCALAPPDATA\RC Medical Specialist\RC Medical Specialist.exe", "C:\Program Files\RC Medical Specialist\RC Medical Specialist.exe", "C:\Program Files\rcms-source\RC Medical Specialist.exe" ) $RCMSExe = $RCMSCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 if ($RCMSExe) { $StartupFolder = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup" $ShortcutPath = "$StartupFolder\RCMS Remote Portals.lnk" $WshShell = New-Object -ComObject WScript.Shell $Shortcut = $WshShell.CreateShortcut($ShortcutPath) $Shortcut.TargetPath = $RCMSExe $Shortcut.Save() Log "RCMS auto-start shortcut created: $ShortcutPath -> $RCMSExe" } else { Log "WARNING: RCMS exe not found at any expected path - auto-start not configured. Set up manually after install." $RCMSExe = $null } # --- Configure Windows Kiosk Mode (Shell Launcher) --- Log "Configuring kiosk shell..." # For IoT LTSC, use Shell Launcher to replace explorer.exe with RCMS # This is a lighter approach than Assigned Access and works with Electron apps # We'll set it up so explorer still runs (for taskbar) but RCMS launches on top # Full Shell Launcher lockdown can be done later once we confirm everything works # For now: auto-start RCMS maximized + hide taskbar # Hide taskbar via registry $TaskbarPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StuckRects3" # We'll configure full kiosk lockdown remotely after confirming the app works # --- Disable unnecessary services --- Log "Disabling unnecessary services..." $DisableServices = @("DiagTrack", "dmwappushservice", "WSearch") foreach ($svc in $DisableServices) { Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue Set-Service -Name $svc -StartupType Disabled -ErrorAction SilentlyContinue Log "Disabled service: $svc" } # --- Disable Windows Update auto-restart --- Log "Configuring Windows Update..." $WUPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" New-Item -Path $WUPath -Force | Out-Null Set-ItemProperty -Path $WUPath -Name "NoAutoRebootWithLoggedOnUsers" -Value 1 Log "Windows Update won't auto-restart while user is logged in" # --- Disable lock screen --- Log "Disabling lock screen..." $LockPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization" New-Item -Path $LockPath -Force | Out-Null Set-ItemProperty -Path $LockPath -Name "NoLockScreen" -Value 1 Log "Lock screen disabled" # --- Disable screen saver --- Log "Disabling screen saver..." Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name "ScreenSaveActive" -Value "0" Log "Screen saver disabled" # --- Firewall: Allow RCMS app --- if ($RCMSExe) { Log "Configuring firewall for RCMS..." New-NetFirewallRule -DisplayName "RCMS Remote Portals" -Direction Inbound -Action Allow -Program $RCMSExe -ErrorAction SilentlyContinue New-NetFirewallRule -DisplayName "RCMS Remote Portals (Out)" -Direction Outbound -Action Allow -Program $RCMSExe -ErrorAction SilentlyContinue } else { Log "Skipping firewall rules — RCMS exe not located" } Log "Firewall rules added" # --- Install OpenSSH Server for remote admin access via Tailscale --- Log "Installing OpenSSH Server..." try { Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 -ErrorAction Stop | Out-Null Start-Service sshd -ErrorAction SilentlyContinue Set-Service sshd -StartupType Automatic -ErrorAction SilentlyContinue New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 -ErrorAction SilentlyContinue | Out-Null Log "OpenSSH Server installed and started" # Authorize Jariah's SSH public key for both rcms user and administrators $AuthorizedKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL+6+DvLDB0qnMPOTGMvz4vYaJLj7DW2NSZ4QKOaRfAC jariahh@gmail.com" # Per-user authorized_keys (works for non-admin SSH) $UserSshDir = "C:\Users\rcms\.ssh" New-Item -ItemType Directory -Path $UserSshDir -Force | Out-Null Set-Content -Path "$UserSshDir\authorized_keys" -Value $AuthorizedKey -Force icacls "$UserSshDir\authorized_keys" /inheritance:r /grant "rcms:(R,W)" /grant "SYSTEM:(R,W)" 2>&1 | Out-Null # Admin SSH requires the key in administrators_authorized_keys with strict ACL $AdminSshDir = "C:\ProgramData\ssh" if (-not (Test-Path $AdminSshDir)) { New-Item -ItemType Directory -Path $AdminSshDir -Force | Out-Null } Set-Content -Path "$AdminSshDir\administrators_authorized_keys" -Value $AuthorizedKey -Force icacls "$AdminSshDir\administrators_authorized_keys" /inheritance:r /grant "SYSTEM:(R)" /grant "BUILTIN\Administrators:(R)" 2>&1 | Out-Null Restart-Service sshd -ErrorAction SilentlyContinue Log "SSH key authorized for rcms + administrators" Log "Connect via Tailscale: ssh rcms@$ShortName" } catch { Log "ERROR: OpenSSH install failed: $_" } # --- Summary --- Log "=========================================" Log "RCMS Kiosk Setup Complete!" Log "=========================================" Log "Pod ID: $ShortName" Log "Windows computer name: $ShortName" Log "Tailscale hostname: $ShortName (enrolled via auth key)" Log "RustDesk device ID: $RustDeskAssignedId (server-assigned numeric — map to '$ShortName' in panel)" Log "RDP: Enabled" Log "SSH: Enabled (port 22) - connect via 'ssh rcms@$ShortName' over Tailscale" Log "Auto-login: rcms (no password)" Log "RCMS: Auto-starts on login" Log "Lock screen: Disabled" Log "Sleep/screensaver: Disabled" Log "Log file: $LogFile" Log "=========================================" # Show completion message [System.Windows.Forms.MessageBox]::Show( "RCMS Pod Setup Complete!`n`nPod ID: $ShortName`n`nRegistered as:`n - Windows computer name: $ShortName`n - Tailscale / Headscale hostname: $ShortName`n - RustDesk numeric ID: $RustDeskAssignedId`n (rename in panel as '$ShortName')`n`nVerify in:`n - Headplane: https://headplane.unveiledsoftwaresolutions.com`n - RustDesk panel: https://rustdesk-panel.unveiledsoftwaresolutions.com`n`nLog: $LogFile`n`nMachine will restart in 30 seconds to apply all changes.", "RCMS Pod Setup Complete", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information ) | Out-Null # Restart to apply computer name and all changes Log "Restarting in 30 seconds..." shutdown /r /t 30 /c "RCMS kiosk setup complete - restarting to apply changes"