BitLocker, Windows 7, and Smart Cards

Mounting frustration with PGP Whole Disk Encryption has me revisiting BitLocker options this week. We last touched on this subject back in the early days of Vista. It appears that enough has changed with Windows 7 that we need to do some policy updates.

First off, I discovered that the BitLocker system volume encryption wizard was not presenting me with startup PIN or startup key options. Further, it also was not backing up recovery key data to Active Directory, as we originally designed it to under Vista. Why? Because Vista BitLocker policies do not apply to Windows 7! A quick step through the “Windows 7”-specific settings in Computer->Policies->Administrative Templates->Windows Components->BitLocker Drive Encryption (and the child “Operating System Drives” settings), and we are back in action.

Next, I noticed new settings related to smart cards in the new Group Policy settings. Does BitLocker now support storage of drive encryption keys on a Smart Card? No… not for system volumes anyway. Still, I was intrigued by option for removable and non-system volumes, and decided to try encrypting my office eSATA drive using BitLocker and a Smart Card certificate.

Microsoft provides the following information on certificate template requirements for BitLocker:
Madeningly, they specify that you should provide the ‘Enhanced Key Usage OID value of’, if you are going to specify an OID. They also say that your “key usage attribute” should be one of three possible options with names like “CERT_DATA_ENCIPHERMENT_KEY_USAGE
“. The problem with this guidance is that the Certificate Template Manager MMC snapin does not expose any such options, and I cannot find docmentation for management of certificate templates using “certutil.exe”, or any other available command line utilities. Am I supposed to use ADSI edit to do all my certificate management these days? Sheesh.

Some googling determined the following:

  1. Enhanced Key Usage, or EKU, is labeled “Application Policies” in the Certificate Template MMC, and they are exposed under the “Extensions” tab of a template properties dialog box.
  2. OID info is not displayed in the list of Application Policies. However, OID maps to the friendly name “BitLocker Drive Encryption”. Select this policy and add it as a cirtical extension, or just leave the Application Policies list empty. Note that you will need to have the “BitLocker Drive Encryption” feature activated on any system from which you you are editing the Certificate Template (I am indebted to DXter for this post which led me on the path to solving this little mystery).
  3. The “key usage attribute” referred to in the BitLocker documentation also is exposed in the “Extensions” tab of the Certificate Template properties dialog. In this case, we will be looking at the “Key Usage” extension. Selecting a value of “Allow key exchange only with key encryption (key encipherment)” appears to work.
  4. Under the “Request Handling” tab, I also specified that the certificate will be used for “Encryption”, and not “smartcard logon”, since I don’t want to use this same cert for Smart card logon… perhaps that will change later on. We also needed to enforce the use of the “Microsoft Smart Card Key Storage Provider” under the “Cryptography” tab, since we want to generate the certificate key on the smart card. (Note that you need CNG-compliant cards to use this option. CNG cards are the way to go!)

Discovering orphaned vmdk files in vSphere

On occasion we have found abandoned vmdk files in our vSphere infrastructure. I often have thought we needed to take some time to hunt down and exterminate these orphans. As is often the case, someone else already did the initial research required to make automation of this task possible, but I fou nd I needed to do some updating of the source scripts for improved accuracy, improved formatting, and compatibility with vSphere 4.1:

# getOrphanVMDK.ps1
# Purpose : List all orphaned vmdk on all datastores in all VC's
# Version : v2.0
# Author  : J. Greg Mackinnon, from original by HJA van Bokhoven
# Change  : v1.1  2009.02.14  DE  angepasst an ESX 3.5, Email versenden und Filegrösse ausgeben
# Change  : v1.2  2011.07.12 EN  Updated for ESX 4, collapsed if loops into single conditional
# Change  : v2.0  2011.07.22 EN: 
	# Changed vmdk search to use the VMware.Vim.VmDiskFileQuery object to improve search accuracy
	# Change vmdk matching logic as a result of VmDiskFileQuery usage
	# Pushed discovered orphans into an array of custom PS objects
	# Simplified logging and email output
Set-PSDebug -Strict

#Initialize the VIToolkit:
add-pssnapin VMware.VimAutomation.Core


[string]$strVC = ""								# Virtual Center Server name
[string]$logfile = "c:localtempgetOrphanVMDK.log"
[string]$SMTPServer = ""							# Change to a SMTP server in your environment
[string]$mailfrom = ""	# Change to email address you want emails to be coming from
[string]$mailto = ""							# Change to email address you would like to receive emails
[string]$mailreplyto = ""						# Change to email address you would like to reply emails

