P03-T10 - HQ-MGMT01 Management Server
Objective
Deploy HQ-MGMT01 as the first dedicated Windows administration endpoint using Windows Server 2025 Desktop Experience.
HQ-MGMT01 is a management server/workstation. It is not a domain controller, DHCP server, DNS server, or file server.
Design decision
The lab uses Windows Server for HQ-MGMT01 because the administrator will connect to a dedicated, domain-joined server remotely for administration.
This avoids the known 4001 / TPL-W11E template issue and uses the validated 4000 / TPL-WS2025 source image instead.
Validated lab status
Validated on Sunday, July 19, 2026:
4015 / HQ-MGMT01was full-cloned from4000 / TPL-WS2025- NIC was moved to
GEILLANwith VLAN tag30 - initial bootstrap was corrected from
VLAN30toVLAN20becauseHQ-MGMT01is a Windows Server management endpoint, not a workstation - guest bootstrap now uses
172.20.20.31/24, gateway172.20.20.1, DNS172.20.20.11, and RDP - QGA validation from inside the guest confirmed:
- temporary Sysprep hostname
WIN-OH3S39A5JR9 WORKGROUP, not yet domain joined- DNS resolution for
hq-dc01.corp.gntech.me - TCP
389toHQ-DC01 - TCP
445toHQ-FS01 - TCP
443togithub.com
The remaining execution steps must be completed interactively inside Windows because the domain join credential must be entered securely by the sysadmin.
The validation VM 4015 was later removed so the sysadmin can redeploy it manually from this runbook.
Inputs
| Key | Value |
|---|---|
| Source template | 4000 / TPL-WS2025 |
| VMID | 4015 |
| Name | HQ-MGMT01 |
| VLAN | 20 |
| DNS | 172.20.20.11 |
| Gateway | 172.20.20.1 |
| Domain | corp.gntech.me |
| OU | OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me |
| Join account | corp.gntech.me\svc.join.hq |
Prechecks
4000 / TPL-WS2025is stopped, protected, and marked as a template.4015is unused.HQ-DC01is online and DNS/DHCP are functional.svc.join.hqhas validated delegation onOU=Servers.- CHR allows server VLAN traffic to reach
HQ-DC01andHQ-FS01.
On Proxmox:
qm config 4015 should fail before deployment because the VMID must be unused.
Step 1 - Clone the Management Server
Run on Proxmox:
qm clone 4000 4015 --name HQ-MGMT01 --full 1 --storage local-zfs
qm set 4015 --cores 2 --memory 4096 --balloon 0 --net0 virtio,bridge=GEILLAN,tag=20,firewall=0
qm start 4015
Step 2 - Bootstrap RDP Through QGA
This is a transport-only bootstrap so the sysadmin can connect and continue inside Windows. It does not perform domain join or application configuration.
Run on Proxmox after QGA responds:
VMID=4015
IPV4_ADDRESS='172.20.20.31'
PREFIX_LENGTH='24'
GATEWAY='172.20.20.1'
DNS1='172.20.20.11'
qm guest exec "$VMID" -- powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "
\$adapter = Get-NetAdapter | Where-Object Status -eq 'Up' | Sort-Object ifIndex | Select-Object -First 1;
if (-not \$adapter) { throw 'No active network adapter was found.' }
Get-NetIPAddress -InterfaceIndex \$adapter.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object { \$_.IPAddress -notlike '169.254.*' } |
ForEach-Object {
Remove-NetIPAddress -InterfaceIndex \$adapter.ifIndex -AddressFamily IPv4 -IPAddress \$_.IPAddress -Confirm:\$false -ErrorAction SilentlyContinue
};
Get-NetRoute -InterfaceIndex \$adapter.ifIndex -DestinationPrefix '0.0.0.0/0' -AddressFamily IPv4 -ErrorAction SilentlyContinue |
ForEach-Object {
Remove-NetRoute -InterfaceIndex \$adapter.ifIndex -DestinationPrefix '0.0.0.0/0' -NextHop \$_.NextHop -Confirm:\$false -ErrorAction SilentlyContinue
};
New-NetIPAddress -InterfaceIndex \$adapter.ifIndex -IPAddress '$IPV4_ADDRESS' -PrefixLength $PREFIX_LENGTH -DefaultGateway '$GATEWAY' | Out-Null;
Set-DnsClientServerAddress -InterfaceIndex \$adapter.ifIndex -ServerAddresses '$DNS1';
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name 'fDenyTSConnections' -Value 0;
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' | Out-Null;
"
Step 3 - Create the In-Guest Deployment Script
Inside HQ-MGMT01, open elevated PowerShell, create a working folder, then create the deployment script from this page.
New-Item -Path 'C:\GNTech\Scripts' -ItemType Directory -Force | Out-Null
Set-Location 'C:\GNTech\Scripts'
notepad.exe .\Deploy-HQ-MGMT01-Stage1.ps1
Paste the full script below into Notepad and save it as C:\GNTech\Scripts\Deploy-HQ-MGMT01-Stage1.ps1.
param(
[string]$TargetName,
[string]$DomainName,
[string]$OuPath,
[string]$JoinCredentialUser,
[string]$ExpectedDnsServer,
[string]$ExpectedGateway,
[switch]$EnableRdp,
[switch]$SkipDomainJoin,
[switch]$RestartIfNeeded
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Assert-Elevated {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
}
}
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:TargetName = if ([string]::IsNullOrWhiteSpace($TargetName)) { Read-RequiredValue 'Target hostname' 'HQ-MGMT01' } else { $TargetName.Trim() }
$script:DomainName = if ([string]::IsNullOrWhiteSpace($DomainName)) { Read-RequiredValue 'Domain FQDN' 'corp.gntech.me' } else { $DomainName.Trim() }
$script:ExpectedDnsServer = if ([string]::IsNullOrWhiteSpace($ExpectedDnsServer)) { Read-RequiredValue 'Expected DNS server IPv4' '172.20.20.11' } else { $ExpectedDnsServer.Trim() }
$script:ExpectedGateway = if ([string]::IsNullOrWhiteSpace($ExpectedGateway)) { Read-RequiredValue 'Expected default gateway IPv4' '172.20.20.1' } else { $ExpectedGateway.Trim() }
}
function Get-PrimaryAdapter {
$adapter = Get-NetAdapter |
Where-Object Status -eq 'Up' |
Sort-Object ifIndex |
Select-Object -First 1
if (-not $adapter) { throw 'No active network adapter was found.' }
return $adapter
}
function Test-FirstBootState {
$setupState = Get-ItemProperty 'HKLM:\SYSTEM\Setup'
$imageState = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\State'
if ($setupState.OOBEInProgress -ne 0) { throw 'OOBE is still in progress. Stop before joining the domain.' }
if ($setupState.SystemSetupInProgress -ne 0) { throw 'System setup is still in progress. Stop before joining the domain.' }
if ($imageState.ImageState -ne 'IMAGE_STATE_COMPLETE') { throw "Unexpected image state '$($imageState.ImageState)'." }
}
function Test-NetworkReadiness {
param([Microsoft.Management.Infrastructure.CimInstance]$Adapter)
$ipConfig = Get-NetIPConfiguration -InterfaceIndex $Adapter.ifIndex
$ipv4Address = $ipConfig.IPv4Address | Select-Object -First 1
$gateways = @($ipConfig.IPv4DefaultGateway | ForEach-Object NextHop | Where-Object { $_ })
$dnsServers = (Get-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -AddressFamily IPv4).ServerAddresses
if (-not $ipv4Address) { throw 'No IPv4 address is assigned on the active adapter.' }
if ($ExpectedGateway -notin $gateways) { throw "Expected gateway $ExpectedGateway was not found." }
if ($ExpectedDnsServer -notin $dnsServers) { throw "Expected DNS server $ExpectedDnsServer was not found." }
Resolve-DnsName $script:DomainName -ErrorAction Stop | Out-Null
Write-Output "ActiveAdapter: $($Adapter.Name)"
Write-Output "IPv4: $($ipv4Address.IPAddress)"
Write-Output "Gateway: $($gateways -join ', ')"
Write-Output "DnsServers: $($dnsServers -join ', ')"
}
function Enable-RemoteDesktopAccess {
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name 'fDenyTSConnections' -Value 0
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' | Out-Null
Write-Output 'RemoteDesktop: Enabled'
}
function Join-ManagementServer {
if ($SkipDomainJoin) {
return [pscustomobject]@{
RestartRequired = $false
Message = 'DomainJoin: Skipped'
}
}
$computerSystem = Get-CimInstance Win32_ComputerSystem
$renameRequired = $env:COMPUTERNAME -ne $script:TargetName
$joinRequired = -not $computerSystem.PartOfDomain
if (-not $renameRequired -and -not $joinRequired) {
return [pscustomobject]@{
RestartRequired = $false
Message = 'No rename or domain join action is required.'
}
}
$script:OuPath = if ([string]::IsNullOrWhiteSpace($OuPath)) { Read-RequiredValue 'Target server OU DN' 'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $OuPath.Trim() }
$script:JoinCredentialUser = if ([string]::IsNullOrWhiteSpace($JoinCredentialUser)) { Read-RequiredValue 'Domain join user' 'corp.gntech.me\svc.join.hq' } else { $JoinCredentialUser.Trim() }
$credential = Get-Credential -UserName $script:JoinCredentialUser -Message 'Enter domain join credential for HQ-MGMT01'
if ($renameRequired) {
Add-Computer -DomainName $script:DomainName -Credential $credential -OUPath $script:OuPath -NewName $script:TargetName -Force
return [pscustomobject]@{
RestartRequired = $true
Message = 'The management server was renamed and joined to the domain.'
}
}
Add-Computer -DomainName $script:DomainName -Credential $credential -OUPath $script:OuPath -Force
return [pscustomobject]@{
RestartRequired = $true
Message = 'The management server was joined to the domain.'
}
}
Resolve-Inputs
Assert-Elevated
Test-FirstBootState
$adapter = Get-PrimaryAdapter
Test-NetworkReadiness -Adapter $adapter
if ($EnableRdp) {
Enable-RemoteDesktopAccess
}
$joinResult = Join-ManagementServer
$restartRequired = [bool]$joinResult.RestartRequired
$computerSystem = Get-CimInstance Win32_ComputerSystem
Write-Output "TargetName: $($script:TargetName)"
Write-Output "DomainName: $($script:DomainName)"
Write-Output "CurrentComputerName: $($computerSystem.Name)"
Write-Output "CurrentDomain: $($computerSystem.Domain)"
Write-Output "PartOfDomain: $($computerSystem.PartOfDomain)"
if (-not $SkipDomainJoin) {
Write-Output "OuPath: $($script:OuPath)"
}
Write-Output $joinResult.Message
Write-Output "RestartRequired: $restartRequired"
Write-Output 'ManagementServerStage1: Complete'
if ($restartRequired -and $RestartIfNeeded) {
Restart-Computer -Force
} elseif ($restartRequired) {
Write-Output 'A restart is required to complete HQ-MGMT01 domain onboarding.'
}
Step 4 - Rename and Join the Domain
Run from elevated PowerShell inside HQ-MGMT01:
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force
Set-Location 'C:\GNTech\Scripts'
.\Deploy-HQ-MGMT01-Stage1.ps1 `
-TargetName 'HQ-MGMT01' `
-DomainName 'corp.gntech.me' `
-OuPath 'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me' `
-JoinCredentialUser 'corp.gntech.me\svc.join.hq' `
-ExpectedDnsServer '172.20.20.11' `
-ExpectedGateway '172.20.20.1' `
-EnableRdp `
-RestartIfNeeded
The script securely prompts for the domain join credential. It does not store passwords.
Do not add -SkipDomainJoin in this step. This is the only step that should prompt for the join credential.
Validation
On HQ-MGMT01 after restart:
Get-CimInstance Win32_ComputerSystem | Select-Object Name,Domain,PartOfDomain
Get-NetIPConfiguration
nltest /dsgetsite
Test-NetConnection hq-dc01.corp.gntech.me -Port 5985
Test-NetConnection hq-fs01.corp.gntech.me -Port 445
Evidence
qm config 4015HQ-MGMT01identity/domain output- network configuration output
- site discovery output
Rollback
- If clone/bootstrap fails before domain join, stop and remove only
4015, then redeploy from4000. - If domain join partially succeeds, remove the failed computer object from AD before retrying with a clean clone.
- Do not install infrastructure roles on
HQ-MGMT01to compensate for a failed server deployment.