Download as Word document

This article describes the full build of a two-server Windows Server 2022 home lab in Proxmox VE. Together, the two virtual machines provide Active Directory Domain Services, DNS, Group Policy and a Certificate Authority (ADCS). RDP connections are secured with PKI certificates so the Mac Mini M1 management workstation connects without certificate warnings.

This is part 3 of the series on building a Windows DevOps lab in Proxmox. In part 1 I described how to create a VM, and in part 2 how to prepare the template.

Lab Environment

ComponentValue
Proxmox hostmacpro2013.local
Proxmox consolehttps://macpro2013.local:8006
Management workstationMac Mini M1
VM 1 - AD + DNSWS2022-AD-DNS - 192.168.178.210
VM 2 - CAWS2022-CA01 - 192.168.178.211
Default gateway192.168.178.1
Subnet prefix/24
DomainLAB01.local
Resources (both VMs)2 vCPU / 4 GB RAM
TemplateWS2022-TEMPLATE-BASE

Build Order

The CA has a hard dependency on a working AD and DNS. Build everything in this order:

  1. Clone WS2022-TEMPLATE-BASE to WS2022-AD-DNS and configure hostname and static IP
  2. Install AD DS + DNS and promote to Domain Controller
  3. Configure DNS zones, the SYSVOL scripts folder and Group Policy
  4. Clone WS2022-TEMPLATE-BASE to WS2022-CA01 and join the domain
  5. Install ADCS (Enterprise Root CA), Web Enrollment and IIS
  6. Configure certificate templates and CRL distribution points
  7. Deploy Set-RDPCert.ps1 through SYSVOL and a Scheduled Task on both servers
  8. Trust the Root CA certificate on the Mac Mini M1

Step 1 - Clone and Prepare WS2022-AD-DNS

Clone WS2022-TEMPLATE-BASE to WS2022-AD-DNS through the Proxmox console. Set 2 vCPU and 4 GB RAM and attach the VM to the lab bridge.

After booting, sign in as local Administrator and run the following in an elevated PowerShell session:

 1# RUN ON: WS2022-AD-DNS (192.168.178.210) - Elevated PowerShell
 2
 3# Rename the computer
 4Rename-Computer -NewName "WS2022-AD-DNS" -Force
 5
 6# Identify the active network adapter
 7$adapter = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | Select-Object -First 1
 8
 9# Remove any existing DHCP configuration
10Remove-NetIPAddress -InterfaceIndex $adapter.ifIndex -Confirm:$false -ErrorAction SilentlyContinue
11Remove-NetRoute     -InterfaceIndex $adapter.ifIndex -Confirm:$false -ErrorAction SilentlyContinue
12
13# Assign a static IP address
14New-NetIPAddress `
15    -InterfaceIndex  $adapter.ifIndex `
16    -IPAddress       192.168.178.210 `
17    -PrefixLength    24 `
18    -DefaultGateway  192.168.178.1
19
20# Point DNS to loopback - after AD DS installation point to own IP
21Set-DnsClientServerAddress `
22    -InterfaceIndex  $adapter.ifIndex `
23    -ServerAddresses 127.0.0.1
24
25# Restart to apply the hostname
26Restart-Computer -Force

Step 2 - Install AD DS and DNS

After the restart, sign in again as local Administrator and run:

 1# RUN ON: WS2022-AD-DNS (192.168.178.210) - Elevated PowerShell
 2
 3# Install AD DS and DNS with all management tools
 4Install-WindowsFeature `
 5    -Name AD-Domain-Services, DNS `
 6    -IncludeManagementTools `
 7    -IncludeAllSubFeature
 8
 9# Promote to Domain Controller and create the new forest
10Import-Module ADDSDeployment
11
12Install-ADDSForest `
13    -DomainName                    "LAB01.local" `
14    -DomainNetBiosName             "LAB01" `
15    -ForestMode                    "WinThreshold" `
16    -DomainMode                    "WinThreshold" `
17    -InstallDns                    $true `
18    -DatabasePath                  "C:\Windows\NTDS" `
19    -LogPath                       "C:\Windows\NTDS" `
20    -SysvolPath                    "C:\Windows\SYSVOL" `
21    -SafeModeAdministratorPassword (ConvertTo-SecureString "P@ssw0rd!DSRM2026" -AsPlainText -Force) `
22    -Force

The server restarts automatically. Afterwards, sign in as LAB01\Administrator.

Then update the DNS pointer and create the reverse lookup zone:

 1# RUN ON: WS2022-AD-DNS (192.168.178.210) - Elevated PowerShell
 2
 3# Update DNS pointer to own IP
 4$adapter = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | Select-Object -First 1
 5Set-DnsClientServerAddress `
 6    -InterfaceIndex  $adapter.ifIndex `
 7    -ServerAddresses 192.168.178.210, 8.8.8.8
 8
 9# Create reverse lookup zone
10Add-DnsServerPrimaryZone `
11    -NetworkID        "192.168.178.0/24" `
12    -ReplicationScope "Forest"
13
14# Add PTR record for the AD server
15Add-DnsServerResourceRecordPtr `
16    -ZoneName      "178.168.192.in-addr.arpa" `
17    -Name          "210" `
18    -PtrDomainName "WS2022-AD-DNS.LAB01.local."
19
20# Create the SYSVOL scripts folder
21$scriptShare = "\\LAB01.local\SYSVOL\LAB01.local\scripts"
22If (-Not (Test-Path $scriptShare)) {
23    New-Item -ItemType Directory -Path $scriptShare -Force
24}

