Windows 10 Enterprise Deployment Tips and Scripts

It’s about time, finally ready to roll Windows 10 in a production environment! For me, this process had a simple workflow (but a lot of effort for each step of the process). I’m not going into great detail on the entire process here, but I figured I’d share my project task list as well as the scripts I used in de-bloating the Windows 10 v1607 image!

My end-goal is to deploy Windows 10 via task sequence with as little bloat as possible, non-enterprise apps removed, and do it as securely (GPT, UEFI, Secure Boot) and efficiently as possible. My benchmark for Windows 7 deployment is 26 minutes from PXE to Windows logon screen.

  1. Upgrade SCCM to v1610 for added BIOS to UEFI conversion task sequence steps
  2. Create Windows 10 driver packages for models I intend to support.
  3. Build and capture an unmodified reference image WIM in Hyper-V.
  4. Implement USMT to allow for profile migration from Windows 7 to Windows 10. Switching from MBR to GPT = disk wipe.
  5. Create task sequences for Windows 10 deployment (in-place as well as bare metal)
  6. Create scripts to include in task sequences to strip the untouched reference WIM of what I consider non-essential applications and features.
  7. Switch from DHCP options to IP Helpers for WDS / PXE
  8. Implement Network Unlock for Bitlocker and enable Bitlocker during task sequences for all systems.
  9. Create Group Policy set for Windows 10, use the DoD STIG and Microsoft Security Baseline.

I’m still in the process of hammering out the final GPO, but the image is clean and I’m deploying successfully to a pilot group already. I’ve been forced to push Network Unlock to a later time due to logistics, but I do enable Bitlocker on laptops during OSD. USMT is on my radar for the coming weeks, but for now I have a functional TS and the Help Desk is happy.


Here’s a screenshot of my task sequence. I captured it on the Partition Disk step as I stumbled a bit getting the partitioning done correctly, so hopefully it’s helpful to someone else.

It was important to me to only modify the reference image in task sequences to allow for complete customization and more importantly, transparency. I need others to be able to understand each step I’m taking, why, and how I’m doing it.

It’s a lot easier to modify task sequence steps than a reference image, and I believe this is the best way to go about it. I have other SCCM users who will administer task sequences and being able to pick and choose what you do or don’t change is crucial to my environment.

