Tome's Land of IT

IT Notes from the Powertoe – Tome Tanasovski

Category Archives: Active Directory

Enable WinRM with Group Policy, but use PowerShell to Create the Policy

This past weekend I finished my chapter on Group Policy for the PowerShell Bible.  One of my coauthors asked me if I was going to include a script that configures WinRM (PowerShell remoting) via group policy in my chapter.  Truth be told, I hadn’t thought of it, but I love a challenge!

Unfortunately, when all was said and done the script that I wrote was a bit overly wordy and dug deeply into concepts that were out of scope for the 12 page chapter I was writing on the GroupPolicy module that ships with the Group Policy Management Console (GPMC) in 2k8R2 and RSAT.  Except for a few minor quirks like using ‘Yes’ and ‘No’ for parameter values instead of using $true and $false, the GroupPolicy module is a really great set of cmdlets because of what you can do with it.  You can create GPO reports, RSOPs, manage links, manage security, adjust inheritance, and backup and restore extremely easily (feel free to preorder the book to see just how easy ;)).

The final bit that makes this module amazing is that you can dynamically manipulate the GPO settings themselves as long as they are either ADMX/ADML registry changes or the relatively new 2008 registry settings under preferences.  Actually, you don’t even need an ADMX/ADML with a registry specified to create an ad-hoc registry change within a policy via the cmdlets (mind you this screws up your normal GPMC view when you do so – they look like orphaned settings like you would see when someone accidentally deletes an admx from your system).  The problem with the cmdlets that let you modify and work with these registry settings is that they are completely disconnected from the ADMX/ADMLs.  In other words you must specify the registry key, name, and value you want the policy to change rather than the name of the setting you are looking to change that is shown in the ADML and the GPMC – names like “Allow automatic configuration of listners”.  The only way you can get these registry keys and possible values is to manually crack open the ADMX/ADML files to see what each setting is touching.  This means you need to do a lot of detective work in order to script changes to a GPO.

Enabling WinRM via group policy is pretty decently documented on many blogs out there on the Internet.  It requires you to touch three places: the WinRM settings, the firewall, and the place where you configure services to start up automatically.  The WinRM settings were easy to track down because it has an ADMX and ADML file.  I was able to crack them open to find that the policy makes changes to this key: HKLM:\Software\Policies\Microsoft\Windows\WinRM\Service.  The firewall setting was a bit trickier because there is no direct ADMX and ADML file, but I saw that the following key was created after the GPO was applied to my computer: HKLM:\Software\Policies\Microsoft\WindowsFirewall\FirewallRules.  The name and value you need to set are both long and undocumented (as far as I can tell), but they make perfect sense.  The final bit was figuring out how to start the service.  This wound up being ridiculously tricky.  After a lot of tinkering I learned that service startup and security information is stored in a flat file in sysvol called GptTmpl.inf underneath the GUID for the policy.  In the end the script really speaks for itself.

I am dying to have someone else try this script out to see if everything works perfectly on another 2k8R2 domain.  It’s possible that I have something tied directly to my domain (other than the OU at the beginning), but I’m not sure.  I’m concerned that the firewall settings need LDAP objects to be created or updated.  Please let me know in the comments if you get it to work or have any problems.

Without further ado, todo, and blahdoo, I present a script to generate a GPO to enable WinRM in your domain that will be bound to an OU you specify in $OU (yes I’m too lazy to param()).

Import-Module GroupPolicy

# Specify the OU to link the GPO to
$OU = 'OU=TheIsland,DC=home,DC=toenuff,DC=com'

# Create the GPO
$gpo = New-GPO Remoting -Comment 'GPO that will enable remoting'

# Add the policy to allow WinRM
$winrmkey = 'HKLM\Software\Policies\Microsoft\Windows\WinRM\Service'
$params = @{
    Key = $winrmkey;
    ValueName = 'AllowAutoConfig';
    Value = 1;
    Type = 'Dword';
}
$gpo |Set-GPRegistryValue @params