Step 3 - Configure Group Policy

 1# RUN ON: WS2022-AD-DNS (192.168.178.210) - Elevated PowerShell
 2
 3Import-Module GroupPolicy
 4
 5# Create the GPO and link to the domain root
 6$gpo = New-GPO -Name "LAB01-Core-Settings"
 7New-GPLink -Name "LAB01-Core-Settings" -Target "DC=LAB01,DC=local" -LinkEnabled Yes
 8
 9# Require NLA for RDP
10Set-GPRegistryValue `
11    -Name "LAB01-Core-Settings" `
12    -Key  "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" `
13    -ValueName "UserAuthentication" -Type DWord -Value 1
14
15# Set TLS security layer
16Set-GPRegistryValue `
17    -Name "LAB01-Core-Settings" `
18    -Key  "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" `
19    -ValueName "SecurityLayer" -Type DWord -Value 2
20
21# High encryption level
22Set-GPRegistryValue `
23    -Name "LAB01-Core-Settings" `
24    -Key  "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" `
25    -ValueName "MinEncryptionLevel" -Type DWord -Value 3
26
27# Automatic certificate enrollment (computers and users)
28Set-GPRegistryValue `
29    -Name "LAB01-Core-Settings" `
30    -Key  "HKLM\SOFTWARE\Policies\Microsoft\Cryptography\AutoEnrollment" `
31    -ValueName "AEPolicy" -Type DWord -Value 7
32
33Set-GPRegistryValue `
34    -Name "LAB01-Core-Settings" `
35    -Key  "HKCU\SOFTWARE\Policies\Microsoft\Cryptography\AutoEnrollment" `
36    -ValueName "AEPolicy" -Type DWord -Value 7

Step 4 - Clone WS2022-CA01 and Join the Domain

Clone WS2022-TEMPLATE-BASE to WS2022-CA01 with 2 vCPU and 4 GB RAM on the same lab bridge. Configure the hostname and static IP on first boot:

 1# RUN ON: WS2022-CA01 (192.168.178.211) - Local VM console / Elevated PowerShell
 2
 3Rename-Computer -NewName "WS2022-CA01" -Force
 4
 5$adapter = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | Select-Object -First 1
 6Remove-NetIPAddress -InterfaceIndex $adapter.ifIndex -Confirm:$false -ErrorAction SilentlyContinue
 7Remove-NetRoute     -InterfaceIndex $adapter.ifIndex -Confirm:$false -ErrorAction SilentlyContinue
 8
 9New-NetIPAddress `
10    -InterfaceIndex  $adapter.ifIndex `
11    -IPAddress       192.168.178.211 `
12    -PrefixLength    24 `
13    -DefaultGateway  192.168.178.1
14
15# REQUIRED: point DNS to the AD server
16Set-DnsClientServerAddress `
17    -InterfaceIndex  $adapter.ifIndex `
18    -ServerAddresses 192.168.178.210
19
20Restart-Computer -Force

After the restart, join the domain:

 1# RUN ON: WS2022-CA01 (192.168.178.211) - Elevated PowerShell
 2
 3# Verify DNS resolution before joining the domain
 4Resolve-DnsName LAB01.local
 5
 6# Join the domain
 7Add-Computer `
 8    -DomainName "LAB01.local" `
 9    -Credential (Get-Credential -Message "Enter LAB01\Administrator credentials") `
10    -Restart -Force

Step 5 - Install ADCS and Configure the Certificate Authority

Sign in as LAB01\Administrator on WS2022-CA01:

 1# RUN ON: WS2022-CA01 (192.168.178.211) - Elevated PowerShell
 2
 3# Check and install IIS
 4Get-WindowsFeature Web-Server
 5Install-WindowsFeature Web-Server -IncludeManagementTools
 6
 7# Install the CA role and Web Enrollment
 8Install-WindowsFeature `
 9    -Name ADCS-Cert-Authority `
