- Remote help for Intune and Microsoft Endpoint Manager - Tue, Jan 25 2022
- Windows 10/11 Azure AD/Intune Enterprise subscription is not valid - Mon, Nov 8 2021
- Upgrade from Windows 10 to Windows 11 with Setupconfig.ini and Intune - Wed, Sep 22 2021
Kiosk machines, or single-purpose computers, are used in many scenarios and organizations. Kiosks come in several flavors, including single-purpose locked down kiosks and multi-app kiosks where you need to be able download files and work with them as well. They also need to be secure, as they are exposed in some use cases.
The result of the example script will look like the screen capture below.
We also allow access to removable media and the Downloads folder, so if we want to save a file in Google Chrome and then open it in Excel, for example, it possible as shown below.
The script will carry out the following configurations:
- Configure AutoLogon with a domain user
- Configure the Start Menu
- Add applications to the allow list
- Allow saving/reading from the Downloads folder
- Allow removable USB drives
- Write kiosk information to the registry
Here is the PowerShell script:
# Name: MultiKiosk # Authors: Jörgen Nilsson [CmdletBinding()] Param( [Parameter(Mandatory=$True)] [string]$Username, [Parameter(Mandatory=$True)] [string]$Password ) # Set values $Version="1" $RegKeyName = "CCMEXECOSD" $FullRegKeyName = "HKLM:\SOFTWARE\" + $regkeyname $Domain="4sysops" # Create Registry key New-Item -Path $FullRegKeyName -type Directory -ErrorAction SilentlyContinue function Set-KioskMode { $DomainUser = "$($Domain)\$($UserName)".TrimStart('\') $nameSpaceName="root\cimv2\mdm\dmmap" $className="MDM_AssignedAccess" $obj = Get-CimInstance -Namespace $namespaceName -ClassName $className Add-Type -AssemblyName System.Web $obj.Configuration = [System.Web.HttpUtility]::HtmlEncode(@" <?xml version="1.0" encoding="utf-8" ?> <AssignedAccessConfiguration xmlns="http://schemas.microsoft.com/AssignedAccess/2017/config" xmlns:v2="http://schemas.microsoft.com/AssignedAccess/201810/config" xmlns:v3="http://schemas.microsoft.com/AssignedAccess/2020/config" > <Profiles> <Profile Id="{9A2A490F-10F6-4764-974A-43B19E722C23}"> <AllAppsList> <AllowedApps> <App AppUserModelId="Windows.PrintDialog_cw5n1h2txyewy" /> <App DesktopAppPath="C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" /> <App DesktopAppPath="C:\Program Files\Microsoft Office\root\Office16\Excel.exe" /> <App DesktopAppPath="C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" /> </AllowedApps> </AllAppsList> <v2:FileExplorerNamespaceRestrictions> <v2:AllowedNamespace Name="Downloads"/> <v3:AllowRemovableDrives/> </v2:FileExplorerNamespaceRestrictions> <StartLayout> <![CDATA[<LayoutModificationTemplate xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout" xmlns:start="http://schemas.microsoft.com/Start/2014/StartLayout" Version="1" xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"> <LayoutOptions StartTileGroupCellWidth="6" /> <DefaultLayoutOverride> <StartLayoutCollection> <defaultlayout:StartLayout GroupCellWidth="6"> <start:Group Name="Product"> <start:DesktopApplicationTile Size="2x2" Column="0" Row="0" DesktopApplicationLinkPath="%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Google Chrome.lnk" /> <start:DesktopApplicationTile Size="2x2" Column="2" Row="0" DesktopApplicationLinkPath="%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk" /> <start:DesktopApplicationTile Size="2x2" Column="4" Row="0" DesktopApplicationLinkPath="%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Excel.lnk" /> </start:Group> </defaultlayout:StartLayout> </StartLayoutCollection> </DefaultLayoutOverride> </LayoutModificationTemplate> ]]> </StartLayout> <Taskbar ShowTaskbar="true"/> </Profile> </Profiles> <Configs> <Config> <Account>$DomainUser</Account> <DefaultProfile Id="{9A2A490F-10F6-4764-974A-43B19E722C23}"/> </Config> </Configs> </AssignedAccessConfiguration> "@) Set-CimInstance -CimInstance $obj } $Code = @' Add-Type @" using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; namespace PInvoke.LSAUtil { public class LSAutil { [StructLayout (LayoutKind.Sequential)] private struct LSA_UNICODE_STRING { public UInt16 Length; public UInt16 MaximumLength; public IntPtr Buffer; } [StructLayout (LayoutKind.Sequential)] private struct LSA_OBJECT_ATTRIBUTES { public int Length; public IntPtr RootDirectory; public LSA_UNICODE_STRING ObjectName; public uint Attributes; public IntPtr SecurityDescriptor; public IntPtr SecurityQualityOfService; } private enum LSA_AccessPolicy : long { POLICY_VIEW_LOCAL_INFORMATION = 0x00000001L, POLICY_VIEW_AUDIT_INFORMATION = 0x00000002L, POLICY_GET_PRIVATE_INFORMATION = 0x00000004L, POLICY_TRUST_ADMIN = 0x00000008L, POLICY_CREATE_ACCOUNT = 0x00000010L, POLICY_CREATE_SECRET = 0x00000020L, POLICY_CREATE_PRIVILEGE = 0x00000040L, POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080L, POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100L, POLICY_AUDIT_LOG_ADMIN = 0x00000200L, POLICY_SERVER_ADMIN = 0x00000400L, POLICY_LOOKUP_NAMES = 0x00000800L, POLICY_NOTIFICATION = 0x00001000L } [DllImport ("advapi32.dll", SetLastError = true, PreserveSig = true)] private static extern uint LsaStorePrivateData ( IntPtr policyHandle, ref LSA_UNICODE_STRING KeyName, ref LSA_UNICODE_STRING PrivateData ); [DllImport ("advapi32.dll", SetLastError = true, PreserveSig = true)] private static extern uint LsaOpenPolicy ( ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, uint DesiredAccess, out IntPtr PolicyHandle ); [DllImport ("advapi32.dll", SetLastError = true, PreserveSig = true)] private static extern uint LsaNtStatusToWinError ( uint status ); [DllImport ("advapi32.dll", SetLastError = true, PreserveSig = true)] private static extern uint LsaClose ( IntPtr policyHandle ); [DllImport ("advapi32.dll", SetLastError = true, PreserveSig = true)] private static extern uint LsaFreeMemory ( IntPtr buffer ); private LSA_OBJECT_ATTRIBUTES objectAttributes; private LSA_UNICODE_STRING localsystem; private LSA_UNICODE_STRING secretName; public LSAutil (string key) { if (key.Length == 0) { throw new Exception ("Key lenght zero"); } objectAttributes = new LSA_OBJECT_ATTRIBUTES (); objectAttributes.Length = 0; objectAttributes.RootDirectory = IntPtr.Zero; objectAttributes.Attributes = 0; objectAttributes.SecurityDescriptor = IntPtr.Zero; objectAttributes.SecurityQualityOfService = IntPtr.Zero; localsystem = new LSA_UNICODE_STRING (); localsystem.Buffer = IntPtr.Zero; localsystem.Length = 0; localsystem.MaximumLength = 0; secretName = new LSA_UNICODE_STRING (); secretName.Buffer = Marshal.StringToHGlobalUni (key); secretName.Length = (UInt16) (key.Length * UnicodeEncoding.CharSize); secretName.MaximumLength = (UInt16) ((key.Length + 1) * UnicodeEncoding.CharSize); } private IntPtr GetLsaPolicy (LSA_AccessPolicy access) { IntPtr LsaPolicyHandle; uint ntsResult = LsaOpenPolicy (ref this.localsystem, ref this.objectAttributes, (uint) access, out LsaPolicyHandle); uint winErrorCode = LsaNtStatusToWinError (ntsResult); if (winErrorCode != 0) { throw new Exception ("LsaOpenPolicy failed: " + winErrorCode); } return LsaPolicyHandle; } private static void ReleaseLsaPolicy (IntPtr LsaPolicyHandle) { uint ntsResult = LsaClose (LsaPolicyHandle); uint winErrorCode = LsaNtStatusToWinError (ntsResult); if (winErrorCode != 0) { throw new Exception ("LsaClose failed: " + winErrorCode); } } private static void FreeMemory (IntPtr Buffer) { uint ntsResult = LsaFreeMemory (Buffer); uint winErrorCode = LsaNtStatusToWinError (ntsResult); if (winErrorCode != 0) { throw new Exception ("LsaFreeMemory failed: " + winErrorCode); } } public void SetSecret (string value) { LSA_UNICODE_STRING lusSecretData = new LSA_UNICODE_STRING (); if (value.Length > 0) { //Create data and key lusSecretData.Buffer = Marshal.StringToHGlobalUni (value); lusSecretData.Length = (UInt16) (value.Length * UnicodeEncoding.CharSize); lusSecretData.MaximumLength = (UInt16) ((value.Length + 1) * UnicodeEncoding.CharSize); } else { //Delete data and key lusSecretData.Buffer = IntPtr.Zero; lusSecretData.Length = 0; lusSecretData.MaximumLength = 0; } IntPtr LsaPolicyHandle = GetLsaPolicy (LSA_AccessPolicy.POLICY_CREATE_SECRET); uint result = LsaStorePrivateData (LsaPolicyHandle, ref secretName, ref lusSecretData); ReleaseLsaPolicy (LsaPolicyHandle); uint winErrorCode = LsaNtStatusToWinError (result); if (winErrorCode != 0) { throw new Exception ("StorePrivateData failed: " + winErrorCode); } } } } "@ Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name "DefaultUserName" -Value "%USERNAME%" Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name "DefaultDomainName" -Value "%DOMAINNAME%" Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name "AutoAdminLogon" -Value "1" [PInvoke.LSAUtil.LSAutil]::new("DefaultPassword").SetSecret("%PASSWORD%") Unregister-ScheduledTask -TaskName "CreateAutologon" -Confirm:$false -EA SilentlyContinue Restart-Computer -Force '@ function Create-Task ($Argument) { $Schedule = New-Object -ComObject "Schedule.Service" $Schedule.Connect('localhost') $Folder = $Schedule.GetFolder('\') $task = $Schedule.NewTask(0) $task.RegistrationInfo.Author = "Onevinn" $task.RegistrationInfo.Description = "CreateAutologon" $action = $task.Actions.Create(0) $action.Path = "PowerShell.exe" $action.Arguments = "$Argument" $task.Settings.StartWhenAvailable = $true $trigger = $task.Triggers.Create(8) $trigger.Delay = "PT120S" $result = $Folder.RegisterTaskDefinition("CreateAutologon", $task, 0, "SYSTEM", $null, 5) } $Code = $Code.Replace("%USERNAME%", $Username) $Code = $Code.Replace("%DOMAINNAME%", $Domain) $Code = $Code.Replace("%PASSWORD%", $Password) $bytes = [System.Text.Encoding]::Unicode.GetBytes($Code) $b64 = [System.Convert]::ToBase64String($bytes) Set-KioskMode Create-Task -Argument "-EncodedCommand $($b64)" # Set registry values to be used later new-itemproperty $FullRegKeyName -Name "Kiosk Version" -Value $Version -Type STRING -Force -ErrorAction SilentlyContinue | Out-Null new-itemproperty $FullRegKeyName -Name "UserName" -Value $username -Type STRING -Force -ErrorAction SilentlyContinue | Out-Null
Configuring Assigned Access
We configure the assigned access parts in the script in the XML that we configure in the script. More information about what we can configure can be found here and here. Examples are included.
<AssignedAccessConfiguration xmlns="http://schemas.microsoft.com/AssignedAccess/2017/config" xmlns:v2="http://schemas.microsoft.com/AssignedAccess/201810/config" xmlns:v3="http://schemas.microsoft.com/AssignedAccess/2020/config" \>
The AllowedApps section lists all apps that can run. In the back end, this includes AppLocker rules that are created on the machine, so troubleshooting in a third-party application calls more binaries. These binaries also need to be listed to be allowed to run. Everything that is not on the list is blocked.
<AllowedApps> <App AppUserModelId="Windows.PrintDialog_cw5n1h2txyewy" /> <App DesktopAppPath="C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" /> <App DesktopAppPath="C:\Program Files\Microsoft Office\root\Office16\Excel.exe"> <App DesktopAppPath="C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" /> </AllowedApps>
Allowing folder access and removable media is done by using the V2 and V3 schemas that we defined earlier in the XML file, as shown in the example below. It is very easy to add new folders if necessary.
<v2:FileExplorerNamespaceRestrictions> <v2:AllowedNamespace Name="Downloads"/> <v3:AllowRemovableDrives/> </v2:FileExplorerNamespaceRestrictions>
Configure auto logon
In my previous post, I used Group Policy preferences to configure AutoLogon, which stores the username and password in clear text in the registry. I also used AutoLogon.exe, which is a Microsoft tool that configures AutoLogon since it stores passwords as LSA secrets and not in clear text in the registry. In the sample script posted here, there is a section rewritten by my colleague, Johan Schrewelius, which does the same thing that AutoLogon.exe does. It also adds a scheduled task that is used to configure AutoLogon, as all attempts to configure it during OS deployment are cleared by the OOBE part of the setup.
Deploying the kiosk using MEMCM
To deploy the kiosk script using Configuration Manager, I have a kiosk group in my task sequence that includes the following steps:
For the kiosk computers, I added them to a collection with the variables shown below:
The variables are picked up by the PowerShell script in the task sequence when we deploy the computer. In my script, I hardcoded the domain name, which could easily have been a variable as well. It is set early in the script:
$Domain="4sysops"
Let's look at the three different steps in the task sequence. Move to correct OUI use a simple PowerShell script to move the computer to my Kiosk OU to make sure that the correct Group Policies are applied. It is then executed using an account with the correct permissions to move the account in AD.
Configure kiosk mode
This step executes the PowerShell script that configures the computer in Kiosk mode. The variables passed to the script are -Username and -Password, as shown below.
Reboot after OSD
This step sets the task sequence variable "SMSTSPOSTaction," which reboots the computer after OSD is finished. There will be dual reboots before the computer is in kiosk mode, one caused by the SMSTSPostaction and one caused by the scheduled task that is configured. AutoLogon.
Writing information to the registry
We also write information to the registry about the account that was used and the version of the kiosk configuration. This comes in handy if and when we need to update the machine's kiosk configuration.
The name of the registry key can be modified in the script at the very beginning by changing the variable to your preference.
$RegKeyName = "CCMEXECOSD"
Summary
If you are still deploying kiosk machines and locking them down with Group Policy, AppLocker, and scripts, I highly recommend that you check out assigned access mode. It is so simple to deploy and lock down straight off and is locked down much more tightly than many kiosk setups I have seen.
Subscribe to 4sysops newsletter!
In my next post, I will explain how you can update the Windows 10 kiosk.
Interresting post, might be useful at school 🙂
Would be so nice if there would be a gui tool to generate such a ps 🙂
This is really helpful!!!
How can I revert from kiosk mode for the user configured?
Thank you!!!
Hi,
Last time i checked there is no easy way of reverting from a MulitApp Kiosk.. If you use Assigned access and single app then it is possible using PowerShell
Regards,
Jörgen
Log into the PC with an admin account, open up 'Settings > Accounts > Access work or school' and then select 'Add or remove a provisioning package' and remove the installed provisioning package.
I get "The property 'Configuration' cannot be found on this object…" when i try to execute the PowerShell script.
What am I missing in PowerShell?
Im getting the same thing. Its because this part of code:
Returns no value at all. So the variable $obj is not created.
Hi,
Are you executing the script in System Context? I will doublecheck if there are any copy/paste errors as well.
Regards,
Jörgen
Hi Jorgen,
I have not. Under standard admin account with elevated rights.
Thought it can be done just by executing the Powershell script, without Config Manager, which I assume you use just to do the required sequence, which could be done manually?
Or is it wrong assumption?
Cheers L
Hi,
It have to be run in System context, that is the only way to use the WMI MDM Bridge in Powershell. When I wrote it and tested it out I used PSexec.exe to run it in System context which works just great.
Regards,
Jörgen
Thanks Jorgen 🙂
You've uploaded the wrong photo on the Configure Kiosk mode step.
That was my bad. I uploaded the correct screenshot now. Thanks for the hint.
Hi,
I am starting to use SCCM and wanted to automate a single app kios for our municipality. I have read you previous post and this one and it maybe just the thing i am looking for. I have something a question on something super simple that I just don't under stand. Once the AD account is created. Where in the script do i put the information. I pasted the top part of the code here and i am guessing that i fill the information here but i wanted to ask to make sure. I am new to all this and trying to learn as I go.
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[string]$Username,
[Parameter(Mandatory=$True)]
[string]$Password
)
# Set values
$Version="1"
$RegKeyName = "NutanixKIOS"
$FullRegKeyName = "HKLM:\SOFTWARE\" + $regkeyname
$Domain="*******"
# Create Registry key
New-Item -Path $FullRegKeyName -type Directory -ErrorAction SilentlyContinue
function Set-KioskMode {
$DomainUser = "$($Domain)\$($UserName)".TrimStart('\')
Hello,
Thank you for the useful post, it is very helpful.
is there a way to cancel the autologon?
Regards,
Yaron
Id say you can just remove lines 81 to 270 and the last line of the script. That should do it.
I’m having an issue with the removable storage portion of the provisioning XML. I configured mine the exact same as yours and the downloads folder shows up but not the removable storage. The more confusing part is if I set it to v3:no restrictions I lose all folder access entirely
I am on W10 Edu v20H2 and can post more info if it would help.
Hello, can I use the same script but using a local user account instead of a domain account? if so, what part should I modify in the script?
Thank you!
Hi Jorgen
Great script.
Can you add a screenshot of the whole task sequence ?
It will help others understand at which step those scripts have to be ran.
Cheers