Skip to content

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-MGMT01 was full-cloned from 4000 / TPL-WS2025
  • NIC was moved to GEILLAN with VLAN tag 30
  • initial bootstrap was corrected from VLAN30 to VLAN20 because HQ-MGMT01 is a Windows Server management endpoint, not a workstation
  • guest bootstrap now uses 172.20.20.31/24, gateway 172.20.20.1, DNS 172.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 389 to HQ-DC01
  • TCP 445 to HQ-FS01
  • TCP 443 to github.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-WS2025 is stopped, protected, and marked as a template.
  • 4015 is unused.
  • HQ-DC01 is online and DNS/DHCP are functional.
  • svc.join.hq has validated delegation on OU=Servers.
  • CHR allows server VLAN traffic to reach HQ-DC01 and HQ-FS01.

On Proxmox:

qm config 4000
qm status 4000
qm config 4015

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.

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 4015
  • HQ-MGMT01 identity/domain output
  • network configuration output
  • site discovery output

Rollback

  • If clone/bootstrap fails before domain join, stop and remove only 4015, then redeploy from 4000.
  • 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-MGMT01 to compensate for a failed server deployment.