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
GPOcreation 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=meOU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=meOU=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-T01andP03-T02. - Confirm
OU=GNTECH,OU=Servers,OU=Workstations, andOU=Usersalready exist in the validated OU layout. - In this guide, those OUs are created by
.\Configure-HQ-DC01-PostPromotion.ps1inP03-T01step 10. Do not continue here until that step completed successfully. - Confirm
HQ-DC01passed the latest validation fromP03-T01.
Execution
- Run the computer baseline GPO script locally on
HQ-DC01:
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:
- Script Catalog
-
Create the first user-scoped baseline by running the user GPO script locally on
HQ-DC01:
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 = 0NoControlPanel = 1ShellFeedsTaskbarViewMode = 2
Validation
Get-ADDefaultDomainPasswordPolicyshows the intended values.GPO-DC-Baseline,GPO-Servers-Baseline, andGPO-Workstations-Baselineexist.GPO-Users-Baselineexists.- 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-GPRegistryValuecall.
Evidence
Get-ADDefaultDomainPasswordPolicyGet-GPO -AllGet-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.htmlGet-GPOReport -Name "GPO-Servers-Baseline" -ReportType Html -Path C:\Temp\GPO-Servers-Baseline.htmlGet-GPOReport -Name "GPO-Workstations-Baseline" -ReportType Html -Path C:\Temp\GPO-Workstations-Baseline.htmlGet-GPOReport -Name "GPO-Users-Baseline" -ReportType Html -Path C:\Temp\GPO-Users-Baseline.htmlGet-GPOReport -Name "GPO-Users-Baseline" -ReportType Xml -Path C:\Temp\GPO-Users-Baseline.xmlgpresult /h C:\Temp\gpresult-user.htmlfrom 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-ADDefaultDomainPasswordPolicyrather 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