10    -IncludeManagementTools `
11    -IncludeAllSubFeature
12
13Install-WindowsFeature ADCS-Web-Enrollment -IncludeManagementTools
14
15# Verify installation
16Get-WindowsFeature ADCS-Cert-Authority, ADCS-Web-Enrollment, Web-Server | `
17    Select-Object Name, Installed, DisplayName
18
19# Configure the Enterprise Root CA
20Install-AdcsCertificationAuthority `
21    -CAType                    EnterpriseRootCA `
22    -CACommonName              "LAB01-Root-CA" `
23    -CADistinguishedNameSuffix "DC=LAB01,DC=local" `
24    -CryptoProviderName        "RSA#Microsoft Software Key Storage Provider" `
25    -KeyLength                 4096 `
26    -HashAlgorithmName         SHA256 `
27    -ValidityPeriod            Years `
28    -ValidityPeriodUnits       10 `
29    -DatabaseDirectory         "C:\Windows\system32\CertLog" `
30    -LogDirectory              "C:\Windows\system32\CertLog" `
31    -Force
32
33# Configure Web Enrollment
34Install-AdcsWebEnrollment -Force
35
36# Verify
37Get-WindowsFeature ADCS-Web-Enrollment
38Get-Service W3SVC
39netstat -an | findstr :80
40
41# Test the Web Enrollment page
42Invoke-WebRequest `
43    -Uri "http://localhost/certsrv" `
44    -UseDefaultCredentials `
45    | Select-Object StatusCode, StatusDescription

The Web Enrollment page is available at http://WS2022-CA01.LAB01.local/certsrv.


Step 6 - Secure RDP with Set-RDPCert.ps1

Create the script in SYSVOL on WS2022-AD-DNS:

1# RUN ON: WS2022-AD-DNS (192.168.178.210) - Elevated PowerShell
2
3notepad \\LAB01.local\SYSVOL\LAB01.local\scripts\Set-RDPCert.ps1

Paste the following content into Notepad, save and close it:

 1#Requires -RunAsAdministrator
 2<#
 3.SYNOPSIS
 4    Binds a CA-issued certificate to the RDP listener on every startup.
 5.DESCRIPTION
 6    Finds the most recent valid certificate in Cert:\LocalMachine\My issued by
 7    LAB01-Root-CA for the local computer's FQDN. If none is found, certificate
 8    auto-enrollment is triggered. The selected certificate thumbprint is written
 9    to the RDP-Tcp registry key and the Remote Desktop service is restarted.
