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
| Component | Value |
|---|---|
| Proxmox host | macpro2013.local |
| Proxmox console | https://macpro2013.local:8006 |
| Management workstation | Mac Mini M1 |
| VM 1 - AD + DNS | WS2022-AD-DNS - 192.168.178.210 |
| VM 2 - CA | WS2022-CA01 - 192.168.178.211 |
| Default gateway | 192.168.178.1 |
| Subnet prefix | /24 |
| Domain | LAB01.local |
| Resources (both VMs) | 2 vCPU / 4 GB RAM |
| Template | WS2022-TEMPLATE-BASE |
Build Order
The CA has a hard dependency on a working AD and DNS. Build everything in this order:
- Clone
WS2022-TEMPLATE-BASEtoWS2022-AD-DNSand configure hostname and static IP - Install AD DS + DNS and promote to Domain Controller
- Configure DNS zones, the SYSVOL scripts folder and Group Policy
- Clone
WS2022-TEMPLATE-BASEtoWS2022-CA01and join the domain - Install ADCS (Enterprise Root CA), Web Enrollment and IIS
- Configure certificate templates and CRL distribution points
- Deploy
Set-RDPCert.ps1through SYSVOL and a Scheduled Task on both servers - 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-DNS192.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:
Comments