Skip to content

P03-T03 - Domain Baseline GPO

Objective

Create the first enforceable domain baseline before onboarding Windows 11 clients.

This runbook covers:

  • default domain password and lockout policy
  • baseline GPO creation and linking
  • minimal auditable registry-backed settings that are safe to seed early

Validated against the live lab on Thursday, July 16, 2026:

  • Proxmox is not the primary execution surface for GPO work.
  • The sysadmin performs GPO creation and validation directly on HQ-DC01.
  • The validated OU layout in this lab is:
  • OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me
  • OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me
  • OU=Users,OU=GNTECH,DC=corp,DC=gntech,DC=me

Inputs

Key Value
VMID 4011
Target VM HQ-DC01
Domain corp.gntech.me
Domain DN DC=corp,DC=gntech,DC=me
Enterprise OU root OU=GNTECH,DC=corp,DC=gntech,DC=me
Servers OU OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me
Workstations OU OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me
Users OU OU=Users,OU=GNTECH,DC=corp,DC=gntech,DC=me

Target State

Property Value
Password policy Defined centrally
Lockout policy Defined centrally
DC baseline GPO Created and linked
Server baseline GPO Created and linked
Workstation baseline GPO Created and linked
User baseline GPO Created and linked
Event log policy Seeded
PowerShell script block logging Seeded

Prechecks

  • Complete P03-T01 and P03-T02.
  • Confirm OU=GNTECH, OU=Servers, OU=Workstations, and OU=Users already exist in the validated OU layout.
  • In this guide, those OUs are created by .\Configure-HQ-DC01-PostPromotion.ps1 in P03-T01 step 10. Do not continue here until that step completed successfully.
  • Confirm HQ-DC01 passed the latest validation from P03-T01.

Execution

  1. Run the computer baseline GPO script locally on HQ-DC01:
.\Configure-HQ-DC01-GPOBaseline.ps1
Show full computer baseline GPO script
param(
    [string]$DomainFqdn,
    [string]$DomainDn,
    [string]$EnterpriseRootOuDn,
    [string]$DomainControllersDn,
    [string]$ServersOuDn,
    [string]$WorkstationsOuDn,
    [string]$DcBaselineGpoName,
    [string]$ServerBaselineGpoName,
    [string]$WorkstationBaselineGpoName,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
    }
}

