Active Directory
The ad.alphasecunited.com forest on two Windows Server 2025 domain controllers, with a tiered OU model, Group Policy, Windows LAPS and a time chain that starts at the PDC emulator.
I run a single-domain forest, ad.alphasecunited.com, on two Windows Server 2025 Standard domain controllers. I built it on 2026-09-09 from cloned virtual machines on grey-server, one of the five nodes in the Galaxy Proxmox cluster. Five computers are members: the two controllers, the member server HQ-MGT01, the test workstation HQ-WS001, and ObiPC, a physical Windows 11 PC. The cloud side is on Microsoft cloud and the workstations are on Endpoints.
At a glance
| Item | Value |
|---|---|
| Forest and domain | ad.alphasecunited.com, NetBIOS ALPHASEC, 2016 functional level |
| Domain controllers | HQ-DC01 (VM 301) and HQ-DC02 (VM 302), both global catalogs |
| Operations masters | All five roles on HQ-DC01 |
| Site | HQ, with the identity VLAN and both client VLANs mapped to it |
| Member server | HQ-MGT01 (VM 303) in OU=Management,OU=Servers |
| Workstations | HQ-WS001 (VM 310) and ObiPC, both in OU=Standard,OU=Workstations |
| Organisational units | 33 |
| Group Policy objects | 11, including the two defaults |
Forest and domain design
I fixed the names before cloning anything, because a forest name cannot change without a rebuild. The domain is ad. under a domain I own, not a made-up suffix such as .local. I added alphasecunited.com as a second UPN suffix so synced accounts keep the same sign-in name in Microsoft 365, and enabled the AD Recycle Bin at build time. The design is deliberately small: one forest, one domain, one site and two controllers, because one controller holding every operations master role is a single point of failure.
The domain controllers
All three servers came from one Windows Server 2025 template, VM 300, prepared on 2026-09-08 with the QEMU guest agent, OpenSSH Server and sysprep already run, on OVMF firmware with a TPM 2.0 device.
| VM | Name | vCPU | Memory | Disk | Role |
|---|---|---|---|---|---|
| 301 | HQ-DC01 | 4 | 4 GiB | 80G | First controller, PDC emulator, all operations masters |
| 302 | HQ-DC02 | 4 | 4 GiB | 80G | Second controller |
| 303 | HQ-MGT01 | 2 | 6 GiB | 100G | Member server for management tooling |
All three sit on IDENTITY-A, VLAN 65, which is its own firewall zone, AlphaSec-Identity, so I can write gateway policy against the identity plane as a unit. The controllers got static addresses before promotion, because a controller that changes address afterwards leaves stale service records.
Cloning broke three things silently: every clone had the template’s SSH host keys, came up on the Public network profile, and had a blank local Administrator password with the “password not required” flag. The last one mattered most. Install-ADDSForest turns the local Administrator into the domain Administrator, so the forest would have had a passwordless built-in administrator. I caught it before promotion and set a real password from standard input, because any argument to qm guest exec lands in the Proxmox task log.
I promoted HQ-DC01 from a scheduled task running as SYSTEM rather than over SSH, since promotion restarts services an SSH session depends on. HQ-DC02 followed as a second global catalog.
Sites and DNS
A new forest starts with Default-First-Site-Name and no subnets. I renamed the site to HQ and mapped the identity VLAN and both client networks, Secure (VLAN 50) and Secure Client (VLAN 60), before any workstation joined, so each client’s first lookup already landed on the right site.
DNS runs integrated with the directory on both controllers: the forward zone at domain scope, _msdcs at forest scope, and a reverse zone for the identity VLAN at forest scope. All three are primary zones that accept secure dynamic updates only. The controllers forward everything else to the gateway, and scavenging uses a seven-day no-refresh and seven-day refresh interval so dead records age out.
Since 2026-09-09, DHCP on Secure and Secure Client hands out HQ-DC01 first and HQ-DC02 second. Those networks sit in the Internal zone and the controllers in AlphaSec-Identity, and the firewall blocks between zones by default. The existing Allow Workstations to AD policy, which selects exactly those two networks, already admitted everything a domain member needs, so the change needed no new rule.
Tiered organisational units
Tier 0 covers the forest, Tier 1 the member servers and Tier 2 the workstations. The shape mattered more than the names: administrative accounts separated by tier, servers apart from workstations, and a place for objects that are not ready yet.
Admin/{Tier 0,Tier 1,Tier 2}/{Accounts,Groups,Service Accounts}
Servers/{Management,Application}
Workstations/Standard
People/Staff
Groups/{Roles,Permissions,Applications,Distribution}
Staging/{Computers,Users}
Disabled/{Users,Computers}
| Organisational unit | What lands there |
|---|---|
Admin/Tier 0, 1, 2 | Administrative accounts, groups and service accounts by tier. The Action1 deployment account is in Tier 2/Service Accounts |
Servers/Management | HQ-MGT01. Server policies link at Servers |
Servers/Application | Application servers. None are recorded yet |
Workstations/Standard | HQ-WS001 and ObiPC. Workstation policies link at Workstations or here |
People/Staff | Staff user accounts, including my daily account |
Groups/Roles | Role groups such as the two ObiPC groups |
Groups/Applications | The two Entra Cloud Sync scope groups |
Staging/Computers, Users | The redirected default containers for new computers and users |
Disabled/Users, Computers | Disabled users and computers |
The default containers are redirected to Staging with redircmp and redirusr, so a machine that joins without an explicit OU never lands in CN=Computers, where no OU-linked policy reaches it. The people container is called People because the built-in CN=Users container already owns the name Users at the domain root, and the directory refuses a second object beside it with error 8305.
Administrative groups
Nobody is placed in Domain Admins directly. It holds only the built-in Administrator and ADM-T0-DomainAdmins, so membership is auditable in one place.
| Group | Purpose |
|---|---|
ADM-T0-DomainAdmins | Nested into Domain Admins |
ADM-T1-ServerAdmins | Local administrator on member servers, through Group Policy |
ADM-T2-WorkstationAdmins | Local administrator on workstations, through Group Policy |
APP-Action1-LocalAdmins | Only the Action1 deployment account; nested into the Tier 2 group, no Domain Admin or Tier 1 |
ROL-Staff | Role group for standard staff accounts |
APP-EntraCloudSync-Users | Users that Cloud Sync exports |
APP-EntraCloudSync-Devices | Computers that Cloud Sync exports |
ROL-ObiPC-Restricted | Principal on the ObiPC allowlist and Settings lockdown |
ROL-ObiPC-Unrestricted | Holds the allow-all AppLocker rule, so restriction lands on one group rather than the machine |
I keep a separate account per administrative tier. The Tier 0 account is in Protected Users and flagged as sensitive and not delegated, which blocks NTLM and unconstrained delegation. The built-in Administrator is the break-glass account.
Note: I made one deliberate exception. My daily directory account joined the Tier 2 group on 2026-09-11 and the Tier 0 and Tier 1 groups on 2026-09-12, so I can administer the five Windows machines and Windows Admin Center without switching accounts. The separate tier accounts still exist, and Microsoft 365, Azure, Linux, Proxmox and UniFi roles were outside that change.
Password policies
| Setting | Default Domain Policy | PSO-Admins |
|---|---|---|
| Applies to | Every account | The three ADM- groups, precedence 10 |
| Minimum length | 8 characters | 14 characters |
| Complexity | On | On |
| Maximum age | None | 365 days |
| Lockout | 10 attempts, 15 minutes | 5 attempts, 30 minutes |
The default policy also keeps a history of 24. I built it at 14 characters and lowered it to 8 on 2026-09-10, the NIST SP 800-63B floor for a user-chosen password, with complexity still on. That is the standing minimum. PSO-Admins is the only way to hold administrators to a higher bar. I built it at 20 characters and lowered it to 14 for a testing window the same day, and restoring 20 is open.
Group Policy
| GPO | Linked to | What it enforces | Why |
|---|---|---|---|
C-CMP-LAPS | Servers, Workstations | Windows LAPS to Active Directory: 20 characters, 30-day rotation, encryption, reset after use | A unique, rotating local administrator password per machine |
C-SRV-LocalAdmins | Servers | Adds ADM-T1-ServerAdmins to local Administrators | Tier 1 rights on servers only |
C-WKS-LocalAdmins | Workstations | Adds ADM-T2-WorkstationAdmins to local Administrators | A Tier 2 account never gains rights on a server |
C-WKS-OnlineLogon | Workstations | No cached domain logons, a controller required to unlock, wait for the network at startup, Hello provisioning off, alternative sign-in providers excluded | Unplugging the network is not a way around domain control |
C-WKS-ObiPC-OnlineLogon | Standard, filtered to OBIPC | The cached-logon and online-unlock settings | The earlier ObiPC-only version; it agrees with the OU-wide policy |
C-WKS-Action1-Deployer-Network | Standard | Inbound SMB, RPC endpoint mapper and service RPC on the Domain profile, from HQ-MGT01 only | Agent pushes without opening workstations to anything else |
C-WKS-ObiPC-AppControl | Standard, filtered to OBIPC | AppLocker, loopback in Merge mode, credential prompt for standard users | The application allowlist |
U-WKS-ObiPC-Restricted | Standard, filtered to ROL-ObiPC-Restricted | Settings and Control Panel allowlists, power menu, MMC, Store and registry tools removed, browser extension and download blocks | User-side restriction for one group, applied through loopback |
C-WKS-ObiPC-Lockdown | Standard, filtered to OBIPC | Recovery tools need an administrator, no sign-in screen power button, sideloading closed, workplace join blocked, 15-minute lock, AutoPlay off | Closes the recovery-menu reset path used on 2026-09-18 |
Default Domain Policy | Domain root | Password and lockout policy | Baseline for ordinary accounts |
Default Domain Controllers Policy | Domain Controllers | Controller baseline; the only GPO in the domain that assigns user rights | Created with the domain |
The ObiPC policies are covered in detail on Endpoints.
Local administrator tiering
The two local-administrator policies use Group Policy Preferences local users and groups. Each adds its tier group to the local Administrators group and leaves existing local accounts in place, so a server gets ADM-T1-ServerAdmins, a workstation gets ADM-T2-WorkstationAdmins, and neither group appears on the other kind of machine. Both halves are proven: HQ-MGT01 carries the Tier 1 group, and HQ-WS001 and ObiPC carry the Tier 2 group, each placed there by policy. Because the preference adds without stripping, ObiPC keeps the local account created at setup, which is its SSH management account.
Warning: If you write
Groups.xmldirectly instead of using the editor, you also have to register the local users and groups client-side extension ingPCMachineExtensionNamesand bump both the object’s version andgpt.ini. A preference the client does not know to process silently does nothing.
Windows LAPS
I extended the schema with Update-LapsADSchema and gave computers self-write permission on Servers, Workstations and Staging/Computers. HQ-MGT01, HQ-WS001 and ObiPC are managed. The controllers are not, which is expected: a domain controller has no local account database for LAPS to manage. When ObiPC was rebuilt on 2026-09-18, its built-in Administrator password rotated within a second of the first policy refresh. From then on the directory holds the authoritative password, and any copy in a password manager is stale.
Get-LapsADSchema does not exist, so I verify the extension by looking for the attribute:
Get-ADObject -SearchBase (Get-ADRootDSE).schemaNamingContext `
-LDAPFilter '(lDAPDisplayName=msLAPS-EncryptedPassword)'
Invoke-LapsPolicyProcessing
Get-LapsADPassword -Identity HQ-MGT01 -AsPlainText
Time source design
The PDC emulator, HQ-DC01, synchronises from an external server, and everything else follows the domain hierarchy.
| Host | Source | Stratum | Read on |
|---|---|---|---|
HQ-DC01 | time.cloudflare.com | 4 | 2026-09-09 |
HQ-DC02 | HQ-DC01 | 5 | 2026-09-09 |
HQ-MGT01 | HQ-DC02 | 6 | 2026-09-09 |
HQ-WS001 | HQ-DC02 | Not recorded | 2026-09-10 |
ObiPC | HQ-DC01 | Not recorded | 2026-09-18 |
# On the PDC emulator
w32tm /config /manualpeerlist:"time.cloudflare.com,0x8 time.windows.com,0x8" /syncfromflags:manual /reliable:yes /update
w32tm /query /source
# On a member that never synchronised
w32tm /config /syncfromflags:domhier /update
Restart-Service w32time
w32tm /resync
The first build ran on HQ-DC01’s own CMOS clock, and nothing failed loudly. The gateway’s outbound NTP allow for the identity zone sat below a catch-all block for the same zone pair, and on this firewall the order decides the outcome. I moved the allow above the block on 2026-09-09, and by 11:25 PM that night w32tm /query /source named the external server. HQ-MGT01 had never synchronised either and was corrected with /syncfromflags:domhier.
Joining machines without an administrator password
A normal Add-Computer needs a domain credential, and on Proxmox any argument to qm guest exec is written to the task log. I use an offline domain join instead. The controller provisions the computer account as SYSTEM through the guest agent and writes a single-use package, and the workstation consumes it with djoin /requestODJ and a reboot.
djoin.exe /provision /domain 'ad.alphasecunited.com' /machine 'HQ-WS001' `
/machineou 'OU=Standard,OU=Workstations,DC=ad,DC=alphasecunited,DC=com' `
/savefile 'C:\Windows\Temp\ws001odj.txt' /reuse
The package is still a machine password, so every copy is shredded afterwards. A hard power-off between applying the join and its reboot rolled the join back through Automatic Repair. /reuse also matters on a rebuild: it keeps the computer object, and hybrid join derives the tenant’s device ID from that object.
Remote administration and LDAPS
On 2026-09-12 I enabled Remote Desktop on all five domain machines, with Network Level Authentication required. Each host’s firewall admits only Trusted VLAN 10, Secure VLAN 50, the management VPN and two individual hosts, and the four identity-plane machines also needed gateway policies.
LDAP over TLS never worked on the original build: both controllers reset every handshake and logged Schannel event 36886, no suitable default server credential. On 2026-09-20 I gave each controller its own small issuing authority, issued a three-year LDAPS certificate from it, and trusted only the authority, so neither private key leaves its controller. A leaf trusted directly as a root satisfied Schannel, but GnuTLS clients refuse a trust anchor without CA=true. App Portal’s directory sign-in is the only LDAPS client so far.
Decommission and rebuild history
| Date | Event |
|---|---|
| 2026-07-27 | Retired the earlier Windows domain, both of its controllers, its test VM and the old AD-SERVERS network on VLAN 65 |
| 2026-09-06 | Retired the earlier Windows Server work to the archive. The current forest shares no state with it |
| 2026-09-07 | Reused VLAN 65 as IDENTITY-A, trunked it to grey-server and checked the identity firewall policies |
| 2026-09-08 | Prepared the Windows Server 2025 template |
| 2026-09-09 | Built the forest, OU tree, three policies and LAPS, joined HQ-MGT01, moved client DNS, fixed the NTP rule order |
| 2026-09-10 | Joined HQ-WS001, created People/Staff, installed the Entra provisioning agent and configured Cloud Sync |
| 2026-09-11 | Joined ObiPC, the first physical member, and installed Windows Admin Center |
| 2026-09-12 | Enabled RDP on all five machines, deployed the Action1 Deployer, enforced the ObiPC allowlist |
| 2026-09-18 | ObiPC wiped from the recovery menu, rebuilt, rejoined to the same object and locked down |
| 2026-09-19 | Applied C-WKS-OnlineLogon to all workstations |
| 2026-09-20 | Added LDAPS on both controllers |
| 2026-09-23 | Disabled the restricted ObiPC account |
| 2026-09-27 | Ran the ObiPC physical-access exercise |
| 2026-09-28 | Enabled the restricted account for about an hour, then disabled it again |
Verification evidence
The build ran through a shell on each server, so its evidence is command output in the Forest Build record, not screenshots.
- Replication. Over SSH,
dcdiag /test:replicationsfailed with access denied andrepadmin /syncallwith error 8440. That was the Kerberos double hop: an OpenSSH logon gets no delegatable ticket, so any check that binds to the other controller fails. In machine-account context throughqm guest exec, the same checks returned 0 of 5 failures in both directions, and a canary contact created onHQ-DC01appeared onHQ-DC02in 4 seconds. The controllers were 11 milliseconds apart. - Policy and LAPS.
HQ-MGT01,HQ-WS001andObiPCapplied the expected policies, carried their tier group in local Administrators, and hadmsLAPS-EncryptedPasswordpopulated with an expiry matching the 30-day rotation. - Online sign-in. The validation script passed on both workstations on 2026-09-19, and a network-disconnect test on
HQ-WS001kept its membership and a healthy secure channel.
Open items
- Interactive offline sign-in and unlock checks on both workstations.
PSO-Adminsback to 20 characters, with unique passwords on the administrative accounts.Credential Validationfailure auditing on both controllers, which currently leave no 4776 trail for a lockout.- BitLocker with TPM and PIN for physical workstations, with recovery keys in the directory.
- RSAT on ObiPC, because Windows Admin Center has no policy editor.
- Action1 enrollment of the controllers, which fails with access denied.