# Set the filters to allow IPv4 and IPv6 traffic from all IPs for WinRM
$winrmkey = 'HKLM\Software\Policies\Microsoft\Windows\WinRM\Service'
$params = @{
    Key = $winrmkey;
    ValueName = 'IPv4Filter';
    Value = '*';
    Type = 'String';
}
$gpo |Set-GPRegistryValue @params
$params.ValueName = 'IPv6Filter'
$gpo |Set-GPRegistryValue @params

# Add the firewall rule to allow port 5985 for WinRM HTTP traffic
$fwrule = 'v2.10|Action=Allow|Active=TRUE|Dir=In|Protocol=6|LPort=5985|'
'App=System|Name=@FirewallAPI.dll,-30253|Desc=@FirewallAPI.dll'
$fwrule += ',-30256|EmbedCtxt=@FirewallAPI.dll,-30252'

$params = @{
    Key = 'HKLM\Software\Policies\Microsoft\WindowsFirewall\FirewallRules';    
    ValueName = 'WINRM-HTTP-In-TCP';
    Value = $fwrule;
    Type = 'String';
}
$gpo |Set-GPRegistryValue @params

# Manually add the inf setting to configure WinRM to start automatically
$inf = @'
[Unicode]
Unicode=yes
[Version]
signature="$CHICAGO$"
Revision=1
[Service General Setting]
"WinRM",2,"D:AR"

'@
$sdsad = [System.DirectoryServices.ActiveDirectory.Domain]
$domain = $sdsad::GetCurrentDomain().name
$path = "\\$domain\sysvol\$($env:LOGONSERVER)\Policies\$($gpo.Id)\Machine\"
$path += "Microsoft\Windows NT\SecEdit"
if (!(Test-Path $path)) {
    md $path
}
$inf |Out-File (Join-Path $path 'GptTmpl.inf')
# Link the GPO to the OU
$link = $gpo |
    New-GPLink -Target $OU -LinkEnabled 'Yes'
Advertisements

Enable CredSSP from a Windows 7 Home Client

While I do have a Win 7 Pro license, pure laziness has kept me from installing it on my Alienware laptop. This is fine for most things, but I ran into a bit of a problem while writing the Active Directory chapter for the PowerShell Bible. In order to use the ActiveDirectory module via remoting on my 2k8r2 server I needed to use CredSSP as my authentication type. Step one was easy. Launch PowerShell as an admin and run:

Enable-WSManCredSSP -Role client -DelegateComputer server1.home.toenuff.com

Now, if you’ve ever run this before you are probably familiar with the next error message that will return if you try and run Enter-PSSession or Invoke-Command with -Authentication CredSSP:

Enter-PSSession : Connecting to remote server failed with the following error message : The 
WinRM client cannot process the request. A computer policy does not allow the delegation of 
the user credentials to the target computer. Use gpedit.msc and look at the following policy
: Computer Configuration -> Administrative Templates -> System -> Credentials Delegation -> 
Allow Delegating Fresh Credentials. Verify that it is enabled and configured with an SPN ap
propriate for the target computer. For example, for a target computer name "myserver.domain.
com", the SPN can be one of the following: WSMAN/myserver.domain.com or WSMAN/*.domain.com. 
For more information, see the about_Remote_Troubleshooting Help topic.
At line:1 char:16
+ Enter-PSSession <<<< -Credential $cred server1 -Authentication credssp
 + CategoryInfo : InvalidArgument: (server1:String) [Enter-PSSession], PSRemoti 
 ngTransportException
 + FullyQualifiedErrorId : CreateRemoteRunspaceFailed

In most versions of Windows you can then follow the instructions in the error message which tell you explicitly how to handle this, but with a home version of Windows this is a bit of a problem because there is no gpedit.msc snapin or local policy to modify. Fortunately, we know that every policy is really just a registry setting. I was able to track it down specifically:

  • Create a Dword key called AllowFreshCredentials with a value of 1 in hklm:\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation
  • Create a separate String entry for each computer in hklm:\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials
  • Each entry within the AllowFreshCredentials key should be named an integer, and the first entry should be 1