function Resolve-Inputs {
    $script:DomainFqdn = if ([string]::IsNullOrWhiteSpace($DomainFqdn)) { Read-RequiredValue 'Domain FQDN' 'corp.gntech.me' } else { $DomainFqdn.Trim() }
    $script:DomainDn = if ([string]::IsNullOrWhiteSpace($DomainDn)) { Read-RequiredValue 'Domain DN' 'DC=corp,DC=gntech,DC=me' } else { $DomainDn.Trim() }
    $script:EnterpriseRootOuDn = if ([string]::IsNullOrWhiteSpace($EnterpriseRootOuDn)) { Read-RequiredValue 'Enterprise root OU DN' 'OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $EnterpriseRootOuDn.Trim() }
    $script:DomainControllersDn = if ([string]::IsNullOrWhiteSpace($DomainControllersDn)) { Read-RequiredValue 'Domain Controllers OU DN' 'OU=Domain Controllers,DC=corp,DC=gntech,DC=me' } else { $DomainControllersDn.Trim() }
    $script:ServersOuDn = if ([string]::IsNullOrWhiteSpace($ServersOuDn)) { Read-RequiredValue 'Servers OU DN' 'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $ServersOuDn.Trim() }
    $script:WorkstationsOuDn = if ([string]::IsNullOrWhiteSpace($WorkstationsOuDn)) { Read-RequiredValue 'Workstations OU DN' 'OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $WorkstationsOuDn.Trim() }
    $script:DcBaselineGpoName = if ([string]::IsNullOrWhiteSpace($DcBaselineGpoName)) { Read-RequiredValue 'DC baseline GPO name' 'GPO-DC-Baseline' } else { $DcBaselineGpoName.Trim() }
    $script:ServerBaselineGpoName = if ([string]::IsNullOrWhiteSpace($ServerBaselineGpoName)) { Read-RequiredValue 'Server baseline GPO name' 'GPO-Servers-Baseline' } else { $ServerBaselineGpoName.Trim() }
    $script:WorkstationBaselineGpoName = if ([string]::IsNullOrWhiteSpace($WorkstationBaselineGpoName)) { Read-RequiredValue 'Workstation baseline GPO name' 'GPO-Workstations-Baseline' } else { $WorkstationBaselineGpoName.Trim() }
}

function Ensure-Module {
    param([string]$Name)
    Import-Module $Name
}

function Ensure-Gpo {
    param(
        [string]$Name,
        [string]$Comment
    )

    $gpo = Get-GPO -Name $Name -ErrorAction SilentlyContinue
    if (-not $gpo) {
        $gpo = New-GPO -Name $Name -Comment $Comment
    }
    return $gpo
}

function Ensure-GpoLink {
    param(
        [string]$Name,
        [string]$Target
    )

    $existingLink = @((Get-GPInheritance -Target $Target).GpoLinks | Where-Object DisplayName -eq $Name)
    if ($existingLink.Count -eq 0) {
        New-GPLink -Name $Name -Target $Target -LinkEnabled Yes | Out-Null
    }
}

function Set-BaselineRegistryValue {
    param(
        [string]$GpoName,
        [string]$Key,
        [string]$ValueName,
        [string]$Type,
        [object]$Value
    )

    Set-GPRegistryValue -Name $GpoName -Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null
}

Resolve-Inputs
Ensure-Module -Name ActiveDirectory
Ensure-Module -Name GroupPolicy

$summary = [pscustomobject]@{
    DomainFqdn = $script:DomainFqdn
    DomainDn = $script:DomainDn
    EnterpriseRootOuDn = $script:EnterpriseRootOuDn
    DomainControllersDn = $script:DomainControllersDn
    ServersOuDn = $script:ServersOuDn
    WorkstationsOuDn = $script:WorkstationsOuDn
    DcBaselineGpoName = $script:DcBaselineGpoName
    ServerBaselineGpoName = $script:ServerBaselineGpoName
    WorkstationBaselineGpoName = $script:WorkstationBaselineGpoName
    ValidateOnly = [bool]$ValidateOnly
}

$summary | Format-List | Out-String | Write-Output

if ($ValidateOnly) {
    Write-Output 'Validation mode only. GPO baseline was not applied.'
    return
}

Set-ADDefaultDomainPasswordPolicy `
    -Identity $script:DomainFqdn `
    -ComplexityEnabled $true `
    -MinPasswordLength 14 `
    -PasswordHistoryCount 24 `
    -MinPasswordAge 1.00:00:00 `
    -MaxPasswordAge 90.00:00:00 `
    -LockoutThreshold 10 `
    -LockoutDuration 0.00:15:00 `
    -LockoutObservationWindow 0.00:15:00

$definitions = @(
    @{ Name = $script:DcBaselineGpoName; Comment = 'Baseline for domain controllers'; Target = $script:DomainControllersDn },
    @{ Name = $script:ServerBaselineGpoName; Comment = 'Baseline for member servers'; Target = $script:ServersOuDn },
    @{ Name = $script:WorkstationBaselineGpoName; Comment = 'Baseline for workstation endpoints'; Target = $script:WorkstationsOuDn }
)

foreach ($definition in $definitions) {
    Ensure-Gpo -Name $definition.Name -Comment $definition.Comment | Out-Null
    Ensure-GpoLink -Name $definition.Name -Target $definition.Target
}

foreach ($gpoName in @($script:DcBaselineGpoName, $script:ServerBaselineGpoName)) {
    Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Application' -ValueName 'MaxSize' -Type DWord -Value 131072
    Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Security' -ValueName 'MaxSize' -Type DWord -Value 262144
    Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\System' -ValueName 'MaxSize' -Type DWord -Value 131072
}

Set-BaselineRegistryValue -GpoName $script:WorkstationBaselineGpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Application' -ValueName 'MaxSize' -Type DWord -Value 65536
Set-BaselineRegistryValue -GpoName $script:WorkstationBaselineGpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Security' -ValueName 'MaxSize' -Type DWord -Value 131072
Set-BaselineRegistryValue -GpoName $script:WorkstationBaselineGpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\System' -ValueName 'MaxSize' -Type DWord -Value 65536

foreach ($gpoName in @($script:DcBaselineGpoName, $script:ServerBaselineGpoName, $script:WorkstationBaselineGpoName)) {
    Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' -ValueName 'EnableScriptBlockLogging' -Type DWord -Value 1
}

Write-Output 'GPO baseline completed successfully.'

Full reference:

.\Configure-HQ-DC01-UserGPOBaseline.ps1
Show full user baseline GPO script
param(
    [string]$UsersOuDn,
    [string]$UserBaselineGpoName,
    [switch]$ValidateOnly
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Read-RequiredValue {
    param(
        [string]$Prompt,
        [string]$DefaultValue
    )

    $fullPrompt = $Prompt
    if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }

    while ($true) {
        $value = Read-Host -Prompt $fullPrompt
        if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
        if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
    }
}

function Resolve-Inputs {
    $script:UsersOuDn = if ([string]::IsNullOrWhiteSpace($UsersOuDn)) { Read-RequiredValue 'Users OU DN' 'OU=Users,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $UsersOuDn.Trim() }
    $script:UserBaselineGpoName = if ([string]::IsNullOrWhiteSpace($UserBaselineGpoName)) { Read-RequiredValue 'User baseline GPO name' 'GPO-Users-Baseline' } else { $UserBaselineGpoName.Trim() }
}

function Ensure-Module {
    param([string]$Name)
    Import-Module $Name
}

function Ensure-Gpo {
    param(
        [string]$Name,
        [string]$Comment
    )

    $gpo = Get-GPO -Name $Name -ErrorAction SilentlyContinue
    if (-not $gpo) {
        $gpo = New-GPO -Name $Name -Comment $Comment
    }
    return $gpo
}

function Ensure-GpoLink {
    param(
        [string]$Name,
        [string]$Target
    )

    $existingLink = @((Get-GPInheritance -Target $Target).GpoLinks | Where-Object DisplayName -eq $Name)
    if ($existingLink.Count -eq 0) {
        New-GPLink -Name $Name -Target $Target -LinkEnabled Yes | Out-Null
    }
}

function Set-UserRegistryValue {
    param(
        [string]$Key,
        [string]$ValueName,
        [string]$Type,
        [object]$Value
    )

    Set-GPRegistryValue `
        -Name $script:UserBaselineGpoName `
        -Key $Key `
        -ValueName $ValueName `
        -Type $Type `
        -Value $Value | Out-Null
}

Resolve-Inputs
Ensure-Module -Name GroupPolicy

$summary = [pscustomobject]@{
    UsersOuDn = $script:UsersOuDn
    UserBaselineGpoName = $script:UserBaselineGpoName
    ValidateOnly = [bool]$ValidateOnly
}

$summary | Format-List | Out-String | Write-Output

if ($ValidateOnly) {
    Write-Output 'Validation mode only. User baseline GPO was not applied.'
    return
}

Ensure-Gpo -Name $script:UserBaselineGpoName -Comment 'Baseline for standard user settings' | Out-Null
Ensure-GpoLink -Name $script:UserBaselineGpoName -Target $script:UsersOuDn

# Show file extensions in Explorer.
Set-UserRegistryValue `
    -Key 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' `
    -ValueName 'HideFileExt' `
    -Type DWord `
    -Value 0

# Do not hide hidden files for standard users yet; keep baseline low-risk.

# Remove the Windows consumer "News and interests" style taskbar feed.
Set-UserRegistryValue `
    -Key 'HKCU\Software\Microsoft\Windows\CurrentVersion\Feeds' `
    -ValueName 'ShellFeedsTaskbarViewMode' `
    -Type DWord `
    -Value 2

# Block Control Panel and Settings access as a visible, user-scoped baseline.
Set-UserRegistryValue `
    -Key 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer' `
    -ValueName 'NoControlPanel' `
    -Type DWord `
    -Value 1

Write-Output 'User baseline GPO completed successfully.'

Full reference:

This baseline was validated in the lab with:

  • HideFileExt = 0
  • NoControlPanel = 1
  • ShellFeedsTaskbarViewMode = 2

Validation

  • Get-ADDefaultDomainPasswordPolicy shows the intended values.
  • GPO-DC-Baseline, GPO-Servers-Baseline, and GPO-Workstations-Baseline exist.
  • GPO-Users-Baseline exists.
  • Each baseline GPO is linked to the intended OU.
  • Event log sizing policy exists in each baseline GPO.
  • PowerShell script block logging is seeded in each baseline GPO.
  • User baseline settings exist in GPO-Users-Baseline.
  • User-scoped validation is collected from a real user session, not from an incomplete Get-GPRegistryValue call.

Evidence

  • Get-ADDefaultDomainPasswordPolicy
  • Get-GPO -All
  • Get-GPInheritance -Target "OU=Domain Controllers,DC=corp,DC=gntech,DC=me"
  • Get-GPInheritance -Target "OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me"
  • Get-GPInheritance -Target "OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me"
  • Get-GPInheritance -Target "OU=Users,OU=GNTECH,DC=corp,DC=gntech,DC=me"
  • Get-GPOReport -Name "GPO-DC-Baseline" -ReportType Html -Path C:\Temp\GPO-DC-Baseline.html
  • Get-GPOReport -Name "GPO-Servers-Baseline" -ReportType Html -Path C:\Temp\GPO-Servers-Baseline.html
  • Get-GPOReport -Name "GPO-Workstations-Baseline" -ReportType Html -Path C:\Temp\GPO-Workstations-Baseline.html
  • Get-GPOReport -Name "GPO-Users-Baseline" -ReportType Html -Path C:\Temp\GPO-Users-Baseline.html
  • Get-GPOReport -Name "GPO-Users-Baseline" -ReportType Xml -Path C:\Temp\GPO-Users-Baseline.xml
  • gpresult /h C:\Temp\gpresult-user.html from a real domain user session inside the target OU

Do not use Get-GPRegistryValue -Name "GPO-Users-Baseline" by itself as evidence. That cmdlet requires a specific -Key and does not validate the full user GPO when run without one.

Rollback

  • Do not delete a baseline GPO until you have removed its link and documented the replacement.
  • If password policy values were set incorrectly, correct them with Set-ADDefaultDomainPasswordPolicy rather than editing ad hoc in the GUI.

Sources

  • Microsoft Learn: New-GPO
  • https://learn.microsoft.com/en-us/powershell/module/grouppolicy/new-gpo?view=windowsserver2025-ps
  • Microsoft Learn: Set-GPRegistryValue
  • https://learn.microsoft.com/en-us/powershell/module/grouppolicy/set-gpregistryvalue?view=windowsserver2025-ps