Renewing SSL Certificates on ADFS 3.0 (Server 2012 R2)

I recently had to replace the public-facing service communication certificates on our primary ADFS deployment on Server 2012 R2. I followed a procedure that I thought had a reasonable chance of actually doing what I wanted it to:

  1. Obtained a new private key with signed certificate.
  2. Saved the file to a pfx, and imported it onto each node in the ADFS cluster
  3. Set permissions on the certificate according to documentation
  4. Used the ADFS MMC -> Certificates -> Set Service Communications Certificate.

Everything seemed to go okay, but after a bit we started to get some complaints that some of our users could not access the Office 365 Pro Plus software download page. This was a curiosity to me, because I could not reproduce the problem. A colleague later noticed a raft of SSL errors in the System event log on one of the ADFS nodes, and disabled it in the load balancer configuration.

When I finally got around to investigating, I noticed that the system log reported problems from source ‘HTTPEvent’, with details DeviceObject: \Device\Http\ReqQueue, Endpoint: (and also Endpoint: What gives?

I found a related TechNet Blog that shed some light on the subject:

According to this document, after setting the Service Communications Certificate in the MMC, you must run:
to fetch the certificate thumbprint of the Service Communications Cert. Take note of the certificate thumbprint, then run:
Set-ADFSSslCertificate -Thumbprint [yourThumbprint]

“Set-AdfsSslCertificate” will fix the HTTP.SYS bindings used by ADFS. Apparently the MMC does not set the bindings, which is pretty annoying because this leaves the service in a pretty darn broken state. The HTTP bindings are mentioned in this TechNet documentation:
BUT, the docs do not explicitly state that the Set-AdfsSslCertificate cmdlet needs to be run on all of the ADFS server nodes in your farm. This also is a key missing detail.

Good Documentation… you always take it for granted, until you don’t have it anymore.

Note above that I mentioned a binding problem with the address This was a carry-over from our initial deployment of ADFS 3. Back then, Microsoft did not provide a health check URL for ADFS, and the supplemental binding was needed to allow health monitor connections from our F5 load balancer without using SNI, which is required by ADFS 3.0, but not supported on the F5. These days (and if you have KB2975719 installed), you can instead monitor the following URL from your F5:
More details on this solution can be found here:

While it is nice having a proper health check, problems can arise when your ADFS server HTTP bindings go sour. It would seem that nothing is perfect.

Ping plotting with PowerShell

Waaaay back we used to use a spiffy little tool called “ping plotter” to discover vacant IP addresses on our subnets. I had not had to do an exhaustive study of this for awhile. When it came up again today, I thought “I’ll bet we can do that with two lines of PowerShell.” But I was wrong… it took three lines, since I needed to initiate an array variable:

#Initialize $range as an array variable:
$range = @()

#Populate $range with integers from 2 though 254 
#  (for a Class C ipv4 subnet):
for ($i=2; $i -le 254; $i++) {$range += $i}

#Write out IP addresses for systems that do not have registered DNS 
#  names in the IP subnet
$range | % {$ip = "192.168.1." + $_ ; $out = & nslookup $ip 2>&1; `
  if ($out -match 'Non-existent domain') {write-host $ip}}

Now some variations… write out only addresses with no DNS entry and that do not respond to ping. (This will help to weed out addresses that are in use that for whatever reason to not have a DNS name.):

$range | % {$ip = "132.198.102." + $_ ; $out = & nslookup $ip 2>&1; `
  if ($out -match 'Non-existent domain') {$out2 = & ping $ip -n 1; `
  if ($out2 -match 'Destination host unreachable') {write-host $ip}}}

… and perhaps most usefully, write out addresses with no DNS entry and that cannot be located using ARP. (This will weed out in use addresses with no DNS and a firewall that blocks ICMP packets.):

$range | % {$ip = "132.198.102." + $_ ; $out = & nslookup $ip 2>&1; `
  if ($out -match 'Non-existent domain') {& ping $ip -n 1 > $null; `
  $out2 = arp -a $ip; if ($out2 -match 'No ARP Entries') {write-host $ip}}}

Server 2012 boot hang in vSphere/ESXi

Over the past year or so we have been having some problems with Server 2012 and 2012 R2 virtual machines hanging during reboot operations. The systems hang at the “spash screen”, showing the Windows logo and the ring of spinning dots… forever!

Finally I was able to find an fix for this problem here:

From a tip-off here:

The problem? Well, probably it is best that you just read the TechNet social thread, if you really want to know. It is none too exciting, and all very aggravating. The fix? Run a PowerShell script, then vMotion your machines to force ESXi to re-read the VMX file for your guests.

I am posting my variation on the script in the KB here, because VMware’s script is incomplete, and difficult to read.

#Script intended to correct the bug where Windows Server 2012+ systems on VM Hardware v10 will hang at the 
#boot-up splash screen.  Problem is caused by the failure of ESX to clear the "TS" counter on system reboot.

Set-PSDebug -Strict

#Initialize the VIToolkit:
if ( (Get-PSSnapin -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue) -eq $null ) {
    Add-PsSnapin VMware.VimAutomation.Core

#Connect to your virtual center:
$viServ = ""
Connect-VIServer -Server $viServ

#Get all VMs in the vCenter:
$vms = Get-VM 
#Loop though the VMs:
ForEach ($vm in $vms){
	#Get a "View" object for each VM.  Views expose useful data that is not contined in the VM object:
	$vmv = Get-VM $vm | Get-View
	$name = $vmv.Name
	$guestid = $vmv.Summary.Config.GuestId
	if ($guestid -like "windows8*Guest") {
		#windows8*Guest will match Windows 8 client 32-bit and 64-bit, as well as 
		# "windows8Server64Guest" (which is Windows Server 2012 and 2012 R2).
		#We need to update the VMX file for the VM, which is dong using a VirtualMachineConfigSpec.
		$vmx = New-Object VMware.Vim.VirtualMachineConfigSpec
		$vmx.extraConfig += New-Object VMware.Vim.OptionValue
		$vmx.extraConfig[0].key = "monitor_control.enable_softResetClearTSC"
		$vmx.extraConfig[0].value = "TRUE"
		write-host "Edited" $  
		$ | out-file -FilePath c:\local\temp\softTscOut.txt -NoClobber -Append

Discover user rights in SharePoint

Among the top items capable of derailing your whole day or week are requests from auditors.  Who has access to a resource?  When did they exercise those rights?  In the pas few months, I have had several requests of this sort related to SharePoint rights.  Since I have once again started working on our SharePoint 2010-to-2013 migration project, and most of the SharePoint Powershell cmdlets were fresh in my mind, I though I would take a crack at this somewhat intimidating task.

As usual, writing a useful script took more time that I would have liked, but I am fairly pleased with the results.  The final product makes heavy use of Regular Expressions.  Special thanks go out to RegEx Hero, an online .NET regular expressions tester:
AND, of course, to the site:

Using .NET-style RegEx named capture groups, I was able to eliminate redundant loops though the SharePoint web site list, thus making it possible to crawl all SharePoint web and site-level ACLs in only a few minutes. Hurray!

This code will work only on SharePoint 2010 farms that use Windows authentication. There may be limitations related to sites with multiple Windows domains as well. I will need to update this script in the near future to handle claims authentication, but we will cross that bridge when we come to it.

The script has some pretty convoluted loops that may not make any intuitive sense… I have tried to insert comments to explain what is going on. If you do choose to use this script in your environment and find it difficult to understand, feel free to contact me with questions.

I think that the script has been posted without wordpress-induced errors. The main problem there is the use of .NET style named capture groups, which use ‘greater than’/’less than’ characters. WordPress hates gt/lt, which resemble XML/HTML/XHTML tags. I had to enter these bits of code as HTML escape sequences instead of raw code. It looks like it worked this time.

#J. Greg Mackinnon, 2015-03-25
#Recurses though the web application provided in -webAppplication for the users (samaccountname format) provided in -Users,
#for the domain provided in -Domain.
# Requires: Microsoft.SharePoint.PowerShell PSSnapin
#          ActiveDirectory PowerShell module
# Provides: Comma-separated value file with user, permission, and site data for all discovered permissions, 
#          at path provided in the -logPath parameter.          
        HelpMessage='Enter a username or comma-separated list of usernames in samAccountName format.')]
        [string]$domain = 'CAMPUS',
        HelpMessage='Enter the URL of the SharePoint web application for which all webs will be searched.')]
        HelpMessage='Enter the URL of the single SharePoint site for which all subwebs will be searched.')]
    [string]$logPath = 'c:\local\temp\findUserPermissions.log'
Set-PSDebug -Strict

function getPermType($mask) {
    #SharePoint permissions/roles are exposed programatically as "PermissionMask" attribute of the SPWeb.Permissions.Member objects.
    #Most of these masks are numeric.  This function will convert the numeric value into a "friendly" string, derived by examining 
    #site permissions in a web browser:
    [string]$return = switch ($mask) {
        'FullMask'   {[string]'FullControl'}
        '1012866047' {[string]'Design'}
        '1011028719' {[string]'Contribute'}
        '138612833'  {[string]'Read'}
        '138612801'  {[string]'ViewOnly'}
        '134287360'  {[string]'List-Library:UnknownAccess'} #Note that this mask has the label "Limited Access" in the GUI.
        default      {[string]"Unknown:$mask"}
    return $return

function checkADGroupMembers {
    param ([string]$adGroupName,[string]$adGroupSid,[string]$userRegex)
    #Searches the AD Group provided in -adGroup for matches against the regex provided in -userRegex.
    #Search is performed against the domain of the computer running the script.
    #The userRegex value can be generated using the "regexifyDomainUser" function. It MUST capture the 
    # username/samAccountName to a named capture group called 'name'.
    #Returns boolean true/false.
    #Need to add ability to return RegEx match objects from the search.
    #Optional ability to specify the domain to search?
    [Int32]$i = $adGroupName.IndexOf('\')
    [string]$groupSam = $adGroupName.Substring($i + 1)
    #write-host "Getting members of:" $groupSam
    [String[]]$returns = @()
    #Domain Users could take a long time to process, so let's just assume that the user is a domain user:
    if (($groupSam -eq 'domain users') -or ($groupSam -eq 'authenticated users')) {
        $returns += "!AllUsers!"
        return $returns
    #Get-ADGroupMember will error frequently because SharePoint contains a lot of orphaned groups.
    #Use "ErrorAction Stop" for force an breaking error when this happens, and just set $match to $false/
    try {
        [array]$grpMembers = @()
        if ($adGroupSid) {
            $grpMembers += Get-ADGroupMember -Recursive -Identity $adGroupSid -ErrorAction Stop `
                | Select-Object -ExpandProperty SamAccountName 
        } else {
            $grpMembers += Get-ADGroupMember -Recursive -Identity $groupSam -ErrorAction Stop `
                | Select-Object -ExpandProperty SamAccountName 
    } catch {
        write-host "    AD group $groupSam does not exist" -ForegroundColor Red
        $returns = $null
    if ($grpMembers.count -gt 0) {
        foreach ($memb in $grpMembers) {
            #write-host "testing" $memb "against" $userregex
            if ($memb -match $userRegex) {
                #write-host "regexmatch found: " $matches.user
                #Return the current regex named group "user".  THis is just showing off...
                #I could simply skip the capture groups and just return $memb.
                $returns += $matches.user
    } else {
        write-host "    AD group $groupSam has no members" -ForegroundColor Red
    if ($returns.count -gt 0) {
        return $returns
    } else {
        return $null

function regexifyDomainUser {
    param ([string]$user,[string]$domain)
    #Converts the provided domain\username pair into a regex that can 
    #be used to search for the same pattern in a larger string.
    #This regex will return the username in a capture group named "user".
    if ($user -match '\.') {
        $userMatch = $user.Replace('.','\.')
    } else {
        $userMatch = $user
    [string]$regexUser = '^' + $domain + '\\(?<user>' + $userMatch + ')$'
    return $regexUser
function regexifyUser {
    #Converts the provided username into a regex that can 
    #be used to search for the same pattern in a larger string.
    #This regex will return the username in a capture group named "user".
    param ([string]$user)
    if ($user -match '\.') {
        $userMatch = $user.Replace('.','\.')
    } else {
        $userMatch = $user
    [string]$regexUser = '^(?<user>' + $userMatch + ')$'
    return $regexUser

if ((Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null) {
    Add-PSSnapin Microsoft.SharePoint.PowerShell
Import-Module ActiveDirectory

#Set up regex strings that contain all of the samAccountNames provided in the input parameters.
#We want to be able to match all possible users with one regex for processing efficiency.

#DomainUsers will match against DOMAIN\username.
[string]$regexDomainUsers = regexifyDomainUser -user $users[0] -domain $domain
#Users will match againstjust username.
[string]$regexUsers = regexifyUser -user $users[0]
if ($users.count -gt 1) {
    for ($i=1; $i -lt $users.count; $i++) {
        #Set up a regex for matching the user in domain\username format:
        [string]$regexDomainUser = '|' + $(regexifyDomainUser -user $users[$i] -domain $domain)
        $regexDomainUsers += $regexDomainUser
        [string]$regexUser = '|' + $(regexifyUser -user $users[$i])
        $regexUsers += $regexUser

## Initialize output log:
if (Test-Path -LiteralPath $logPath) {
    Remove-Item -LiteralPath $logPath -Force
#CSV header row:
[string]$out = "user,Role,WebUrl,aceDetails"
$out | Out-File -FilePath $logPath 

#Get selected SharePoint sites:
[array]$sites = @()
if ($spSite) {                 #If spSite is specified, search only one site:
    $sites += Get-SPSite -Identity $spSite -Limit All
} elseif ($webApplication){    #If webApplication is specified, search all sites in the webapp:
    $sites += Get-SPSite -WebApplication $webApplication -Limit All
} else {                       #Otherwise, search all web applications defined on the local farm:
    $sites += Get-SPSite -Limit All

##Begin Main Loop:
foreach ($site in $sites) {
    $webs = @()
    # Gets webs in current site
    $webs = $site | % {Get-SPWeb -Site $_ -Limit All}
    foreach ($web in $webs) {
        Write-Host "Testing web:" $web.url -ForegroundColor cyan 
        $webPerms = @()
        $webPerms = $web.Permissions
        foreach ($perm in $webPerms) {
            #Scenario 1: ACL entry is for this specific user:
            if ($perm.member.loginName -match $regexDomainUsers) {
                [string]$user = ($perm.member.loginName).split('\') | select -Last 1
                write-host "    Found match $user in the web ACL list." -ForegroundColor yellow
                [string]$aclData = 'Acl:Direct'
                [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                $out | Out-File -Append -FilePath $logPath
            #Scenario 2: ACL entry is an Active Directory Group
            if ($perm.Member.IsDomainGroup) {
                [String[]]$ADGroupusers = @()
                $ADGroupUsers = checkADGroupMembers -adGroupName $perm.Member.LoginName -adGroupSid $perm.Member.Sid -userRegex $regexUsers
                if ($ADGroupUsers.count -gt 0) {
                    foreach ($user in $ADGroupUsers) {
                        write-host "    Found user $user in AD Group:" $perm.member.loginName ", which is on the web ACL list." -ForegroundColor yellow
                        [string]$aclData = 'Acl:Embedded:' + $perm.Member.LoginName 
                        [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData 
                        $out | Out-File -Append -FilePath $logPath
            #Scenarios: ACE is for a SharePoint group:
            if ($perm.Member.GetType().Name -eq 'SPGroup') {
                $members = @()
                $members += $perm.member.users
                foreach ($member in $members) {
                    #Scenario 3: ACL is a SharePoint group, and the group contains a matching user:
                    if ($member.loginName -match $regexDomainUsers) {
                        [string]$user = ($member.loginName).split('\') | select -Last 1
                        write-host "    Found match for" $user "in a Sharepoint group that is in the web site ACL." -ForegroundColor yellow

                        [string]$aclData = 'SPGroup:Direct:' + $perm.Member.LoginName
                        [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                        $out | Out-File -Append -FilePath $logPath
                    } #End Scenario 3
                    #Scenario 4: ACL is a SharePoint group, and the group contains an AD group that contains a matching user:
                    if ($Member.IsDomainGroup) {
                        [String[]]$ADGroupUsers = @()
                        $ADGroupUsers = checkADGroupMembers -adGroupName $Member.LoginName -adGroupSod $Member.LoginName.Sid -userRegex $regexUsers
                        if ($ADGroupUsers.count -gt 0) {
                            foreach ($user in $ADGroupUsers) {
                                write-host "    Found user $user in an AD Group that is found in a SharePoint group that appears in the web site ACL." -ForegroundColor yellow
                                [string]$aclData = 'SPGroup:Embedded:' + $perm.Member.LoginName + ':' + $member.loginName
                                [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                                $out | Out-File -Append -FilePath $logPath
                    } #End Scenario 4
            } #End SPGroup Eval
        } #End foreach $perm
        foreach ($admin in $web.SiteAdministrators) {
            #Enumerate Web Site Administrators (should be at least two)
            if ($admin.LoginName -match $regexDomainUsers) {
                [string]$user = $matches.user
                write-host "    Found web site administrator match for:" $user -ForegroundColor yellow
                [string]$out = $user + ',' + 'webSiteAdministrator,' + $web.Url
                $out | Out-File -Append -FilePath $logPath
                #exit #for debugging
    if ($site.owner.UserLogin -match $regexDomainUsers) {
        #Discover Site Owner (should be only one)
        [string]$user = $matches.user
        write-host "    Found site collection owned by:" $user -ForegroundColor yellow
        [string]$out = $user + ',' + 'SiteCollectionOwner,' + $site.Url
        $out | Out-File -Append -FilePath $logPath
        #exit #for debugging

Migrating to the SCCM UDI for OSD, part 6c: Operations – Applications

Continued from:

The following is a procedure for updating Application information in the UVM UDI environment.  Use it as a template for your own operations:

  1. When adding a new application (or app version): Categorize the application:
    1. In the SCCM console under Software Library->Application Management->Applications, select the new application and get “properties”.
    2. Under the “General Information” tab, Click “Select” next to the “Administrative Categories”.  Add the new application to an existing App Category or create a new category, as appropriate.  Applications that are categorized will be added to UDI. Non-categorized applications will not be available.
  2. Update the UDI config files:
    1. Run C:\local\scripts\CM-TaskSequences\build-UDIAppList.ps1.
    2. Locate the “MDT 2013 Files” package in the SCCM console under Software Library->Application Management->Packages, and run an “Update Distribution Points” action.  Verify that distribution was successful before proceeding.

And that is the whole story of the SCCM/UDI migration here at UVM to date. Clearly there is room for improvement. I will try to keep this series updated with revisions as we make them. As always, I hope that these posts will be of help to others in similar situations. If any of the code in this series makes its way into your environment, please let me know. I also am happy to answer any [short] questions about the topics covered here.

-J. Greg Mackinnon | Systems Administrator | University of Vermont
Phone: 802-656-8251 | Web:

Series Index:

Migrating to the SCCM UDI for OSD, part 6b: Operations – Operating Systems

Continued from part 6a:

The following is a procedure for updating Operating System image data in UDI that is specific to the UVM environment.  Use it as a template for your own operations:

      1. Add the new WIM image to SCCM. The WIM must contain only one OS image (e.g. at index 1).
      2. Run c:\local\scripts\CM-TaskSequences\build-udiInfoFiles.ps1.
        (This make a CSV files available to the UDI client that matches the OS Image Name to the version of the OS within the image.) (Required for driver injection.)
      3. Run c:\local\scripts\CM-TaskSequences\build-udiImageList.ps1.
        (This updates the UDI XML-based control file that generates the OS selection options to the end-user in the UDI Wizard.)
      4. Run c:\local\scripts\Update-OSApplyTaskSequence.ps1. When prompted, provide the name of the UDI task sequence to be updated.
        (This script updates the task sequence so that the OS selected in the UDI Wizard can be applied to the UDI target machine.)
      5. Locate the “MDT 2013 Files” package in the SCCM console under Software Library->Application Management->Packages, and run an “Update Distribution Points” action.  Verify that distribution was successful before proceeding.
        (This makes the new CSV info files and Wizard configs available to end-users.)


Series Index:

Migrating to the SCCM UDI for OSD, part 6a: Operations – Drivers

Continued from part 5:

The following is a operational procedure for updating applications, specific to the UVM environment. Use it as a template for managing your own procedures:

  1. If adding a new model: Establish the driver package source:
    1. Download the driver packages from Dell (or Lenovo). You will need different packages for each supported OS.
    2. Determine the WMI model name:
      1. Wmic computersystem get model -or-
      2. Get-WmiObject -class win32_computersystem
        (or more verbosely get-wmiobject -namespace root\cimv2 -class win32_computersystem -property model)
    3. Create a new folder under the matching OS version folder (i.e. Win7 or Win8) within the driver import source directory: “\\confman3\sources\drivers\import“. The name of this new folder must match the model information discovered in step 2. Extract the new drivers into this folder.
  2. If updating a model: Update the driver source:
    1. Locate the source folder for the driver package to update. Remove all contents of the directory including the “.hash” file in the root.
    2. Extract the new drivers into this folder.
  3. If updating a model: Cleanup existing drivers in SCCM:
    1. Find the package to be updated in the SCCM management console. Delete it.
    2. Right-click and get the properties on any driver in the SCCM console. Call up the list of driver categories, and delete the category for the driver to be updated.
  4. Import the drivers from source:
    1. Run C:\local\scripts\CM-Drivers\ImportDrivers.ps1 to import the new drivers into the SCCM environment. The script will create new packages and driver categories for each new folder that you created.
    2. Wait.
  5. Distribute the drivers:
    1. Refresh the SCCM console to reveal the new driver packages and categories.
    2. Right-click the driver package and select “Distribute Content”. Distribute the drivers to “Confman2”.
    3. Monitor the distribution process in the bottom pane of the SCCM console. Make sure distribution succeeds before proceeding.
  6. If adding a model/package: Update the MDT files with the new driver information:
    1. Run C:\local\scripts\CM-TaskSequences\build-UdiInfoFiles.ps1 on Confman3
    2. Locate the “MDT 2013 Files” package in the SCCM console under Software Library->Application Management->Packages, and run an “Update Distribution Points” action. Verify that distribution was successful before proceeding. This will publish a new list of drivers packages to the UDI clients.
  7. If adding a model/package: Update the OS Installation Task Sequence:
    1. Run C:\local\scripts\CM-TaskSequences\Update-DriverInjectionTaskSequence.ps1. Specify the name of the Task Sequence to update when prompted, or provide the name using the “-name” parameter. This script will update the Task Sequence to allow for injection of the new driver package, if one is available for the current model.
  8. If adding or updating WinPE, Peripheral, or “Other” drivers: Update the boot media:
    1. In the SCCM console under Software Library->Operating Systems->Drivers, select “Saved Searches”, then select WinPE 64-bit or WinPE 32-bit. Once the drivers have been filtered, Select All, and then right-click and select Edit->Boot Images. Select the MDT boot image for your architecture (32-bit or 64-bit).
    2. Select “Task Sequences” in the console, then select “Create Task Sequence Media”:
      1. Select to create “Bootable Media”
      2. Select “Site Based Media”
      3. Select “CD/DVD Set”, and specify the path: \\CONFMAN3\sources\os\boot\mdt\iso\UDI-x64.iso
      4. Clear “Protect media with password”, and select “Import PKI certificate”. Select a certificate and password from our super-secret source location. Select “Allow user device affinity with auto-approval”. (If the certificate is expired, see the separate procedure below for generating a new workgroup computer certificate.)
      5. For boot media, select the MDT boot image that you updated in part 8a. For distribution point, select CONFMAN2. For Management Point, select CONFMAN3.
      6. Complete the wizard, and then distribute the boot media to the usual locations (\\files\software\deploy).

As noted above, you need a custom Workstation Authentication certificate to generate the boot media.  If your certificate has expired or you want to generate a new certificate for any other reason, use the following procedure, adopted from:

  1. From a workstation that has access to your Certificate Authority web interface, open Internet Explorer using your admin account, and access:
    Logon as a user with rights to generate workgroup certificates.  Currently this is scoped to high-level admins in our organization.

    1. Add the site to the “Trusted sites” zone in your Internet Settings Control Panel.
    2. Activate “compatibility view” if your CA is still running on Server 2008 R2, otherwise the required ActiveX controls will not load.
    3. Select “Request a certificate”.
    4. Select “Create and submit a request to this CA”.
    5. Select the template: “UVM – SCCM Workgroup or WinPE Client Authentication”
      • Note: This is a lightly-modified copy of the stock “Workstation Authentication” certificate template.  As per MS requirements, the certificate has been forced to use the legacy “Server 2003” certificate format, not the Vista+ “CNG” format.
      • An Enterprise Admin can add permissions to the template to allow enrollment rights for additional users/groups, if necessary.
    6. Fill in the identifying information and friendly name for the certificate.  Make sure to select the option to “Mark keys as exportable”, then click “Submit”.
    7. When prompted, install the certificate.  The certificate will be installed into the “Personal” or “My” store for the user running Internet Explorer.
  2. Run MMC.EXE as the user who requested the certificate:
    1. Add the “Certificates” snap-in for the current user.
    2. Navigate to the “personal” store.  Locate the new certificate.
    3. Right-click the certificate and select All Tasks -> Export.
      1. In the certificate export wizard, select the option “Yes, export the private key.”
      2. Select Personal Information Exchange – PCKS #12 (.PFX), and ensure that “Include all certificates in the certification path if possible” AND “Export all extended properties” are checked, then click Next.
      3. Type in a password and confirm it in the boxes provided on the Password screen, then click Next. (Save this password for later use)
      4. Browse for a location to which to export the certificate.  Make sure that it is somewhere accessible from SCCM, give it a name (i.e. ‘WinPE-Cert.pfx’) and click Save.
  3. Use this new certificate file when completing step 8.4, above.


Series Index:

Migrating to the SCCM UDI for OSD, part 5: Quirks

Continued from part 4:

In the previous parts of this blog series I have shown how PowerShell and VBScript can be used to automate Driver, Application, and Operating System selection and installation in MDT/UDI. In this final post I will discuss a few final UDI quirks that we worked though without using scripts.

UDI Boot Media:

We experienced an odd problem during our OS Deployments when booting from SCCM/UDI media. The deployment process starts, as documented, with a dialog allowing the user to configure the networking environment for OS deployment, followed by a Task Sequence selection dialog. After selecting the task sequence, we expected some scripts to run, followed by the initiation of the UDI Wizard dialogs. Instead what we found was that UDI would first format an partition the local drive, copy a new WinPE environment to the drive, and then restart into WinPE running from local disk before brining up the UDI Wizard. This created a substantial delay between Task Sequence selection and the start of the Wizard. My impression was that users would find this delay unacceptable.

Our MS consultant suggested that this was happening because the revision of the UDI/WinPE environment on our boot media did not match boot image referenced by our active task sequence. To test this, we rebuilt all of the boot images in our environment from the Windows 8.1 ADK sources, then created a new MDT boot image from the ADK image, created a new task sequence referencing the ADK image, and finally generated new UDI Boot Media with the MDT boot image as a source. After completing these actions and updating our distribution points, we were able to get the UDI Wizard to start after Task Sequence selection without first requiring a reboot.

Improper OS Drive Letter Assignment

In our first deployments, we found that at the end of the Task Sequence, our operating system was left running on a D: or E: drive, instead of the usual “C:” drive. Knowing full well that this could cause massive end user confusion, I set out to correct this problem.

I was able to find a few potential solutions in Michael Neihaus’s always dependable TechNet blog:

After weighing the options, I chose to try the “Delete Mounted Devices from registry” option. This required me to import a custom task sequence into our SCCM instance, and then copy/paste the required Task Sequence steps into our production Task Sequence. This approach instantly was successful, so I am sticking with it. Thanks again, Michael.

Failure of UserExit Script Processing

Under MDT/LTI, we made use of the “User Exit Script” capabilities of MDT to generate unique default computer names for UDI clients. Cursory inspection of the docs suggested that User Exit script functionality also is supported under SCCM/UDI, so I decided to transplant our script. However, after doing so I found that the default presented in the UDI Wizard was #GenUniComp()#, rather than a generated computer name.

#GenUniComp()# is the value assigned in our CustomSettings.ini file for the OSDComputerName variable. However, MDI/UDI is supposed to “exit” any code placed between the hash tags (#) out to the Windows Scripting Host. “GenUniComp” is the name of a VBScript function that I defined in our “ZUVMUserExit.vbs” User Exit script. The function works well enough under MDT/LTI, so what is going wrong in UDI?

To diagnose this problem, it helps to know a bit about how User Exit scripts are processed. “UserExit” is set as a variable in the CustomSettings.ini for your environment. This variable is detected and processed by the ZTIGather.wsf script that is run several times during each MDT/LTI and SCCM/UDI deployment. To troubleshoot problems with the script, you should look to the SCCM/UDI log files generated by ZTIGather.wsf. When I examined the ZTIGather logs, I could find no reference at all to the UserExit variable getting processed by the ZTIGather script. The next logical question is, why not?

Logically, the problem is not likely to be caused by an error in the script. First, the same script works just fine under MDT/LTI. Additionally, the script uses the MDT logging functions, so if it had executed, we would have expected to see at least some log output. I thus assumed that the script was not getting executed at all. I started looking at the ZTIGather logs from the top, and found reported “processing [Settings]…” followed immediately by “processing [Default]…”. Interestingly, I had defined the UserExit variable under the [Settings] section of CustomSettings.ini, which worked find under MDT/LTI. However, the SCCM version of ZTIGather appears to process only “Priority” and “Properties” settings under the [Settings] section. My UserExit definition was getting ignored because it was in the wrong place. After moving the “UserExit” definition to the [Default] section, UDI started to process the GenUniComp() function of my User Exit script just as it had under MDT/UDI.

In the final section of this series, I will present operational instructions for the use of these scripts in our environment. These documents are tailored for the UVM environment, but might serve as a template for instructions at other sites.


Series Index:

Migrating to the SCCM UDI for OSD, part 4: Applications

Continued from part 3b:

Previously in this series I demonstrated how you can manage Operating System selection and application in UDI, and one (rather complicated) method for managing drivers in SCCM/UDI. To complete our UDI experience, I now present a handy script for automation of populating select SCCM applications into UDI.

There is some conventional wisdom floating around out there in the ‘tubes that you should not deploy the new SCCM 2012-style “Applications” in SCCM OSD Task Sequences. Instead, it is asserted that you should use only traditional application “Packages”. Since we have developed all or our application installer using the new “Application” objects, I have chosen to ignore this guidance. Fortunately, it appears that the earlier bugs that led to dispensing of this advice have been corrected in the 2012 R2 CU2-CU3 timeframe.

The script below will make use of Configuration Manager application tags to determine which Applications should be populated into UDI. Additionally, the tags are used to generate application groups in the UDI wizard. If you don’t want an application to appear in UDI, don’t tag it. Applications can have multiple tags, and thus can end up getting defined more than once un UDI. This does not appear to cause any problems during deployment.

This script will re-write the entire UDI designer “.app” file, which is an XML-formatted document that defines all of the applications and groups that will be displayed during the UDI Wizard (the default name for this file is ‘’). Because I am writing out the entire file, I chose to use the System.Xml.XmlTextWriter .Net Framework class to do the heavy lifting for me. I probably could have used System.Xml.XmlDocument (which I used in part 3 of this series for updating the main UDI XML control file), but the XmlTextWriter seemed to be a more direct route to getting the job done in this case.

Note that in this code I am again using the SCCM PowerShell cmdlets. In this case, I am using Get-CMCategory and Get-CMApplication. These commands easily could be replaced with simple WMI queries (as seen in part 3 of this series). I also am using some SCCM managed code. I call the “Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer” class in order to use the “Deserialize” method. This method converts the large volume of data in the SDMPackageXML attribute of the application object into somewhat more accessible PowerShell objects.

To use this script in your environment, you will need to update the $outPath, $CMSiteCode, and $CMBinPath variables to match your environment. After running the script, you will need to redistribute the content of your MDT Files package to your distribution points. You UDI clients will read the new data as soon as it is made available on your DP.

# build-UDIImageList.ps1
# J. Greg Mackinnon
# Created: 2015-01-26
# Updated: 2015-03-17 - Added host output to indicate task progress.  Removed block comments for blogging clarity.
# Populates the UDI Configuration Wizard XML APP file with all categorized applications gathered 
# from Configuration Manager.  
# Requires: a local installation of the Configuration Manager administration tools.
#   Modify $outPath, $CMSiteCode, and $CMBinPath to match your environment.
Set-PSDebug -Strict

[string] $outPath = 'O:\sources\os\mdt\files\Scripts\'

[string] $CMSiteCode = 'UVM'
[string] $CMBinPath = 'F:\Program Files\Microsoft Configuration Manager\AdminConsole\bin\'
[string] $CMModName = 'ConfigurationManager.psd1'
[string] $CMModPath = Join-Path -Path $CMBinPath -ChildPath $CMModName

[string] $CMDrive = $CMSiteCode + ':\'

Import-Module -Name $CMModPath | out-null
Push-Location $CMDrive

#Gather current "Administrative Categories" used to classify Applications, capture to an Array of strings:
write-host "Gathering Application Categories..."
[String[]] $CMAppGroups = Get-CMCategory -CategoryType AppCategories | % {$_.LocalizedCategoryInstanceName}

[int] $appCount = 1

# Output XML requires: 
    # DisplayName (map to LocalizedDisplayName)
    # Name, 
    # Guid (with ScopeID, ApplicationGUID... maps to "ModelName" CMApplication Property), 
    # description (optional? map to LocalizedDescription), 
    # type (deployment type?), 
    # ProductID (which can be found in the sdmpackagexml.deploymenttypes[#].installer.productcode)
# All separated into "ApplicationGroup" stanzas, with name= attributes, I think we can map this to "LocalizedCategoryInstanceNames"

#$xmlDoc = New-Object System.Xml.XmlDocument # Note this is the same as the 1 type accelerator
$utf8 = New-Object System.Text.UTF8Encoding
# Create The Document Writer:
$XmlWriter = New-Object System.XMl.XmlTextWriter($outPath,$utf8)
$xmlWriter.Formatting = "Indented"
$xmlWriter.Indentation = "5"

write-host "Gathering CM Applications..."
$CMApps = Get-CMApplication | Select-Object -Property LocalizedDisplayName,LocalizedDescription,ModelName,LocalizedCategoryInstanceNames,IsLatest,IsExpired,IsSuperseded,SDMPackageXML | ? {$_.IsLatest -and ($_.IsExpired -eq $false) -and ($_.IsSuperseded -eq $false)}

foreach ($group in $CMAppGroups) {
    write-host "Processing group: $group" -ForegroundColor yellow
    foreach ($app in $CMApps) {
        if ($app.LocalizedCategoryInstanceNames.contains($group)) {
            write-host "    Adding application " $app.LocalizedDisplayName -ForegroundColor cyan

            $appXml = [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::Deserialize($app.SDMPackageXML,$true)




                        $xmlWriter.WriteEndElement() # <-- End Setter
                    $xmlWriter.WriteEndElement() # <-- End Match
                    foreach ($type in $appXml.DeploymentTypes) {
                                if ($type.Installer.Technology -match 'MSI') {
                                } else {
                                    $xmlWriter.WriteString(' ')
                            $xmlWriter.WriteEndElement() # <-- End Setter
                        $xmlWriter.WriteEndElement() # <-- End Match
                $xmlWriter.writeEndElement() # <-- End ApplicationMappings

            $xmlWriter.WriteEndElement() # <-- End Application
            $appCount++ # Increment the appCount by one for use in the "ID" Application element property.
    $XmlWriter.WriteEndElement() # <-- End ApplicationGroup

$xmlWriter.WriteEndElement() # <-- End Application
$xmlWriter.WriteEndDocument() # <-- End XML Document
# Finish The Document


And that is all of the new code that I needed to write for SCCM/UDI. In the final part of this series, I will discuss a few SCCM/UDI quirks that were easily corrected without custom coding:

Series Index:

Migrating to the SCCM UDI for OSD, part 3b: Operating System Selection (Continued)

Continued from part 3a:

In part 3a of this series, I provided a script that automates updating of the OS selection dialog boxes in UDI. I also noted that while UDI presents that OS selection, the UDI client ignores the selection and instead installs whichever OS image was specified at image creation time. This was very frustrating because I was able to verify that the syntax of the generated UDI XML file was correct. In examining the task sequence logs, I even could see that the “OSDImageName” and “OSDImageIndex” variables had been set as expected. Yet still UDI would not apply the select image. Why?

I found multiple explanations for this phenomenon, none in the MDT/UDI documentation:

The upshot is, while UDI wizard will set the “OSDImageName” variable, the SCCM Task Sequence ignores it by design. This issue is exposed in the Task Sequence reference:

The observant reader will notice that the “Apply Operating System Image” task sequence step does not contain a variable for supplying an image name or path. You only can provide an image index. It looks like MS only supports selection of different images indexed in one monolithic WIM image. I am not enamored with this limitation, so I have authored the following script which will create one “Apply Operating System Image” for each OS in the SCCM inventory. Each step will have a condition that will allow the step to run only if the OSDImageName environment variable matches the name of the image specified in the task sequence step. Note that we are assuming only one image per WIM file in this case, so the image index of the selected WIM has to be ‘1’.

Note that this script requires the “UVM-ConfigurationManager.psm1” module file, which can be found in this post:

# Update-OSApplyTaskSequence:
# Created 2015-03-09, by J. Greg Mackinnon
# Updated 2015-03-12 - Added ability to update an existing, full OS installation Task Sequence.
#                    - Also removes pre-existing "Apply Apply Operating System Image " step.  

# Script will update the SCCM Task Sequence named in the $TSPackageName variable with a conditional 
# "Apply OS Image" step for each OS currently defined in SCCM.  

# Why?  Because unless all of your OS images are contained within a single WIM file, the SCCM deployment
# sequence will ignore any OS Image selection that you make in UDI.  Instead, we will have to rely on 
# UDI to set the "OSDImageName", and we will apply OS images based on that information.  
# NOTE: We are ASSUMING one image per WIM file, so the image index value will always be set to "1".

#WMI Classes associated with CM Task Sequences (that are relevant to us):
#   SMS_TaskSequencePackage                <-- The master Task Sequence object
#   SMS_TaskSequence                       <-- Each Task Sequence Package has one of these.
#   SMS_TaskSequence_Group                 <-- Logical groups of actions in the sequence.
#   SMS_TaskSequence_Condition             <-- A condition that can be attached to an action or group
#      SMS_TaskSequence_WMIConditionExpression        <-- Use "Model" MDT TS Variable instead!
#      SMS_TaskSequence_MakeModelConditionExpression  <-- Does not exist in the GUI! Do not use!
#      SMS_TaskSequence_VariableConditionExpression   <-- Condition based on a TS variable.
#      SMS_TaskSequence_ApplyOperatingSystemAction    <-- Apply an Operating System Image

# See them all by running:
#    Get-WmiObject -list -Namespace $namespace | select -property name | ? -Property name -Match "SMS_TaskSequence"

# Helpful resources:
#    The authority... MSDN on programming task sequences (VBScript and C#):
#    Describes how to expand properties from SMS objects with "lazy" properties:
#    Describes using the [wmi] type accelerator to retrieve WMI objects by absolute path:
#    Describes the difference between [wmi] and [wmiclass] objects:


Set-PSDebug -Strict

######## Begin Site-Specific Settings: ########
#Name of the TaskSequence to configure (must already exist):
[string]$TSPackageName = $name

#Name of the MDT Settings Package that contains the unattend.xml file to be used during deployment:
[string] $MDTSettingsName = 'MDT 2013 Settings'

#Name of the new Apply OS Images Group to be applied to the Task Sequence:
[string]$TSGroupName = 'UVM Apply Operating System Image Group'
######### End Site-Specific Settings: #########

Import-Module 'c:\local\scripts\UVM-ConfigurationManager.psm1'

#Set default value for $namespace, if not provided as a parameter:
if ((-not (Test-Path Variable:\namespace)) -or !$namespace) {[string]$namespace = Get-SMSSiteNamespace}

########## Begin Create New OS Apply TS Group ##########
# Get the Name and PackageID for all OS image packages currently defined in SCCM, and put them into an array:
[array]$OSPackages = @()
$OSPackages = Get-WmiObject -Namespace $namespace -Query "Select Name,PackageID from SMS_ImagePackage" | Select-Object -Property Name,PackageID | Sort-Object -Property Name 
#Instantiate a new, empty TS Group:
$newTSGroup = New-SMSObject -class SMS_TaskSequence_Group
#Configure the group:
$newTSGroup.Name = $TSGroupName
$newTSGroup.Description = "Copy this group into a task sequence to replace all pre-existing 'Apply Operating System Image' actions."
#Get the PackageID for our MDT Settings Package.  We will need this later when defining the "Apply Operating System Image" TS Step:
[string] $MDTSettingsID = $(Get-WmiObject  -namespace $namespace -query "Select PackageID from SMS_Package where NAME = '$MDTSettingsName'").PackageID
foreach ($package in $OSPackages) {
    #Add an action to run each Apply Operating System Image Action:
    $ApplyOSTSAction = New-SMSObject -class SMS_TaskSequence_ApplyOperatingSystemAction
    $ApplyOSTSAction.Name = ("Apply OS Package: " + $
    $ApplyOSTSAction.ConfigFileName = "unattend.xml"
    $ApplyOSTSAction.ConfigFilePackage = $MDTSettingsID
    $ApplyOSTSAction.Description = "Conditionally applies this OS Image Package to the local drive."
    $ApplyOSTSAction.DestinationVariable = "OSDisk"
    $ApplyOSTSAction.ImagePackageID = $package.PackageID
    $ApplyOSTSAction.ImageIndex = "1"
    #Create a Task Sequence Condition object:
    $TSCondition = New-SMSObject -namespace $namespace -class SMS_TaskSequence_Condition
    #Create a Task Sequence Condition Expression object:
    $TSConditionExp = New-SMSObject -namespace $namespace -class SMS_TaskSequence_VariableConditionExpression
    $TSConditionExp.Operator = "equals"
    $TSConditionExp.Value = $
    $TSConditionExp.Variable = "OSDImageName"
    #Add the condition expression to the "operands" attribute of the condition object:
    $TSCondition.Operands = @($TSConditionExp) #  Multiple conditions are possible, use an array.
    #Add the Condition object to the condition attribute of the Group object:
    $ApplyOSTSAction.Condition = $TSCondition #  Only one condition, not an array.
    $newTSGroup.Steps += @($ApplyOSTSAction)
########### End Create New OS Apply TS Group ###########

############# Begin Retrieve Existing TS #############
#Run the WQL queries required to get the fully-populated Task Sequence Package object (no loosely bound parameters)
[string]$filter = "name = '" + $TSPackageName + "'"
$TSP = Get-SMSFullObject -namespace $namespace -class SMS_TaskSequencePackage -filter $filter
#Get a class object for the object retrieved above.  This will allow access to static properties and methods not available in individual WMI objects.
$TSPClass = New-SMSClass -smsObject $TSP 
#For Task Sequence Packages, the GetSequence method allows us to get the sequence associated with a package.
#(Under CM 2012, each Package has one (and only one) Task Sequence)
#  Q: Why do this? The WMI object retrieved above already has a property "sequence", which contains all groups/steps in XML format.  Why can't we use that?
#  A: Because this is XML data that will be challenging to manipulate!  CM has separate classes for more controlled TS step manipulation.
#Note1: Interestingly, the retrieved object as a property "TaskSequence", which is the actual Task Sequence.  WHY!?!?!
#Note2: We also could use "New-SMSObject" to create an entirely new Task Sequence.
$TS = $TSPClass.GetSequence($TSP).TaskSequence
############## End Retrieve Existing TS ##############

#Locate the Task Sequence items that need to be modified:
[int32]$exeIndex = Get-SMSTSStepIndex -TSObject $TS -TSStepName 'Execute Task Sequence'
$ExeTSGroup = $TS.steps[$exeIndex]
[int32]$instIndex = Get-SMSTSStepIndex -TSObject $ExeTSGroup -TSStepName 'Install'
$instTSGroup = $ExeTSGroup.steps[$instIndex]

#Remove the existing "Apply OS Image" step, if it exists:
[string]$osApplyStepName = 'Apply Operating System Image'
if (Test-SMSTSStep -TSObject $instTSGroup -TSStepName $osApplyStepName) {
    Remove-SMSTSStep -TSObject $instTSGroup -TSStepName $osApplyStepName

#Remove the existing UVM Driver Group (if it exists):
if (Test-SMSTSStep -TSObject $instTSGroup -TSStepName $TSGroupName) {
    Remove-SMSTSStep -TSObject $instTSGroup -TSStepName $TSGroupName

#Identify the position within the task sequence group where we will add our new UVM OS Apply Group:
[int]$i = [int]$(Get-SMSTSStepIndex -TSObject $instTSGroup -TSStepName 'Set Variable for Drive Letter') + 1

#Add the new OS Apply Group to the Install Group after the position discovered in the previous step:
Add-SMSTSStep -TSObject $instTSGroup -TSStep $NewTSGroup -StepIndex $i

#Walk back up the task sequence tree, updating each parent group with the revised child groups:
Remove-SMSTSStep -TSObject $ExeTSGroup -TSStepName $instTSGroup.Name
Add-SMSTSStep -TSObject $ExeTSGroup -TSStep $instTSGroup -StepIndex $instIndex
Remove-SMSTSStep -TSObject $TS -TSStepName $ExeTSGroup.Name 
Add-SMSTSStep -TSObject $TS -TSStep $ExeTSGroup -StepIndex $exeIndex

#The moment of truth...
#Use the Task Sequence Package class "SetSequence" method to add our new or updated sequence to the task sequence package object:
try {
} catch  [System.Management.Automation.MethodInvocationException] {
    [string] $out = "Could not commit the Task Sequence to the Task Sequence Package. "
    $out += "Perhaps this Package is open for editing elsewhere? "
    $out += "Check the Management Point SMSProv.log for details. "
    Write-Error $out
#At this point, our Task Sequence has been updated, and the new steps will be available to clients!

# Dispose of all remaining objects: (Do we really need to do this?)

The script will create a single group of Apply Operating System tasks (one task for each OS Image Package in the infrastructure) in the Task Sequence, using the group name supplied in the $TSPackageName variable. If the group already exists, it will be updated. Additionally, if the stock "Apply Operating System Image" step is still present, it will be removed. Each time you add or remove an OS Image, you will need to re-run the script and re-copy/paste the actions.

We have now dealt with handling Drivers and Operating Systems in SCCM/UDI OS deployment. That leaves managing UDI application selection, which I will discuss in part four of this series, coming up next.

Series Index: