Tag Archives: Group Policy

PowerShell for EAP-PEAP secured SSTP VPN on Windows 8.1

Simple VPN configurations can be deployed by Group Policy but EAP authentication settings cannot be configured like this, even using Windows 8.1 and Windows Server 2012 R2. Microsoft added some new PowerShell cmdlets to Windows 8.1 for configuring VPNs, but the worked examples do not appear to function for all the settings for PEAP connections, and they do not show a worked example of how you go about exporting and re-importing a connection’s XMLStream.

Defining the XML as a block within the script itself, even assigning it as data type XML does not seem to work. Not being particularly accustomed to PowerShell, the following script took a while to get right. I assigned it as a laptop startup script by GPO. If I need to modify the connection in future I can increment the version number since the script checks the local machine Registry for that, and will not install if the desired version marker is already present.

# VPN Connection EAP-PEAP VPN provisioning 
# patters 2013

# This script is loosely based on the EAP-TTLS one published by Microsoft at http://technet.microsoft.com/en-us/library/jj613766.aspx
# The worked examples on that page and at http://technet.microsoft.com/en-us/library/jj554822.aspx
# are rudimentary, and in some details for PEAP, incorrect. To set advanced options like the TrustedRootCAs and the
# the server identity checking warning, you *must* export a GUI-authored config as XML. Configuring XML attributes alone
# will not work because some of them are missing when creating a new connection, and adding them results in errors.

# Check for marker in the Registry, and quit if found
# Desired version is 1
$version = 1
$test = Get-ItemProperty "HKLM:\Software\MyCompany" "MyCompany VPN" -ErrorAction SilentlyContinue
If ($test -eq $null) {
       $test = 0
} else {
       $test = $test."MyCompany VPN"
If ($test -ge $version) {exit} 

# VPN Connection look-up to remove any previous installations
$isTestVpn = $false
$vpnConnections = Get-VpnConnection -AllUserConnection
If($vpnConnections.Name -eq "MyCompany VPN") {Remove-VpnConnection -Name "MyCompany VPN" -AllUserConnection -Confirm:$false -Force}
$vpnConnections = Get-VpnConnection
If($vpnConnections.Name -eq "MyCompany VPN") {Remove-VpnConnection -Name "MyCompany VPN" -Confirm:$false -Force}

       #The following section documents the attempts to get this working manually before I got importing/exporting of XML working

       # http://technet.microsoft.com/en-us/library/jj554822.aspx says to use "New-EapConfiguration -Peap" here, but is wrong      
       #$a = New-EapConfiguration

       # Generate configuration XML for PEAP authentication method with EAP-MSCHAPv2 as its inner method
       #$b = New-EapConfiguration -Peap -VerifyServerIdentity -FastReconnect $true -TunnledEapAuthMethod $a.EapConfigXmlStream

       # Edit properties within the generated configuration XML
       #$c = $b.EapConfigXmlStream
       #$c.EapHostConfig.Config.Eap.EapType.ServerValidation.ServerNames = "vpn.mycompany.com"

       # Specify AddTrust Root CA for Comodo - This attribute is missing unless you create the connection using the GUI
       # The following appears to generate the XML correctly, but it won't be accepted by the Add-VpnConnection cmdlet
       #$c.EapHostConfig.Config.Eap.EapType.ServerValidation.SetAttribute("TrustedRootCA","02 fa f3 e2 91 43 54 68 60 78 57 69 4d f5 e4 5b 68 85 18 68")   

       # PeapExtensions settings are nested XML objects so setting them as string datatype will fail
       # see http://www.vistax64.com/powershell/173859-xml-property-text.html
       #$c.EapHostConfig.Config.Eap.EapType.PeapExtensions.PerformServerValidation."#text" = "true"
       #$c.EapHostConfig.Config.Eap.EapType.PeapExtensions.AcceptServerName."#text" = "true"
       # Once again this attribute is missing unless the connection is created using the GUI. Adding it does not work
       #$c.EapHostConfig.Config.Eap.EapType.PeapExtensions.PeapExtensionsV2.AllowPromptingWhenServerCANotFound."#text" = "true"      

       # Create the VPN connection ‘MyCompany VPN’ with the EAP configuration XML generated above
       #Add-VpnConnection -Name "MyCompany VPN" -ServerAddress "vpn.mycompany.com" -TunnelType Sstp -EncryptionLevel Maximum -AuthenticationMethod Eap -EapConfigXmlStream $c -AllUserConnection

       # FORTUNATELY THERE IS AN EASIER WAY (once you figure out PowerShell XML – why couldn’t MS have shown a worked example in the docs)...

       # Create your VPN configuration entry manually then export its XML like so:
       #$exportXML = (Get-VpnConnection -Name "My_VPN_Final" -AllUserConnection).EapConfigXmlStream

       $importXML = New-Object XML
       $importXML.Load("\\mycompany.com\data\Software\MyCompany VPN\MyCompany VPN.xml")
       Add-VpnConnection -Name "MyCompany VPN" -ServerAddress "vpn.mycompany.com" -TunnelType Sstp -EncryptionLevel Maximum -AuthenticationMethod Eap -EapConfigXmlStream $importXML -AllUserConnection
       # Leave a marker in the Registry
       If (-Not (Test-Path "HKLM:\Software\MyCompany")) {New-Item -Path "HKLM:\Software\MyCompany"}
       if (Get-ItemProperty "HKLM:\Software\MyCompany" "MyCompany VPN" -ErrorAction SilentlyContinue) {
              Set-ItemProperty -Path "HKLM:\Software\MyCompany" -Name "MyCompany VPN" -Value $version
       } else {
              New-ItemProperty -Path "HKLM:\Software\MyCompany" -Name "MyCompany VPN" -Value $version

       Write-Host "Error in connection setup!"
       Write-Host $_.Exception.Message

Printing and Windows 7 rollout

The goal is installing drivers automatically as printers are connected at logon, without user interaction.
If your users are non-privileged, you will need to add a Group Policy setting to suppress the elevation prompts. Using a PC running Vista or later with the Group Policy Management MMC tool, set:

  • Computer Configuration > Policies > Administrative Templates > Printers > Point and Print Restrictions

If your print servers are likely to remain on Windows 2003, you will most likely need to find a way of loading Windows 7 drivers for x86 and x64 architectures. You can’t do this on the Windows 2003 host, however you can accomplish this by using a Windows 7 machine and loading Print Management from Administrative Tools. Then right-click on Print Servers branch and add your 2003 print server. Now you can add the necessary drivers.

Print Management

The final problem is an intermittent one. Occasionally you will find that documents won’t print, and when you investigate, you will find a document in front of the print job in the print queue called Local Downlevel Document.

According to this thread, it transpires that Windows Vista and newer introduced a new protocol (Asynchronous RPC) and for some reason the OS is too stupid to actually check whether the remote print server supports it before attempting to use it (Windows 2003 does not support it).

The workaround is to set to disable Async RPC on Vista/Windows 7 clients, again by Group Policy. Set the following Registry value:

HKLM\Software\Policies\Microsoft\Windows NT\Printers\EnabledProtocols
Data: 6

Windows 7 login scripts and missing network drives

Most Windows sysadmins use a Group Policy object to launch their login script. You may have noticed that Windows 7 and Vista fail to connect network drives defined in the login script if the user is a local admin and UAC is enabled. The script completes successfully and no error condition is encountered, but no drive mappings. Run it again manually and everything’s fine.

This is in fact by design, and it’s caused by the way UAC works. When you’re a member of the Administrators group and you log in, your account is busted down to a non-privileged user by UAC. This running context is completely separate from the context you get when you right-click Command Prompt and launch as an administrator. As you’ll probably have noticed, network drives connected in one context are not visible in the other. The problem is that GPO-based login scripts are being executed in the Administrator context – not the ordinary user context.

So, how do we get it working? Microsoft offer an unsupported kludge in KB937624 – getting around the issue by weakening Windows security and forcing network connections to be common to both user contexts. They carefully designed this not to be the case, so modifying the behaviour does seem like a bad idea.

However, Microsoft’s preferred solution (example launchapp.wsf script in the appendix of that page) is to use the GPO-triggered script to set a Scheduled Task to run immediately in the other (non-admin) context, and run your login script from there – much better.
The reasons I’m writing all this up are that:

  • Microsoft’s example script has some illegal character/line-wrap in there – copying and pasting it won’t work!
  • This method doesn’t work with XP so some forking logic is needed if you have mixed clients.
  • They make no allowances for multiple admin/non-admin users sharing the same PC.
  • This appears to be Microsoft’s sole example document of how to program using the Task Scheduler 2.0 API, and it neglects to define several absolutely essential object properties if you want to do something more useful than simply open Notepad.

My particular problem was that I needed to launch a script with a space character in the path, e.g.:

cscript.exe //nologo "\\domain.com\netlogon\departmentX users\logon.vbs"

For me changing this path name was not an option as there were many other dependencies. I spent a long time wondering why the API was eating my quotes as I fed it the above string and I tried various ways to escape them. Eventually I launched the Scheduled Tasks MMC tool (click on the root of Task Scheduler Library to see the job). Looking in the Action properties I realised that there are separate fields for the starting directory and for the arguments. Manually editing the job to use these got it working:
Task Properties Dialog
Frustratingly, there don’t seem to be any examples on the Web showing you how to populate these fields programmatically. Guessing the Arguments property was straighforward but StartIn is not a valid propery name. I read on Wikipedia that Task Scheduler 2.0 uses XML to store its jobs so I exported the job and viewed it. Luckily they used consistent property names in the XML (Arguments and WorkingDirectory) and I was able to set them in VBScript (see highlighted lines below).

There was an additional complication though. Once a user has run the Scheduled Task, it’s left behind on the system. In my initial testing this wasn’t a problem because I was testing admin users, but I soon discovered that a non-privileged user cannot delete and recreate the task if one created by another user already exists. So we need only schedule the task if the current user is running in an elevated security context. By far the simplest method is to parse the output of the whoami /groups command, as explained in this post:

UPDATE – added some logic to prevent the login script from launching for RemoteApp sessions to Terminal Servers.

'launchapp.vbs, modified from Microsoft's launchapp.wsf
'launches a process as interactive user, NOT as the elevated privilege user context

Option Explicit

Const TriggerTypeRegistration = 7
Const ActionTypeExecutable = 0
Const FlagTaskCreate = 2
Const LogonTypeInteractive = 3

Dim strWorkingDirectory, strHostname, strOSVer, colProcessList, strUser, strDomain
Dim objNetwork, objComputer, objShell, objExec, objWMI, objItem, strScriptName, strStdOut

strWorkingDirectory = "\\domain.com\netlogon\DepartmentX Users"

'launch this login script
strScriptName = "logon.vbs"

Set objNetwork = CreateObject("WScript.Network")
Set objShell = CreateObject("WScript.Shell")
Set objWMI = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2") 
strHostname = objNetwork.ComputerName
Set objComputer = GetObject("WinNT://" & strHostname & ",computer")
strOSVer = objComputer.OperatingSystemVersion

If strOSVer >= "6.0" Then
  If IsElevated() Then
    'Machine has UAC and user is elevated so use LAUNCHAPP.WSF Task Scheduler method based
    'on appendix from http://technet.microsoft.com/en-us/library/cc766208(WS.10).aspx

    'Are we launched in a RemoteApp session on a Terminal Server? If so, quit.
    Set colProcessList = objWMI.ExecQuery("Select * from Win32_Process Where Name = 'rdpshell.exe'")
    For Each objItem In colProcessList
      objItem.GetOwner strUser, strDomain
      'If we're an admin we can see all users' processes so we need to check only our own
      If strUser = objNetwork.UserName Then
      End If  

    'User is not elevated, so launch the script normally
    objShell.Run "cscript.exe //nologo " & Chr(34) & strWorkingDirectory & "\" & strScriptName & Chr(34), 1
  End If
  'This is a Windows XP/2003 machine, so launch the script normally
  objShell.Run "cscript.exe //nologo " & Chr(34) & strWorkingDirectory & "\" & strScriptName & Chr(34), 1
End If

Set objNetwork = nothing
Set objComputer = nothing
Set objShell = nothing

Function IsElevated()
  IsElevated = False
  strStdOut = ""
  Set objExec = objShell.Exec ("whoami /groups")
  Do While (objExec.Status = 0)
    WScript.Sleep 100
    If Not objExec.StdOut.AtEndOfStream Then
      strStdOut = strStdOut & objExec.StdOut.ReadAll
    End If
  If InStr(strStdOut,"S-1-16-12288") Then
    IsElevated = True
  End If
  Set objExec = nothing
End Function

Sub LaunchApp
  Dim objTaskService
  Dim strTaskName, rootFolder, taskDefinition, triggers, trigger, Action

  'Create the TaskService object
  Set objTaskService = CreateObject("Schedule.Service")
  Call objTaskService.Connect()
  strTaskName = "Launch App As Interactive User"

  'Get a folder to create a task definition in
  Set rootFolder = objTaskService.GetFolder("\")

  'Delete the task if already present
  On Error Resume Next
  Call rootFolder.DeleteTask(strTaskName, 0)

  'Create the new task
  Set taskDefinition = objTaskService.NewTask(0)

  'Create a registration trigger
  Set triggers = taskDefinition.Triggers
  Set trigger = triggers.Create(TriggerTypeRegistration)

  'Create the action for the task to execute
  Set Action = taskDefinition.Actions.Create(ActionTypeExecutable)
  Action.Path = "cscript.exe"
  Action.Arguments = "//nologo " & strScriptName
  Action.WorkingDirectory = strWorkingDirectory

  'Register (create) the task
  call rootFolder.RegisterTaskDefinition(strTaskName, taskDefinition, FlagTaskCreate,,, LogonTypeInteractive)

  Set objTaskService = nothing
End Sub