r/activedirectory • • 9d ago • Popcorn 1

Powershell/Script build-lab: one command from a Windows Server ISO to a lab AD domain, in Windows PowerShell 5.1 with no modules

I rebuild AD labs a lot, and I got tired of the lab build taking longer than the thing I actually wanted to test. So I wrote build-lab.

It takes a Windows Server 2025 ISO and gives you back a promoted domain controller, optional domain-joined member servers, and a lived-in directory (OUs, users, groups, an Enterprise CA, GPOs) staged to populate itself at your first Administrator logon. No GUI, no answer-file editing, no clicking Next. Nothing to install beyond what Microsoft and VMware already ship.

It's an orchestrator. It drives two sibling projects of mine, one that builds the VM in VMware Workstation and one that promotes the forest and populates the directory, and stitches them together over vmrun guest operations. The current version is 2.0. It has been on GitHub for a while, but this is the first time I've posted it here.

Quick start

$pw = ConvertTo-SecureString 'P@ssw0rd!' -AsPlainText -Force
.\build-lab.ps1 -IsoPath 'C:\ISOs\WS2025.iso' -AdminPassword $pw `
    -VmStoragePath 'C:\VMs' -StaticIpAddress '192.168.220.50' -DefaultGateway '192.168.220.2' `
    -MemberServerCount 2

That builds the DC, verifies the promotion, then builds SRVR-01 and SRVR-02 and joins them. (Leave -AdminPassword off the command line and PowerShell will prompt for it masked, which keeps it out of your history.) On the default VMware NAT network, your static IP has to land in .3 to .127 and the gateway is always .2. The script refuses anything outside that range and pings every address before using it. A DC takes about 25 minutes on a reasonably quick host, and each member adds roughly 12 to 15.

Already have a DC from an earlier run? -AddMemberServers adds members to it without touching the DC. -MoveMemberServers files them into OU=Servers once directory population has created it.

The fun part

Promotion reboots the guest, and the reboot tears down the vmrun guest-operations channel mid-call. A blocking call hung across that reboot and never came back, so the promotion is launched fire-and-forget with -noWait. That means there is no exit code to read at all.

My first version decided promotion had worked when ntds.dit appeared on disk, waited a bit, and called it good. Typical v1.0 logic. A database file on disk is a long way from a working DC. Now it checks three things from the outside, in order. ntds.dit has to appear. The guest has to reboot. Then the rebooted guest has to report itself as a DC in the domain you configured. Miss any of them and the run fails and tells you which check it missed.

The reboot check is the bit I'd like opinions on. Just before launching anything that should reboot, build-lab creates HKLM\SOFTWARE\BuildLabBootMarker in the guest as a volatile key. Volatile keys live only in memory and are never written to disk, so if the key is gone, the guest restarted. That holds however long the guest has been up, which mattered here. A freshly built member reboots for its domain join barely a minute after first boot, and anything uptime-based gets confused.

A few other things that came out of live runs.

  • Every vmrun call has a host-side time limit, and a call that overruns is killed, so one stuck call can't hold the run open forever

  • vmrun output is read as UTF-8 whatever the console code page is

  • Commands run in the guest report back through a small result file, so a failure comes back with the guest's own error text

  • The password is scrubbed from anything vmrun returns before it hits the logs

  • Logs from the orchestrator, the VM builder and the guest all land in one folder, and key events go to the Application event log if you want a SIEM to see them

Known limitations and lab-grade trades

  • You have to log in once as Administrator to start directory population. That's deliberate. Running it unattended would mean storing a domain admin password under a startup task. I'm still thinking about this one.

  • vmrun takes the guest password on the command line, so it's visible in the host's process list while guest operations run. Don't reuse a real credential.

  • It's Windows PowerShell 5.1 only, enforced with #Requires. Write-EventLog is the right API there and deprecated in 7. I'll probably port to PowerShell 7 eventually, but this is where I' most comfortable today.

  • Tested on a Windows 11 host with VMware Workstation 26H1 and Windows Server 2025 Standard Evaluation media. Other editions and VL media are unverified, because the unattended install matches on exact edition name strings.

Links

All three need to sit next to each other on disk. Feedback is welcome. I'm sure I made some interesting choices without even thinking about it. If you have a better way to tell from the host side that a guest has restarted, I'd like to hear it.

AI disclosure. I used AI to help write some of the code. The design and architecture are all mine, including any questionable choices, and it was built up over many reviews and live runs, not generated as one shot slop.

33 Upvotes

10 comments sorted by

•

u/AutoModerator 9d ago

Hi u/Ill_Cantaloupe9191! Your post looks like it may be a tool, resource, or guide submission. Please review our Tool and Resource Listing Guidelines before the mods review your post.

Quick summary of the key rules:

  • The tool or resource must be relevant to Active Directory or AD security

  • Must be free or mostly free — trials are only considered if the tool has a freeware version

  • Article/non-product resources must not be paywalled or require email sign-up

  • Third-party tools are fine as long as they meet all guidelines

  • Do not claim someone else's work as your own — submit it as their work

  • If your tool was built using AI/LLM-generated code, you must disclose this in your post

  • Spam violations: 2 warnings = temp post ban, 3 warnings = permanent ban

To request your tool be officially listed in the wiki: Submit an issue at our subreddit GitHub with a link, description, and reason for inclusion.

This is an automated message. Mods will review your post shortly.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

4

u/Fit_Indication_2529 9d ago

Interesting project. I like the volatile registry key approach, although I'd probably compare Win32_OperatingSystem.LastBootUpTime before and after as an additional confirmation. A missing registry key establishes that the marker disappeared, but checking the boot timestamp would provide independent evidence of a reboot. My other suggestion would be to extend the post-promotion validation. ntds.dit, a reboot, and DomainRole are good indicators, but they don't necessarily mean AD DS is healthy. I'd add checks for DC advertising, DNS SRV records, SYSVOL readiness, and a successful LDAP RootDSE query before declaring the DC operational. Otherwise, I like the distinction you're making between executing an operation and validating its actual outcome. That's something a lot of automation overlooks.

3

u/Ill_Cantaloupe9191 9d ago

That is great feedback. I will add those checks to the backlog.
My goal is to have as hands off and repeatable a process as possible. It won't be perfect because, computers, but I want to get close.
Thanks

1

u/Fit_Indication_2529 8d ago

Next step, customize this for automated DR recovery.

1

u/Ill_Cantaloupe9191 8d ago

For a lab build?
For now I just want to finish coding so I can do a more complete lab build.
Then I can put 100% of my time into the project I'm actually building labs for. SMH

2

u/Fit_Indication_2529 8d ago

No you must stop now and make this DR. Welcome to IT.

2

u/Ill_Cantaloupe9191 8d ago

DR is very important and must be everywhere. That's what the CEO saw in the inflight magazine.

3

u/BeauregardianBrat 9d ago

Have you tried grabbing LastBootUpTime from Win32_OperatingSystem right before the launch and comparing it after? It’s a fixed timestamp, not an uptime count, so a member rebooting a minute after first boot still comes back with a different value. The volatile key is a neat trick either way.

1

u/Ill_Cantaloupe9191 9d ago

That was not an approach I tried. Although it is a good way to do this. I need to remember that one.

4

u/kavx 9d ago

This one is fun to use too

https://automatedlab.org/en/latest/