Lability tutorial

A tutorial for working with Lability

View project on GitHub

Chapter 7: AD domain

WARNING: THIS IS AN UNFINISHED CHAPTER, AND THE WINDOWS EVENT FORWARDING STUFF DOES NOT YET WORK. CURRENTLY, THIS CHAPTER MAY FAIL TO DEPLOY ENTIRELY.

Starting with the network from Chapter 6, add a domain controller.

Last tested: NEVER

On this page

Adding a domain controller

Our domain controller lets us scale the lab more easily. Now that we have one, we can use it to configure user accounts and assign DHCP leases, so adding a new machine to the network is very easy.

Creating AD user accounts

Creating the accounts themselves are very easy.

One potentially surprising behavior in the configuration as we’ve written it here is that it uses the local admin password for the password of the user1 user also. Of course, you could pass in a different password instead, but for a disposable lab like this one, using the same password is unlikely to cause any security problems.

Once the account is created, it can be used to log on to any machine in the domain, and since the user1 account is a member of both Domain Admins and Enterprise Admins, that account will have administrative privileges on all VMs in the domain and over the domain itself.

Use DHCP for clients, and static IP addresses for servers

You may notice that we wrap the networking configuration in an if statement:

node $AllNodes.Where({$_.Role -NotIn 'EDGE'}).NodeName {
    if (-not [System.String]::IsNullOrEmpty($node.IPAddress)) {
        xIPAddress 'PrimaryIPAddress' {
            # ... snip ...

This lets us apply that node block to all machines on the network except the gateway, but only configure manual networking if a static IP address was defined in the configuration data. (If networking is not configured manually, Windows will use DHCP to try to obtain a configuration.)

Windows event forwarding

Now that we have a domain, we can easily enable Windows event forwarding. This can be very helpful when debugging problems with labs consisting of multiple VMs, because (assuming the event forwarding configuration gets applied) you should only need to log on to the VM where the events are being forwarded in order to see the logs from any other VM.

If you are in the target audience for this tutorial, you probably know that there are dozens of logging solutions available. We choose WEF in this chapter because it is agentless and supported out of the box. In fact, it’s supported all the way back to Windows XP SP2 / Server 2003 SP1.

TODO: Finish this section

Adding event source subscriptions

See the Query setting in the xWEFSubscription DSC resource in our configuration. By default, that looks like this:

Query = @(
    'Application:*'
    'System:*'
    'Microsoft-Windows-Desired State Configuration-Admin:*'
    'Microsoft-Windows-Desired State Configuration-Operational:*'
)

These go in to the Windows event subscription XML something like:

<Select Path="Application">*</Select>
<Select Path="System">*</Select>
<Select Path="Microsoft-Windows-Desired State Configuration-Admin">*</Select>
<Select Path="Microsoft-Windows-Desired State Configuration-Operational">*</Select>

One thing that may not be obvious is that events can actually be filtered from these sources - for instance, by replacing the * with *[System[EventId=2]], you can ignore all events with an EventId other than 2.

It may be useful to see examples from other organizations.

Configuring servers to push their events to the collector

TODO: Write this section

See also

Resource ordering

As we have discussed in Chapter 3, when writing the DSC configuration, you can minimize confusing errors by paying careful attention to ordering.

This chapter gives us a good place to illustrate this. Our Configure.ADLAB.ps1 script has these sections:

TODO: don’t forget to update when I add WEF

  1. node $AllNodes.Where({$true}).NodeName { ... }: LCM setup and ICMP ECHO firewall rules
  2. node $AllNodes.Where({$_.Role -in 'EDGE'}).NodeName { ... }: Networking for the gateway server
  3. node $AllNodes.Where({$_.Role -NotIn 'EDGE'}).NodeName { ... }: Networking for all other servers
  4. node $AllNodes.Where({$_.Role -in 'DC'}).NodeName { ... }: Creating the AD domain on the domain controller
  5. node $AllNodes.Where({$_.Role -NotIn 'DC'}).NodeName { ... }: Joining all other servers to the AD domain
  6. node $Allnodes.Where({'Firefox' -in $_.Lability_Resource}).NodeName { ... }: Installing Firefox

Note how the new, complicated functionality of creating and joining the AD domain is not configured until basic networking is configured. Keeping networking as early in the configuration as possible, and certainly before new, untested functionality, ensures you will be able to log in via PS Remoting if something were to go wrong with the new functionality in the configuration.

Lab exercises and files

  1. Add more users via active directory

  2. Change the CORPNET Hyper-V switch to “internal” instead of “private”.

    Redeploy, then see if you notice any network problems from your host. What problems are you seeing? Why are they manifesting?

    (Once finished, delete the CORPNET internal Hyper-V switch, and any problems you were seeing should dissipate.)

  3. Log on to the domain controller and view Windows events forwarded from the other machines.

  4. Collect more logs, perhaps WinRM logs from Applications and Services Logs\Microsoft\Windows\Windows Remote Management, then redeploy the lab, log on to the domain controller, and view the newly forwarded events.

  5. Advanced/bonus exercise: Follow the Microsoft Advanced Threat Analytics deploy instructions to deploy MS ATA to your lab using the GUI. MS ATA uses Windows Event Forwarding and is a good real-world use case for this functionality.

    Follow-up bonus: automate the installation by adding an ATA Lability resource and install ATA using Powershell DSC.

ConfigurationData.ADLAB.psd1

@{
    AllNodes = @(
        @{
            NodeName                    = '*';
            InterfaceAlias              = 'Ethernet';
            AddressFamily               = 'IPv4';
            DnsConnectionSuffix         = 'adlab.invalid';
            DnsServerAddress            = '10.0.0.1';
            DefaultGateway              = '10.0.0.2';
            DomainName                  = 'adlab.invalid';
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser        = $true; # Removes 'It is not recommended to use domain credential for node X' messages
            Lability_SwitchName         = 'ADLAB-CORPNET';
            Lability_ProcessorCount     = 1;
            Lability_StartupMemory      = 2GB;
            Lability_Media              = "2016_x64_Standard_EN_Eval";
            Lability_Timezone           = "Central Standard Time";
        }
        @{
            NodeName                = 'ADLAB-DC1';
            IPAddress               = '10.0.0.1/24';
            DnsServerAddress        = '127.0.0.1';
            Role                    = 'DC';
        }
        @{
            NodeName                     = 'ADLAB-EDGE1';
            Role                         = 'EDGE'
            IPAddress                    = '10.0.0.2/24';
            Lability_MACAddress         = @('00-15-5d-cf-01-01', '00-15-5d-cf-01-02')
            Lability_SwitchName         = @('Wifi-HyperV-VSwitch', 'ADLAB-CORPNET')
            InterfaceAlias              = @('Public', 'ADLAB-CORPNET')
        }
        @{
            NodeName                    = 'ADLAB-CLIENT1';
            Role                        = 'CLIENT';
            Lability_Media              = 'WIN10_x64_Enterprise_EN_Eval';
            Lability_Resource           = @(
                'Firefox'
            )
            PSDscAllowPlainTextPassword = $true;
        }
    )
    NonNodeData = @{
        Lability = @{
            Media = @()
            Network = @(
                # Use a *private* switch, not an internal one,
                # so that our Hyper-V host doesn't get a NIC w/ DHCP lease on the corporate network,
                # which can cause networking problems on the host.
                @{ Name = 'ADLAB-CORPNET'; Type = 'Private'; }

                # The Wifi-HyperV-VSwitch is already defined on my machine - do not manage it here
                # If that switch does not exist on your machine, you should define an External switch and set its name here
                # @{ Name = 'Wifi-HyperV-VSwitch'; Type = 'External'; NetAdapterName = 'WiFi'; AllowManagementOS = $true; }
            )

            DSCResource = @(
                @{ Name = 'xActiveDirectory'; RequiredVersion = '2.17.0.0'; }
                @{ Name = 'xComputerManagement'; RequiredVersion = '4.1.0.0'; }
                @{ Name = 'xDhcpServer'; RequiredVersion = '1.6.0.0'; }
                @{ Name = 'xDnsServer'; RequiredVersion = '1.7.0.0'; }
                @{ Name = 'xNetworking'; RequiredVersion = '5.7.0.0'; }
                @{ Name = 'xSmbShare'; RequiredVersion = '2.0.0.0'; }
                @{ Name = 'xWindowsEventForwarding'; RequiredVersion = '1.0.0.0'; }
            )

            Resource = @(
                @{
                    Id = 'Firefox'
                    Filename = 'Firefox-Latest.exe'
                    Uri = 'https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=en-US'
                }
            )
        }
    }
}

Configure.ADLAB.ps1

Configuration AdLabConfig {
    param (
        [Parameter()] [ValidateNotNull()] [PSCredential] $Credential = (Get-Credential -Credential 'Administrator')
    )
    Import-DscResource -Module PSDesiredStateConfiguration

    Import-DscResource -Module xActiveDirectory -ModuleVersion 2.17.0.0
    Import-DscResource -Module xComputerManagement -ModuleVersion 4.1.0.0
    Import-DscResource -Module xDHCPServer -ModuleVersion 1.6.0.0
    Import-DscResource -Module xDnsServer -ModuleVersion 1.7.0.0
    Import-DscResource -Module xNetworking -ModuleVersion 5.7.0.0
    Import-DscResource -Module xSmbShare -ModuleVersion 2.0.0.0
    Import-DscResource -Module xWindowsEventForwarding -ModuleVersion 1.0.0.0

    # Common configuration for all nodes
    node $AllNodes.Where({$true}).NodeName {

        LocalConfigurationManager {
            RebootNodeIfNeeded   = $true;
            AllowModuleOverwrite = $true;
            ConfigurationMode    = 'ApplyOnly';
            CertificateID        = $node.Thumbprint;
        }

        xFirewall 'FPS-ICMP4-ERQ-In' {
            Name        = 'FPS-ICMP4-ERQ-In';
            DisplayName = 'File and Printer Sharing (Echo Request - ICMPv4-In)';
            Description = 'Echo request messages are sent as ping requests to other nodes.';
            Direction   = 'Inbound';
            Action      = 'Allow';
            Enabled     = 'True';
            Profile     = 'Any';
        }

        xFirewall 'FPS-ICMP6-ERQ-In' {
            Name        = 'FPS-ICMP6-ERQ-In';
            DisplayName = 'File and Printer Sharing (Echo Request - ICMPv6-In)';
            Description = 'Echo request messages are sent as ping requests to other nodes.';
            Direction   = 'Inbound';
            Action      = 'Allow';
            Enabled     = 'True';
            Profile     = 'Any';
        }

    }

    node $AllNodes.Where({$_.Role -in 'EDGE'}).NodeName {

        xNetAdapterName "RenamePublicAdapter" {
            NewName     = $node.InterfaceAlias[0];
            MacAddress  = $node.Lability_MACAddress[0];
        }
        # Do not specify an IP address for the public adapter so that it gets one via DHCP

        xNetAdapterName "RenameCorpnetAdapter" {
            NewName     = $node.InterfaceAlias[1];
            MacAddress  = $node.Lability_MACAddress[1];
        }

        xIPAddress 'CorpnetIPAddress' {
            IPAddress      = $node.IPAddress;
            InterfaceAlias = $node.InterfaceAlias[1];
            AddressFamily  = $node.AddressFamily;
            DependsOn      = '[xNetAdapterName]RenameCorpnetAdapter';
        }

        xDnsServerAddress 'CorpnetDNSClient' {
            Address        = $node.DnsServerAddress;
            InterfaceAlias = $node.InterfaceAlias[1];
            AddressFamily  = $node.AddressFamily;
            DependsOn      = '[xIPAddress]CorpnetIPAddress';
        }

        xDnsConnectionSuffix 'CorpnetConnectionSuffix' {
            InterfaceAlias           = $node.InterfaceAlias[1];
            ConnectionSpecificSuffix = $node.DnsConnectionSuffix;
            DependsOn                = '[xIPAddress]CorpnetIPAddress';
        }

        Script "NewNetNat" {
            GetScript = { return @{ Result = "" } }
            TestScript = {
                try {
                    Get-NetNat -Name NATNetwork -ErrorAction Stop | Out-Null
                    return $true
                } catch {
                    return $false
                }
            }
            SetScript = {
                New-NetNat -Name NATNetwork -InternalIPInterfaceAddressPrefix "10.0.0.0/24"
            }
            PsDscRunAsCredential = $Credential
            DependsOn = '[xIPAddress]CorpnetIPAddress';
        }
    }

    node $AllNodes.Where({$_.Role -NotIn 'EDGE'}).NodeName {

        if (-not [System.String]::IsNullOrEmpty($node.IPAddress)) {
            xIPAddress 'PrimaryIPAddress' {
                IPAddress      = $node.IPAddress
                InterfaceAlias = $node.InterfaceAlias
                AddressFamily  = $node.AddressFamily
            }

            if (-not [System.String]::IsNullOrEmpty($node.DnsServerAddress)) {
                xDnsServerAddress 'PrimaryDNSClient' {
                    Address        = $node.DnsServerAddress;
                    InterfaceAlias = $node.InterfaceAlias;
                    AddressFamily  = $node.AddressFamily;
                    DependsOn      = '[xIPAddress]PrimaryIPAddress';
                }
            }

            if (-not [System.String]::IsNullOrEmpty($node.DnsConnectionSuffix)) {
                xDnsConnectionSuffix 'PrimaryConnectionSuffix' {
                    InterfaceAlias           = $node.InterfaceAlias;
                    ConnectionSpecificSuffix = $node.DnsConnectionSuffix;
                    DependsOn                = '[xIPAddress]PrimaryIPAddress';
                }
            }

            # Do not set the default gateway for the EDGE server to avoid errors like
            # 'New-NetRoute : Instance MSFT_NetRoute already exists'
            # When this configuration was part of the .Where({$true}) stanza above,
            # I got those errors on EDGE all the time.
            xDefaultGatewayAddress 'NonEdgePrimaryDefaultGateway' {
                InterfaceAlias = $node.InterfaceAlias;
                Address        = $node.DefaultGateway;
                AddressFamily  = $node.AddressFamily;
                DependsOn      = '[xIPAddress]PrimaryIPAddress';
            }

        } #end if IPAddress

    }

    # Configure the AD domain
    node $AllNodes.Where({$_.Role -in 'DC'}).NodeName {

        xComputer 'Hostname' {
            Name = $node.NodeName;
        }

        ## Hack to fix DependsOn with hyphens "bug" :(
        foreach ($feature in @(
                'AD-Domain-Services',
                'GPMC',
                'RSAT-AD-Tools',
                'DHCP',
                'RSAT-DHCP'
            )) {
            WindowsFeature $feature.Replace('-','') {
                Ensure               = 'Present';
                Name                 = $feature;
                IncludeAllSubFeature = $true;
            }
        }

        xADDomain 'ADDomain' {
            DomainName                    = $node.DomainName;
            SafemodeAdministratorPassword = $Credential;
            DomainAdministratorCredential = $Credential;
            DependsOn                     = '[WindowsFeature]ADDomainServices';
        }

        xDhcpServerAuthorization 'DhcpServerAuthorization' {
            Ensure    = 'Present';
            DependsOn = '[WindowsFeature]DHCP','[xADDomain]ADDomain';
        }

        xDhcpServerScope 'DhcpScope10_0_0_0' {
            Name          = 'Corpnet';
            IPStartRange  = '10.0.0.100';
            IPEndRange    = '10.0.0.200';
            SubnetMask    = '255.255.255.0';
            LeaseDuration = '00:08:00';
            State         = 'Active';
            AddressFamily = 'IPv4';
            DependsOn     = '[WindowsFeature]DHCP';
        }

        xDhcpServerOption 'DhcpScope10_0_0_0_Option' {
            ScopeID            = '10.0.0.0';
            DnsDomain          = 'corp.contoso.com';
            DnsServerIPAddress = '10.0.0.1';
            Router             = '10.0.0.2';
            AddressFamily      = 'IPv4';
            DependsOn          = '[xDhcpServerScope]DhcpScope10_0_0_0';
        }

        xADUser User1 {
            DomainName  = $node.DomainName;
            UserName    = 'User1';
            Description = 'Lability Test Lab user';
            Password    = $Credential;
            Ensure      = 'Present';
            DependsOn   = '[xADDomain]ADDomain';
        }

        xADGroup DomainAdmins {
            GroupName        = 'Domain Admins';
            MembersToInclude = 'User1';
            DependsOn        = '[xADUser]User1';
        }

        xADGroup EnterpriseAdmins {
            GroupName        = 'Enterprise Admins';
            GroupScope       = 'Universal';
            MembersToInclude = 'User1';
            DependsOn        = '[xADUser]User1';
        }

    }

    node $AllNodes.Where({$_.Role -NotIn 'DC'}).NodeName {
        # Use user@domain for the domain joining credential
        $upn = "$($Credential.UserName)@$($node.DomainName)"
        $domainCred = New-Object -TypeName PSCredential -ArgumentList ($upn, $Credential.Password);
        xComputer 'DomainMembership' {
            Name       = $node.NodeName;
            DomainName = $node.DomainName;
            Credential = $domainCred
        }
    }

    # Configure Windows Event Forwarding on all source machines
    node $AllNodes.Where({$_.Role -NotIn 'DC'}).NodeName {

        # 1. Get the computer account for the WEF Collector
        # 2. Add that account to the local "Event Log Readers" group on each other server

    }

    # Configure Windows Event Forwarding
    node $AllNodes.Where({$_.Role -in 'DC'}).NodeName {
        xWEFCollector "CreateWefCollector" {
            Ensure = "Present"
            Name = "UniqueIgnoredNameLolWhatever"
        }

        xWEFSubscription "WebSubscription" {
            SubscriptionId = "AdLabSub"
            Ensure = "Present"
            SubscriptionType = "CollectorInitiated"
            DeliveryMode = "Push"
            ReadExistingEvents = $true

            # Create a list of FQDNs like 'dc1.adlab.invalid'
            Address = $configData.AllNodes.Where({$_.NodeName -ne '*'}).NodeName | Foreach-Object -Proces { "$_.$node.DomainName" }

            # Which event logs to request to be forwarded
            Query = @(
                'Application:*'
                'System:*'
                'Microsoft-Windows-Desired State Configuration-Admin:*'
                'Microsoft-Windows-Desired State Configuration-Operational:*'
            )

            DependsOn = "[xWEFCollector]CreateWefCollector"
        }
    }


    node $Allnodes.Where({'Firefox' -in $_.Lability_Resource}).NodeName {
        Script "InstallFirefox" {
            GetScript = { return @{ Result = "" } }
            TestScript = {
                Test-Path -Path "C:\Program Files\Mozilla Firefox"
            }
            SetScript = {
                $process = Start-Process -FilePath "C:\Resources\Firefox-Latest.exe" -Wait -PassThru -ArgumentList @('-ms')
                if ($process.ExitCode -ne 0) {
                    throw "Firefox installer at $ffInstaller exited with code $($process.ExitCode)"
                }
            }
            PsDscRunAsCredential = $Credential
        }
    }

} #end Configuration Example

Deploy-ADLAB.ps1

[CmdletBinding()] Param(
    [SecureString] $AdminPassword = (Read-Host -AsSecureString -Prompt "Admin password"),
    [string] $ConfigurationData = (Join-Path -Path $PSScriptRoot -ChildPath ConfigurationData.ADLAB.psd1),
    [string] $ConfigureScript = (Join-Path -Path $PSScriptRoot -ChildPath Configure.ADLAB.ps1),
    [string] $DscConfigName = "AdLabConfig",
    [switch] $IgnorePendingReboot
)

$ErrorActionPreference = "Stop"

. $ConfigureScript
& $DscConfigName -ConfigurationData $ConfigurationData -OutputPath $env:LabilityConfigurationPath -Verbose
Start-LabConfiguration -ConfigurationData $ConfigurationData -Path $env:LabilityConfigurationPath -Verbose -Password $AdminPassword -IgnorePendingReboot:$IgnorePendingReboot
Start-Lab -ConfigurationData $ConfigurationData -Verbose