[int]$countOrphaned = 0
[int64]$orphanSize = 0

# vmWare Datastore Browser query parameters
# See
$fileQueryFlags = New-Object VMware.Vim.FileQueryFlags
$fileQueryFlags.FileSize = $true
$fileQueryFlags.FileType = $true
$fileQueryFlags.Modification = $true
$searchSpec = New-Object VMware.Vim.HostDatastoreBrowserSearchSpec
$searchSpec.details = $fileQueryFlags
#The .query property is used to scope the query to only active vmdk files (excluding snaps and change block tracking).
$searchSpec.Query = (New-Object VMware.Vim.VmDiskFileQuery)
#$searchSpec.matchPattern = "*.vmdk" # Alternative VMDK match method.
$searchSpec.sortFoldersFirst = $true

if ([System.IO.File]::Exists($logfile)) {
    Remove-Item $logfile

#Time stamp the log file
(Get-Date –f "yyyy-MM-dd HH:mm:ss") + "  Searching Orphaned VMDKs..." | Tee-Object -Variable logdata
$logdata | Out-File -FilePath $logfile -Append
#Connect to vCenter Server
Connect-VIServer $strVC

#Collect array of all VMDK hard disk files in use:
[array]$UsedDisks = Get-View -ViewType VirtualMachine | % {$_.Layout} | % {$_.Disk} | % {$_.DiskFile}
#The following three lines were used before adding the $searchSpec.query property.  We now want to exclude template and snapshot disks from the in-use-disks array.
# [array]$UsedDisks = Get-VM | Get-HardDisk | %{$_.filename}
# $UsedDisks += Get-VM | Get-Snapshot | Get-HardDisk | %{$_.filename}
# $UsedDisks += Get-Template | Get-HardDisk | %{$_.filename}

#Collect array of all Datastores:
#$arrDS is a list of datastores, filtered to exclude ESX local datastores (all of which end with "-local1" in our environment), and our ISO storage datastore.
[array]$allDS = Get-Datastore | select -property name,Id | ? {$ -notmatch "-local1"} | ? {$ -notmatch "-iso$"} | Sort-Object -Property Name

[array]$orphans = @()
Foreach ($ds in $allDS) {
	"Searching datastore: " + [string]$ds.Name | Tee-Object -Variable logdata
	$logdata | Out-File -FilePath $logfile -Append
	$dsView = Get-View $ds.Id
	$dsBrowser = Get-View $dsView.browser
	$rootPath = "["+$dsView.summary.Name+"]"
	$searchResult = $dsBrowser.SearchDatastoreSubFolders($rootPath, $searchSpec)
	foreach ($folder in $searchResult) {
	    foreach ($fileResult in $folder.File) {
			if ($UsedDisks -notcontains ($folder.FolderPath + $fileResult.Path) -and ($fileResult.Path.length -gt 0)) {
				IF ($countOrphaned -eq 1) {
					("Orphaned VMDKs Found: ") | Tee-Object -Variable logdata
					$logdata | Out-File -FilePath $logfile -Append
				$orphan = New-Object System.Object
				$orphan | Add-Member -type NoteProperty -name Name -value ($folder.FolderPath + $fileResult.Path)
				$orphan | Add-Member -type NoteProperty -name SizeInGB -value ([Math]::Round($fileResult.FileSize/1gb,2))
				$orphan | Add-Member -type NoteProperty -name LastModified -value ([string]$fileResult.Modification.year + "-" + [string]$fileResult.Modification.month + "-" + [string]$
				$orphans += $orphan
				$orphanSize += $fileResult.FileSize
				$orphan | ft -autosize | out-string | Tee-Object -Variable logdata
				$logdata | Out-File -FilePath $logfile -Append
				[string]("Total Size or orphaned files: " + ([Math]::Round($orphanSize/1gb,2)) + " GB") | Tee-Object -Variable logdata
				$logdata | Out-File -FilePath $logfile -Append
				Remove-Variable orphan
(Get-Date –f "yyyy-MM-dd HH:mm:ss") + "  Finished (" + $countOrphaned + " Orphaned VMDKs Found.)" | Tee-Object -Variable logdata
$logdata | Out-File -FilePath $logfile -Append

if ($countOrphaned -gt 0) {
	[string]$body = "Orphaned VMDKs Found: `n"
	$body += $orphans | Sort-Object -Property LastModified| ft -AutoSize | out-string
	$body += [string]("Total Size or orphaned files: " + ([Math]::Round($orphanSize/1gb,2)) + "GB")
    $SmtpClient = New-Object
    $ = $SMTPServer
    $MailMessage = New-Object
    $MailMessage.from = $mailfrom
    $MailMessage.replyto = $mailreplyto
    $MailMessage.IsBodyHtml = 0
    $MailMessage.Subject = "Info: VMware orphaned VMDKs"
    $MailMessage.Body = $body
	"Mailing report... " | Tee-Object -Variable logdata
	$logdata | Out-File -FilePath $logfile -Append
Disconnect-VIServer -Confirm:$False

WSUS Reporting with PowerShell

I have been trying to determine if our SCCM service has most of our domain clients registered, and have decided that the WSUS client database may be the best source of information on currently active domain members. As previously mentioned, WSUS is not pre-configured with a lot of useful infrastructure reports, but pulling data out with PowerShell is not overly difficult. Have a gander… this script generates a count of all current clients, counts by OS type, a count of Virtual Machine clients, and a few counts based of various source IP addresses.

#Get WSUS Computers script
# Finds and counts all registered computers matching various criteria specified in the script
# Optionally, the found computer names to the file defined in $outFile, forced to uppercase, trimmed of whitespace, and sorted.
# Generates an object $out, that is sent to the console at the end of the script.

set-psdebug -strict

#Initialize Variables
	#$outFile = [string] "\filessharedetsSAAjgmWSUSXps.txt"

	$hwModel = "Virtual|vm"
	$ipMatch = "^132.198|^10.245" # specify your internal network ip ranges here, in RegEx format.
	$wsusParentGroup = [string] "All Computers"
	$wsusgroup = ""
	$WindowsUpdateServer= [string] "" #specify your WSUS server here
	$useSecureConnection = [bool] $true
	$portNumber = [int] "443" #required if you have added SSL protection to your WSUS (which you should do).

#Instantiate Objects:
	#Required WSUS Assembly – auto installed with WSUS Administration Tools
	$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($WindowsUpdateServer,$useSecureConnection,$portNumber)
	$computerScope = new-object Microsoft.UpdateServices.Administration.ComputerTargetScope
	$computerScope.IncludedInstallationStates = [Microsoft.UpdateServices.Administration.UpdateInstallationStates]::All
	$computers = $wsus.GetComputerTargets($computerScope)
	$wsusData = new-object System.Object
	$out = @()

$wsusData | add-member -type NoteProperty -name Criteria -value ("Total comptuers")
$wsusData | add-member -type NoteProperty -name Count -value ($computers.count)
$out += $wsusData
remove-variable wsusData

$osType = "Windows 7"
$filtComps = $computers | ? {$_.OSDescription -match $osType}
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("Windows 7")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData

$osType = "Windows Vista"
$filtComps = $computers | ? {$_.OSDescription -match $osType} 
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("Windows Vista")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData

$osType = "Windows XP"
# final "select" in the pipeline if you want to generate a list of computer names matching the criteria.
$filtComps = $computers | ? {$_.OSDescription -match $osType} | select-object -Property FullDomainName
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("XP Professional")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData

#Filter for virtual machine models
$filtComps = $computers | ? {$_.Model -match $hwModel}
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("Virtual Machines")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData 

$filtComps = $computers | ? {$_.IPAddress -notmatch $ipMatch} | select-object -Property IPAddress
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("Non-UVM Addresses")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData

## Following section does not produce useful data... WSUS does not see NAT-based addresses, on the public IP in front of the NAT.
## However, it is a good regex... it matches any non-routable (private) IPv4 address.  Take note for future use.
#$ipMatch = "^10.|^192.168.|^72.[1-2][0-9].|^72.3[0-1]."
#$filtComps = $computers | ? {$_.IPAddress -match $ipMatch}
#$wsusData = new-object System.Object
#$wsusData | add-member -type NoteProperty -name Criteria -value ("NAT Addresses")
#$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
#$out += $wsusData
#remove-variable wsusData

$ipMatch = "^10.245." # Our Wi-Fi and VPN clients fall in this IP range.  Substitute your internal (non-routed) IPs here.
$filtComps = $computers | ? {$_.IPAddress -match $ipMatch} 
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("UVM Wireless/VPN Addresses")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData

#Generate file output by: removing all but the RDN of the computer name, trimming any whitespace, forcing to uppercase, 
# sorting, suppressing headers, then writing to file.
#$filtComps | foreach {$_.FullDomainName.split('.')[0]} | foreach {$_.Trim()} | foreach {$_.ToUpper()} | `
	#sort-object | Format-Table -HideTableHeaders | Out-File -FilePath $outFile -Force
$out | Format-Table -AutoSize