10.NOTES
11    Deploy via SYSVOL and run as SYSTEM via Scheduled Task at startup.
12    Lab: LAB01.local - WS2022-AD-DNS / WS2022-CA01
13#>
14
15Set-StrictMode -Version Latest
16$ErrorActionPreference = 'Stop'
17
18$LogFile = "C:\Scripts\Set-RDPCert.log"
19
20function Write-Log {
21    param([string]$Message)
22    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
23    "$timestamp  $Message" | Tee-Object -FilePath $LogFile -Append | Write-Host
24}
25
26Write-Log "=== Set-RDPCert.ps1 started ==="
27
28# 1. Determine the computer's FQDN
29$fqdn = [System.Net.Dns]::GetHostEntry('').HostName
30Write-Log "FQDN: $fqdn"
31
32# 2. Find a valid certificate issued by LAB01-Root-CA
33$rdpCert = Get-ChildItem Cert:\LocalMachine\My |
34    Where-Object {
35        $_.Issuer    -like "*LAB01-Root-CA*"  -and
36        $_.Subject   -like "*$fqdn*"           -and
37        $_.NotAfter  -gt (Get-Date)            -and
38        $_.HasPrivateKey
39    } |
40    Sort-Object NotAfter -Descending |
41    Select-Object -First 1
42
43# 3. If no certificate is found, trigger auto-enrollment and retry
44if (-not $rdpCert) {
45    Write-Log "No valid certificate found - triggering auto-enrollment..."
46    & certutil -pulse | Out-Null
47    Start-Sleep -Seconds 30
48
49    $rdpCert = Get-ChildItem Cert:\LocalMachine\My |
50        Where-Object {
51            $_.Issuer    -like "*LAB01-Root-CA*"  -and
52            $_.Subject   -like "*$fqdn*"           -and
53            $_.NotAfter  -gt (Get-Date)            -and
54            $_.HasPrivateKey
55        } |
56        Sort-Object NotAfter -Descending |
57        Select-Object -First 1
58}
59
60if (-not $rdpCert) {
61    Write-Log "ERROR: No certificate available after auto-enrollment. Aborting."
62    exit 1
63}
64
65Write-Log "Certificate selected: $($rdpCert.Subject)"
66Write-Log "Thumbprint : $($rdpCert.Thumbprint)"
67Write-Log "Valid until: $($rdpCert.NotAfter)"
68
69# 4. Grant Network Service read access to the private key
70$keyPath = $rdpCert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
71if ($keyPath) {
72    $keyFile = Get-ChildItem "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$keyPath" `
73               -ErrorAction SilentlyContinue
74    if ($keyFile) {
75        $acl  = Get-Acl $keyFile.FullName
76        $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
77                    "NT AUTHORITY\NETWORK SERVICE", "Read", "Allow")
78        $acl.AddAccessRule($rule)
79        Set-Acl -Path $keyFile.FullName -AclObject $acl
80        Write-Log "Private key ACL updated for NETWORK SERVICE."
81    }
82}
83
84# 5. Bind the certificate to the RDP listener
85$rdpReg = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"
86Set-ItemProperty -Path $rdpReg -Name SSLCertificateSHA1Hash -Value $rdpCert.Thumbprint
87Write-Log "Registry updated with new thumbprint."
88
89# 6. Restart Remote Desktop Services to apply the new certificate
90Write-Log "Restarting TermService..."
91Restart-Service -Name TermService -Force
92Write-Log "TermService restarted."
93
94Write-Log "=== Set-RDPCert.ps1 completed successfully ==="

Then copy the script locally and register the Scheduled Task on both servers:

 1# RUN ON: BOTH SERVERS - WS2022-AD-DNS + WS2022-CA01 - Elevated PowerShell
 2
 3# Create local copy
 4If (-Not (Test-Path "C:\Scripts")) { New-Item -ItemType Directory -Path "C:\Scripts" -Force }
 5Copy-Item `
 6    -Path        "\\LAB01.local\SYSVOL\LAB01.local\scripts\Set-RDPCert.ps1" `
 7    -Destination "C:\Scripts\Set-RDPCert.ps1" `
 8    -Force
 9
10Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine -Force
11
12# Register the Scheduled Task
13$action    = New-ScheduledTaskAction `
14    -Execute "powershell.exe" `
15    -Argument "-NonInteractive -ExecutionPolicy Bypass -File C:\Scripts\Set-RDPCert.ps1"
16$trigger   = New-ScheduledTaskTrigger -AtStartup
17$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
18$settings  = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 1) `
19    -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 5) -StartWhenAvailable $true
20
21Register-ScheduledTask `
22    -TaskName "Set-RDPCertificate" -TaskPath "\LAB01\" `
23    -Action $action -Trigger $trigger -Principal $principal -Settings $settings `
24    -Description "Binds CA certificate to RDP on every startup" -Force
25
26# Run immediately for first-time installation
27Start-ScheduledTask -TaskPath "\LAB01\" -TaskName "Set-RDPCertificate"

Step 7 - Trust the Root CA Certificate on Mac Mini M1

 1# RUN ON: Mac Mini M1 - Terminal (macOS)
 2
 3# Download the Root CA certificate
 4curl -o ~/Downloads/LAB01-Root-CA.cer http://192.168.178.211/LAB01-Root-CA.cer
 5
 6# Install in the System Keychain as a trusted root
 7sudo security add-trusted-cert \
 8    -d \
 9    -r trustRoot \
10    -k /Library/Keychains/System.keychain \
11    ~/Downloads/LAB01-Root-CA.cer

Add both VMs in Microsoft Remote Desktop:

  • 192.168.178.210 - LAB01\Administrator - WS2022-AD-DNS
  • 192.168.178.211 - LAB01\Administrator - WS2022-CA01

RDP connections will no longer display certificate warnings.


Verification

 1# WS2022-AD-DNS - full DC diagnostics
 2dcdiag /test:Replications /test:DNS /test:KnowsOfRoleHolders /v
 3
 4# WS2022-CA01 - CA and Web Enrollment
 5Get-Service certsvc | Select-Object Name, Status
 6Get-WindowsFeature ADCS-Web-Enrollment
 7Get-Service W3SVC
 8netstat -an | findstr :80
 9certutil -CRL
10certutil -verifystore Root "LAB01-Root-CA"
11
12# Both servers - verify the RDP certificate
13$rdpReg = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"
14$thumb  = (Get-ItemProperty $rdpReg -Name SSLCertificateSHA1Hash).SSLCertificateSHA1Hash
15Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Thumbprint -eq $thumb } |
16    Select-Object Subject, Thumbprint, NotAfter, Issuer

Download

The complete manual, including all commands, the full Set-RDPCert.ps1 script, troubleshooting and reference material, is also available as a Word document:

WS2022-Lab-Manual-EN.docx