Strangely, the above fix used to work for me, however, recently a new error started appearing that talked about another local policy.  I thought this was related to SP1, but I’ve been told by others that this has existed for some time.   This additional local policy has to be set in order to allow CredSSP to use NTLM authentication instead of Kerberos.  The fix is the exact same as the above except that the AllowFreshCredentials name also has AllowFreshCredentialsWhenNTLMOnly.
The following script will do both of these registry changes for you.  Mind you, it doesn’t handle entries already in the AllowFreshCredentials or AllowFreshCredentialsWhenNTLMOnly key, but you could easily handle that if it was a concern you had:
$allowed = @('WSMAN/*.home.toenuff.com','WSMAN/server1')            

$key = 'hklm:\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation'
if (!(Test-Path $key)) {
    md $key
}
New-ItemProperty -Path $key -Name AllowFreshCredentials -Value 1 -PropertyType Dword -Force            

$key = Join-Path $key 'AllowFreshCredentials'
if (!(Test-Path $key)) {
    md $key
}
$i = 1
$allowed |% {
    # Script does not take into account existing entries in this key
    New-ItemProperty -Path $key -Name $i -Value $_ -PropertyType String -Force
    $i++
}

Attack the AD! – All computers in the domain that have been logged in during the last 60 days

I’ve been scripting for a while.  From batch to VBS to Perl, I’ve often found a need to automate things.  When I was learning Perl, ADSI was the new hotness.  It gave a real way to query and use AD through automation.  Sure there were caveats: You can hit it via LDAP or WINNT, but not both now.  What property did you need?  Oh!  Go find that through the other provider.

My needs for AD scripting have fortunately always been very limited – until now, that is.  Now that my scripting muscle must touch hundreds of thousands of computers I’ve had to be honest with myself that scripting against AD truly does matter.  So let’s look at a question that I was asked during the interview process with my current employer: “How would you get me a list of all of the computers in a domain that are enabled and have been logged in within the past 60 days?”  The answer at the time was, “I don’t know”, but I had enough knowledge to dance around it in a way that was informed – plus I knew a ton of other stuff.  Needless to say I landed the job, but now I had to ask myself, “how would I do it?”

So let’s explore.  I had a trick up my sleeve to connect a DirectoryEntry object to my current domain.  So I started with that:

$de = New-Object DirectoryServices.DirectoryEntry('LDAP://rootDSE')
$root = New-Object DirectoryServices.DirectoryEntry("LDAP://$($de.DefaultNamingContext)")

The next step was to create a DirectorySearcher against my DirectoryEntry:

$searcher = New-Object System.DirectoryServices.DirectorySearcher($root)

With a searcher in hand I could set a filter that would show me all computers and then execute that searcher to get me a list of all computers:

$searcher.Filter = "(&(objectCategory=computer))"
$comps = $searcher.findall()
$comps

The first thing I noticed was that it only returned 1000 records when there should have been 50K.  This is due to a limitation with the maximum returned by the server.  To get around it you need to tell your searcher to have a page value less than the maximum of 1000:

$searcher.PageSize = 500

After doing this there were two obvious problems:

  1. It was returning all computers without constraints
  2. It was slow as s*#t

The speed wound up being very simple.  By adding constraints and telling it to only return the values I cared about the speed increased dramatically.  I’ll get to the constraints in a moment. The syntax to force it to only return the value (name) I wanted was:

$searcher.PropertiesToLoad.AddRange(@("name")) #obviously this can be a list of properties if you need more than the name

Now the constraints – the first of which was to make sure I only returned enabled accounts:

$searcher.Filter = "(&(objectCategory=computer)(!(userAccountControl:1.2.840.11356.1.4.803:=2)))"

While it seems bizarre it’s doing some bitwise operators to determine if the account is disabled.  You should read here to learn more about this technique.  It can also be used to determine if a user is a member of a group.  You can find more info specifically about the useraccountcontrol property here.