So, here are the scripts I am using in my Customizing the applied OS step. I found most of this through many hours of research and experimentation.

  • Apply Customized Start Menu This step imports a start menu XML template that I captured from a Windows 10 machine. The end result is a start menu that is free of ads and remains completely customizable by the end user. There are three pinned items: Internet Explorer, Software Center, and File Explorer. It was important that I do this in the task sequence, as all other methods I read about would result in some amount of “lockdown” on the start menu. I want my users to all start with the same template, but be able to customize as they see fit. You can do this via GPO (with the caveat I just described). Following the one-line import is my Start.xml file.
    Import-StartLayout –LayoutPath Start.xml -MountPath $env:SystemDrive\ 
    <LayoutModificationTemplate Version="1" xmlns="">
    <LayoutOptions StartTileGroupCellWidth="6" />
    <defaultlayout:StartLayout GroupCellWidth="6" xmlns:defaultlayout="">
    <start:Group Name="Default Applications" xmlns:start="">
    <start:DesktopApplicationTile Size="2x2" Column="0" Row="0" DesktopApplicationLinkPath="%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Internet Explorer.lnk" />
    <start:DesktopApplicationTile Size="2x2" Column="2" Row="0" DesktopApplicationLinkPath="%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Software Center.lnk" />
    <start:DesktopApplicationTile Size="2x2" Column="4" Row="0" DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk" />
  • Customized Start Menu, Part 2: Shortcuts The other consideration here is that the tiles do not like to work with their default start menu entries. Without creating the shortcuts in the root Programs folder, the tiles will not appear! I create these in Powershell.
    #Create an all users Iexplore shortcut in root of Start menu (programs directory).
    #Without this shortcut the pin will not display on the customized start menu (laid down during task sequence)
    $WshShell = New-Object -ComObject WScript.Shell
    $Shortcut = $WshShell.CreateShortcut("C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Internet Explorer.lnk")
    $Shortcut.TargetPath = "C:\Program Files (x86)\Internet Explorer\iexplore.exe"
    #Create a Software Center shortcut in root of start menu (programs directory).
    #Same reason as above. We have to specify the icon to display, else it will be blank
    #as iexplore points to an exe and software center does not (directly)
    $WshShell = New-Object -ComObject WScript.Shell
    $Shortcut = $WshShell.CreateShortcut("C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Software Center.lnk")
    $Shortcut.TargetPath = "softwarecenter:"
    $Shortcut.IconLocation = "%SystemRoot%\CCM\scclient.exe,0"
  • Disable Edge Browser Looking to the future, I opted not to enable this step at the sites I’m an administrator of, but I left it in place for use at other locations utilizing my task sequences. This is a really simple “run command line” step.
  • Disable Cloud Content / Consumer Experience / OneDriveSetup / Contact Support Cloud content and Consumer experience are settings you can publish through GPO, but at least in testing, I found myself getting logged in before they were in place. I decided to go ahead and place them during OSD, just to be safe. This will prevent “suggested apps” and such from showing up on the start menu. Consumer Experience is similar, I believe you’ll get things like “Candy Crush” without that setting. Contact Support obviously has no place in an Enterprise deployment. The Fix First Logon bit references an article that can explain what it’s for. I ran into an error and had two choices to fix it: insert quick command line correction or insert another reboot during OSD. Obviously, went with the former.
  • Remove provisioned applications This was a huge one for me. Most of the Apps included in Windows 10 are completely unnecessary in my environment. My ultimate goal in mind, I had to find a way to prevent these from appearing in my users’ otherwise uncluttered start menus. I found this script online (Google for it if you’d like) in many variations, but ultimately I chose one that removes everything that I don’t specify wanting to keep. I modified the script to output a log file of each entry it uninstalls to C:\Windows\Logs\Software. This just happens to be the default log directory for Powershell Application Deployment Toolkit, which I use heavily on all systems.Also important to me: I am not entirely confident that when I deploy a servicing update to my clients these applications won’t be re-provisioned. I’ll find out in testing, but this script can be re-run at any time to re-deprovision the apps if necessary. That’s huge for me. I would recommend you start out with a machine you’ve laid your reference image onto and pull a list of the currently provisioned apps with: “Get-AppxProvisionedPackage -Online”. Note what you *do* want in your production build before proceeding.
    # Get a list of all apps
    $AppArrayList = Get-AppxPackage -PackageTypeFilter Bundle | Select-Object -Property Name, PackageFullName | Sort-Object -Property Name
    # Start a log file for apps removed successfully from OS.
    $Location = "C:\Windows\Logs\Software"
    If((Test-Path $Location) -eq $False) {
    new-item -path C:\Windows\Logs\Software -ItemType Directory
    get-date | Out-File -append C:\Windows\Logs\Software\OSDRemovedApps.txt
    # Loop through the list of apps
    foreach ($App in $AppArrayList) {
    # Exclude essential Windows apps
    if (($App.Name -in "Microsoft.WindowsCalculator","Microsoft.WindowsStore","Microsoft.Appconnector","Microsoft.WindowsSoundRecorder","Microsoft.WindowsAlarms","Microsoft.MicrosoftStickyNotes")) {
    Write-Output -InputObject "Skipping essential Windows app: $($App.Name)"
    # Remove AppxPackage and AppxProvisioningPackage
    else {
    # Gather package names
    $AppPackageFullName = Get-AppxPackage -Name $App.Name | Select-Object -ExpandProperty PackageFullName
    $AppProvisioningPackageName = Get-AppxProvisionedPackage -Online | Where-Object { $_.DisplayName -like $App.Name } | Select-Object -ExpandProperty PackageName
    # Attempt to remove AppxPackage
    try {
    Write-Output -InputObject "Removing AppxPackage: $AppPackageFullName"
    # Write the name of the removed apps to a logfile
    $AppProvisioningPackageName | Out-File -append C:\Windows\Logs\Software\OSDRemovedApps.txt
    Remove-AppxPackage -Package $AppPackageFullName -ErrorAction Stop
    catch [System.Exception] {
    Write-Warning -Message $_.Exception.Message
    # Attempt to remove AppxProvisioningPackage
    try {
    Write-Output -InputObject "Removing AppxProvisioningPackage: $AppProvisioningPackageName"
    Remove-AppxProvisionedPackage -PackageName $AppProvisioningPackageName -Online -ErrorAction Stop
    catch [System.Exception] {
    Write-Warning -Message $_.Exception.Message

So, there it is. The customization of the reference image took me a significant amount of time to nail down to my own specifications, but I think my environment will benefit from the time invested for a long time. I hope that these centralized scripts are useful to someone in their quest to deploy Windows 10 Enterprise in a way that minimizes confusion to end users, reduces Help Desk inquiries, and ensures that client systems are as secure, efficient, and uniform as possible.

#as, #create, #deployment, #osd, #same, #sccm, #windows10, #without

PS App Deployment Toolkit: User Logged On / Off Deployment Types

I started using PSADT a year or two ago for my commonly updated applications. Flash, Java, Reader, etc.

One of the first issues I encountered was having a single deployment type. Per PSADT documentation, your deployment type should be deployed with “Allow users to view and interact with the program installation” ticked. Unfortunately, if you set “Logon Requirement” to “Whether or not a user is logged on”, this field greys out, unticked.

So, with this box unticked, PSADT proceeded in Noninteractive mode. Instantly closing Internet Explorer and whatever other apps I had specified. This didn’t make me (or anyone else) happy.

My workaround is quite simple. I have two identical deployment types with different User Experiences. Additionally, I have created a Global Condition to determine whether the workstation is currently in use or not (whether locally or via RDP). This Global Condition is set as a requirement on each Deployment Type.

You can create the Global Condition under Software Library -> Global Conditions. I named mine “Workstation in Use”. The discovery script is incredibly simple:

[bool](query user)

On your “User Logged On” deployment type, configure as such:
User Experience Tab
Installation Behavior: Install For System
Logon Requirement: Only when a user is logged on
Installation Program visibility: Normal
Tick the Allow users to view and interact box.
Requirements Tab
Add -> Custom -> Condition -> Workstation In Use -> Value -> Equals -> True

On your “User Logged Off” deployment type, configure as such:
User Experience Tab
Installation Behavior: Install For System
Logon Requirement: Only when no user is logged on
Installation Program visibility: Normal
The “Allow Users to View and Interact” will be greyed out automatically.
Requirements Tab
Add -> Custom -> Condition -> Workstation In Use -> Value -> Equals -> False

This setup will allow you to give your users the PSADT experience, but also leverage PSADT (in noninteractive mode) to perform installations while no users are logged into the system(s).




Scripting bulk client actions.

I had about 11 applications rolling out this weekend. Tonight, I saw about 200 systems hung up in the “Content Downloaded” status. They were well past the deadline date/time but had not yet enforced. I couldn’t find a common denominator, if I connected to any of them in Client Center and ran the App Deployment Cycle, they installed immediately. My maintenance window was closing, so I needed to focus on resolution rather than going CSI: on the issue.

Current Branch allows you to right click a collection and Notify clients to evaluate Application Policies, but there is not yet the same functionality in the Monitoring tab for particular groups with same reported status on a deployment.

Double Clicking the “Content downloaded” header gives you a copy-pastable list of clients. I stripped out the client names and saved them in a text file. I mass-triggered the Application Deployment Cycle with the following script:

$clients = Get-Content C:\users\chris\desktop\clients.txt
ForEach ($client in $clients)
Invoke-WMIMethod -ComputerName $client -Namespace root\ccm -Class SMS_CLIENT -Name TriggerSchedule "{00000000-0000-0000-0000-000000000121}"

I suppose you could take the same list and create a collection, then trigger the client notification from the console. I use this simple script frequently for quickly getting things run on clients. Things like cycling the ccmexec service (or changing cache value and cycling ccmexec), where SCCM would not be practical.

SystemCenterDudes has an extensive list of triggers you can plug into this script HERE. You could also use this with the CCMCache script I posted HERE earlier this month.

Deploying ccmcache location and size changes, part two.

If you didn’t read part one, you can find it at this link.

My original issue was with systems during migration defaulting back to incorrect ccmcache location and size values. Rather than continuing to deploy to specific systems to resolve, I went ahead and created a configuration item to ensure all client systems are set to intended values.

If you’re only looking to change the ccmcache size, there is an item for this now in Client Settings policies. Unfortunately, that doesn’t allow for changing location. I haven’t vetted this in a lab, but I wouldn’t be surprised making this change in client settings also locks you from modifying the values client-side. This won’t work for me, I have a small number of applications that break the 5120MB barrier. Not enough that I want to increase all systems cache sizes, but enough that I don’t want to revert to manual installations.

Using a compliance item will allow for me to make changes in the future as needed during installations, but also ensure that the value is changed back to standard afterward.

First, we create a Configuration Item with both a discovery script and a remediation script.


When you click Next, you’ll be prompted for a list of applicable operating systems. I selected all as in my environment, I’d like all my configmgr clients using the same values and thus enforced in the same manner. In an environment where separate values may be needed based on demand, you’d likely create multiple configuration items to include in multiple configuration baselines.


For discovery of the value we’re looking for, we use the following Powershell bit to query the SoftMgmtAgent WMI namespace for the CacheConfig class values. We simply return the values of the cache size and location to console.

$Cache = Get-WmiObject -Namespace 'ROOT\CCM\SoftMgmtAgent' -Class CacheConfig
write-host $Cache.Size $Cache.Location


The value returned, in my environment (and by default), should be: “5120 C:\Windows\ccmcache”. We know that if a client system returns anything else, it does not conform to our desired values for ccmcache and we need to run another script to fix that. In the rule creation below, I’m setting the expected returned value from the discovery script and enabling remediation where necessary.


Our remediation script is pretty simple, too. We know the client’s settings aren’t 5120MB and C:\Windows\ccmcache, so I correct that with the following:

$Cache = Get-WmiObject -Namespace 'ROOT\CCM\SoftMgtAgent' -Class CacheConfig
$Cache.Location = 'C:\Windows\ccmcache'
$Cache.Size = '5120'
Restart-Service -Name CcmExec



The CcmExec service restart is necessary to apply the new values. I was not able to find a documented alternative to this.. so systems that run the remediation script will have their CcmExec service restarted. Implications from this: Software Center instances will close automatically. Potential policy evaluations and subsequent balloon notifications upon service initialization.

I opted to only remediate during maintenance windows for my Configuration Baselines, so this is less of a concern but still something to be aware of.

Once you’ve created your CI, add it to a pilot Configuration Baseline deployed to a small batch of test systems. I generally use a few different Win7, Win10, and usually even an XP system. In this case, I left a few default and changed the ccmcache size and/or location on the bulk of them. Their values were all uniform during the next maintenance window.



Deploying ccmcache location and size changes

I was working on some deployments today and discovered a large chunk of systems that have had ccmcache location set to c:\ccm\cache and size set to 250MB since I migrated to a Current Branch hierarchy.

I did not want to deploy something to all systems in the target collection for this deployment as the ccmexec service would have to cycle, and I’m not sure what would happen if an install were in progress when that happens. My other option would have been to create a collection with only the machines failing with the same “not enough temporary space is reserved” message, and deploy a fix app/package to it.

In the end, I had the list of clients with “not enough space reserved” and just ran the following from my system:

Invoke-Command -ComputerName PCNAMEHERE,ANOTHERPC,ANDANOTHER,YETANOTHER -ScriptBlock { $Cache = Get-WmiObject -Namespace 'ROOT\CCM\SoftMgmtAgent' -Class CacheConfig
$Cache.Location = 'C:\Windows\ccmcache'
$Cache.Size = '5120'
Restart-Service -Name CcmExec }

In the future, I’ll probably look at spending some time creating a configuration item with this script utilized for remediation.

Creating a ConfigMgr lab environment

I’ve been toying with the idea of bringing up a lab for tinkering around with Configuration Manager lately. It’s something I’d love to do at work but there’s not enough time and never will be. Since Windows 10 offers the Hyper-V role, I decided to give it a shot. I’m running all of this on a Core i5 w/ 16GB RAM until I can build up something more substantial.

My background is desktop support. I spent a few years at Geek Squad driving the bug, setting up wireless routers, and watching the company slowly turn technicians into sales people. This drove me to corporate I.T. and desktop support. I spent three or four years doing that before I had finally picked up enough responsibilities to make it official as a system admin. That said, I’ve never had the opportunity to setup a domain from scratch before, outside of the labs for the MCSA course for Server 2012. A few downloads and a couple VMs later..


I’m not going to make step-by-step instructions for getting the labs ready for ConfigMgr as there is plenty out there. Here are some useful links and my notes. My daily work is in an environment with a CAS and three primaries with 8 DPs. For now, my lab will consist of a standalone Primary site with all roles.

It’s worth noting that in production, I’ve read that you shouldn’t use an external DNS name for your AD forest. Not a problem in this lab environment, but in the real world I’d do something like Not sure of all the ramifications, but if I gave them Internet access I know my clients wouldn’t load this blog by visiting “” without being preceded by www. It also goes without saying that this is a bare minimum configuration to bring up a working domain, and I won’t be focusing on Best Practices other than Configuration Manager stuff.

Domain Controller: DC.SCCMCHRIS.COM
Windows 7 Ent. Client: CLIENT-W7.SCCMCHRIS.COM
Windows 10 Ent. Client: CLIENT-W10.SCCMCHRIS.COM

Downloads and notes..

Windows Server 2016

Configuration Manager 1606 / CB

SQL Server 2016

Sweet packaged Hyper-V systems for Windows 7, 8, 10.

Lab Setup Guide

Install Server 2016 on DC VM
Install Server 2016 on Primary VM
Create External switch in Hyper-V for DC Internet access (optional)
Create Internal switch in Hyper-V for use between VMs
On DC, install DNS/DHCP/AD DS roles.
Promote system to DC, create forest
Add second network adapter on internal network virtual switch, configure static IP. I used This adapter will be on the same virtual switch as all other VMs.
Configure dhcp. add a scope to assign, set router and all else to
Connect other systems virtual adapter to the internal network
clients receiving IPs after this step.
Create a domain user, add to domain admins
Join clients to newly created domain with new account

Creating snapshots and calling it a day. Ready  to begin pre-req installation for 1606!

#2016, #configmgr, #lab, #windows-server

Collecting local group membership/local admin details via Compliance Settings in ConfigMgr

Hi all! I wrote this some time ago back before my environment rebuild and content migration. As a result, some of this is not necessary (for instance, Report Builder worked out of the box on Server 12 R2 for me), but thought this was worth sharing with you guys.

I was asked to create a report to show what accounts and groups were inside of the Administrators group on all of our client systems. I found a post by Sherry Kissinger back with SCCM’07 I believe, which lead me to the post below, updated with logging for SCCM2012.. Shout out to Sherry for this post, please reference it for the MOF for extending hardware inventory and further information: .

This was written for a CAS running Configuration Manager 2012 SP1 on a Windows Server 2008 R2 host.

Preparing the console host for report builder 3.0

Please note that I only had to do this when running SQL ’08 on Server ’08 R2. Leaving instructions here for reference.  Worked fine with Server ’12 R2 and SQL ’16 out of the box. The first thing we want to do is get SQL Server 2008 R2 Report Builder 3.0 working. By default, you’re going to get a message saying whatever MP you’re connected to is missing the click-to-run application. You will want to do this on your MP, indeed, as we’ll use it to build our report later. Contrary to the message, you need to do this on any client running the console that you intend to edit reports from in the future as well.

  • Install .NET Framework 3.5.1
  • Install Report Builder 3.0
  • Open Notepad as an administrator and open the console config
    • Path: C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\Microsoft.ConfigurationManagement.exe.config
  • Under “ReportBuilderMapping”, change the last two (of four) lines from 2_0_0_0 to 3_0_0_0 and save.
  • Launch the SCCM console and go to Monitoring -> Reporting -> Reports, then verify that clicking “Create Report” successfully launches Report Builder 3.0

Creating the configuration item


We need to create a custom WMI namespace to hold the information we’re looking to obtain. We do this by creating a configuration item.

  • Assets and Compliance -> Compliance Settings -> Configuration Items -> Create Configuration Item



This script was written by Sherry Kissinger and can be found via the link to her blog I mentioned earlier.

I’m not currently enforcing any compliance rules with this since I am looking to monitor, not remediate with this setting. This should work as long as WMI isn’t broken, in which case, you’ll have bigger issues with the client than just compliance settings.


Creating the configuration baseline

Local Group Membership is the only configuration item/baseline in use in my environment right now. This can contain all of your configuration items in the future, I have created one for top-level items (company-wide) and one for each site with their own SCCM admin as I use RBAC and allow the site admins to do their own thing with daily operations in general. This allows me to scope this out to keep it out of the hands of less-experienced admins. Note that you can include other configuration baselines in a configuration baseline, so you can create additional baselines for settings/configuration items that only apply to subsets of your All ConfigMgr Clients collection.

  • Assets and Compliance -> Compliance Settings -> Configuration Baselines -> Create Configuration Baseline
  • Add your configuration item and any other desired items (software updates, other configuration baselines, config items)
  • Deploy the newly created configuration baseline to your desired collection. I originally set our schedule to be simple, run every 1 days. I found that there was a dire need for this data to be current when Software Inventory ran (we schedule for once daily, but that time can vary with randomization and laptops), so have since changed to every 6 hours. When deployed, it’s my understanding that the clients will run it immediately, then your schedule will take effect.

Configuring the Hardware Inventory Classes

We now need to populate the new namespace with data. This data is gathered by the configuration item and submitted to the management point during Hardware Inventory. Please reference Sherry’s post for the MOF, you’ll use it to add the cm_LocalGroupMembers class. Afterward, you’ll just need to make sure the class is enabled in your client settings policy.

  • Administration -> Client Settings -> Client Settings – ABC -> Hardware Inventory -> Set Classes -> LocalGroupMembers (cm_LocalGroupMembers)

Building the report

We’re collecting the data now, we just need to create the report to view it. You can also create a subscription to this report. Note: I did this on the MP directly (or wherever your SQL is hosted)… if you create the report from a client, from my understanding you’ll have to import the sql server certificate and such… easier to just create on the MP.


  • Monitoring -> Reporting -> Reports -> Create Report
  • The report is created and opens in Report Builder 3.0
  • Right click Datasets, Add Dataset. On the Query tab, click “Use a dataset embedded in my report.”
  • Select the AutoGen datasource
  • Enter the following as a text query
select sys1.netbios_name0
 ,lgm.name0 [Name of the local Group]
 ,lgm.account0 as [Account Contained within the Group]
 , lgm.category0 [Account Type]
 , lgm.domain0 [Domain for Account]
 , lgm.type0 [Type of Account]
 v_gs_localgroupmembers0 lgm
 join v_r_system_valid sys1 on sys1.resourceid=lgm.resourceid
 where lgm.name0 = 'Administrators'
 order by sys1.netbios_name0, lgm.name0, lgm.account0
  • Click “refresh fields”. You’ll be prompted for credentials, I used my reporting services account
  • The “Fields” tab will populate with the localgroupmembers0 fields. OK out of this dialog.

Let’s setup the report to display the data now.


  • Click “Table or Matrix”, then Dataset1, then Next
  • Drag all available fields to Values
  • (next, next, finish). Save your newly created report.

I created this report on my CAS to report across my 3 primaries.


You may have to play with the report to get the display to your liking, but the above steps gave me the following results from the console.


That’s it. I recently built a new hierarchy from the ground up and performed a content migration, which meant extending hardware inventory again and recreating all my custom reports. Still working with ConfigMgr CB v1607, Server ’12R2, and SQL 2016.



#configuration-item, #dcm, #group-membership, #local-administrators, #local-admins