Tackling the date last logged in was a bit trickier.  After some digging I found an attribute in AD called lastlogontimestamp.  When inspecting it on the computer I had just logged into I found the following value: 129195723370097473.  Interesting – my first thought was that it was a Unix time, but I soon found that I was mistaken.  Of course it’s not the number of seconds since 1970, it’s the number of 100 nanoseconds since January 1st, 1601.  Duh!

The question was how could I get today’s date into this format to compare it against the number before me.  I knew I could write a function to do this, but I couldn’t believe that it was not built-in to the .net DateTime class.  Fortunately, Microsoft proved their worth with two functions to convert to and from this value:

  1. Convert a date to this number:
    (Get-Date).toFiletime()
  2. Convert the number to a DateTime:
    [Datetime]::ConvertFromFileTime(129195723370097473)

The strange thing was that I found that the time converted was not the actual last time logged in on the computer.  It winds up that this is by design.  The lastlogontime attribute only gets updated when the logon took place after a predefined threshold in AD called msDS-LogonTimeSyncInterval.  In my case this is 14 days.  If the logon happens within 14 days of what’s written to the attribute it will not update and replicate throughout AD.  This ensures that unnecessary replication does not take place.  So to account for this I could add 14 days to my 60 day limit and rewrite my filter with the following:

$logondate = (get-date).adddays(-74).tofiletime()
$searcher.Filter = "(&(objectCategory=computer)(lastlogontimestamp>=$logondate)(!(userAccountcontrol:1.2.840.113556.1.4.803:=2)))"

The final script was as follows:

$de = New-Object DirectoryServices.DirectoryEntry('LDAP://rootDSE')
$root = New-Object DirectoryServices.DirectoryEntry("LDAP://$($de.DefaultNamingContext)")
$searcher = New-Object System.DirectoryServices.DirectorySearcher($root)
$searcher.PageSize = 500
$searcher.PropertiesToLoad.AddRange(@("name"))
$searcher.Filter = "(&(objectCategory=computer)(lastlogontimestamp>=$logondate)(!(userAccountcontrol:1.2.840.113556.1.4.803:=2)))"
$comps = $searcher.findall()
$comps

Mission accomplished – until I found out that the domain I needed to run it against was a Windows 2000 domain that did not have this attribute.  Back to the drawing board.  Fortunately, I found another attribute called lastlogon in Windows 2000 that looked identical.  However, once I started using it I realized that it was impossible for it to be accurate.  After a discussion with my AD guru I learned that this attribute does not replicate.  The lastlogontimestamp was added to 2k3 in order to ensure that this data was replicated so that people could run reports like the one I was trying to run.  He told me the only thing I could do would be to query every domain controller.  This task sounded impossible – until I tried it.

If you need to do this in 2k it’s actually quite simple.  Using System.DirectoryServices.ActiveDirectory.Domain you can get all of the DCs in your domain and query each of them in turn.  I wrote the code below to do this.  Mind you it would require some error checking in production since the data must be from all DCs in order to be valid.  It also would require extra thought if you are so far from a DC that your query will timeout.  With that in mind here is the quick roundabout:

$logondate = (get-date).adddays(-30).tofiletime()
$comps = @()
([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).DomainControllers|select name | foreach {
    "Connecting to $($_.name)"
    $Root = New-Object directoryservices.directoryentry("LDAP://$($_.name)")
    $searcher = new-object System.DirectoryServices.DirectorySearcher($root)
    $searcher.Filter="(&(objectCategory=computer)(lastlogon>=$logondate)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
    $searcher.PageSize = 500
    $searcher.PropertiesToLoad.AddRange(@("name","lastlogon"))
    $searcher.findall()|foreach {
        if ($comps -notcontains $_.properties.name) {
            $comps += $_.properties.name
        }
    }
}
$comps.count

Funny – a long post over something I considered putting as a powerbit just to remind myself about the two datetime functions for working with the nanoseconds from 1601.

%d bloggers like this: