Join Windows Server to AD Domain with PowerShell

Domain-join a Windows Server with Add-Computer: point DNS at the DC first, land in the right OU, verify the secure channel, fix the usual join errors.

Share
Join Windows Server to AD Domain with PowerShell

Domain-joining a server is one cmdlet. Getting it to work on the first try is three checks before that cmdlet. Almost every failed join I've looked at was DNS, and the rest was time or the OU path.

TL;DR: DNS to the DC, check you can find the DC, then join straight into the right OU.

$idx = (Get-NetAdapter | Where-Object Status -eq 'Up').ifIndex
Set-DnsClientServerAddress -InterfaceIndex $idx -ServerAddresses 10.0.0.10

Resolve-DnsName _ldap._tcp.dc._msdcs.lab.example.com -Type SRV

Add-Computer -DomainName 'lab.example.com' `
  -Credential (Get-Credential 'LAB\Administrator') `
  -OUPath 'OU=Servers,OU=Computers,OU=Corp,DC=lab,DC=example,DC=com' `
  -NewName 'srv01' `
  -Restart

Run it in Windows PowerShell 5.1 (powershell.exe), not PowerShell 7. Details below.

Domain, OU names and DC naming match my AD series: Part 1 — the first DC built lab.example.com on dc01, Part 2 — OUs, users, GPO basics built the OU=Corp tree. 10.0.0.10 is the DC's IP — swap in yours.

Before the join: the server itself

The member server needs the same basics as any new box — static IP, patched, sensible hostname. My list for that is the Windows Server initial setup checklist. Two things matter specifically for the join:

  • Static IP. Not strictly required for a member server, but a server that changes IP and has stale DNS records is a support ticket waiting to happen.
  • Hostname. If it's still WIN-8F3K2..., fix it. You can rename in the same step as the join with -NewName (above), or rename first with Rename-Computer -NewName srv01 -Restart and join after. Both work. Renaming after the join is possible but renames the AD object too and is one more thing to go sideways.

Step 1: DNS must point at the DC

This is the one. A domain-joined machine finds its DC through DNS SRV records, and those only exist on AD-integrated DNS — usually the DC itself. If the server is still using your router, 1.1.1.1 or the hoster's resolver, the join fails with "An Active Directory Domain Controller for the domain could not be contacted".

# What am I using right now?
Get-DnsClientServerAddress -AddressFamily IPv4 |
  Where-Object ServerAddresses | Format-Table InterfaceAlias, ServerAddresses

# Point at the DC (two DCs? list both)
$idx = (Get-NetAdapter | Where-Object Status -eq 'Up').ifIndex
Set-DnsClientServerAddress -InterfaceIndex $idx -ServerAddresses 10.0.0.10
Clear-DnsClientCache

Don't add a public resolver as secondary "just in case". Windows will happily ask it at some point, get NXDOMAIN for lab.example.com, and you get intermittent weirdness that's hell to reproduce. Public names get resolved through the forwarders on the DC — that's what they're for (set up in Part 1).

If the NIC has IPv6 enabled and gets DNS via router advertisements, check that too. An IPv6 resolver that doesn't know your domain can win the race.

Step 2: prove you can see the DC

# SRV record for domain controllers — must return dc01
Resolve-DnsName _ldap._tcp.dc._msdcs.lab.example.com -Type SRV

# DC locator — the same path the join uses
nltest /dsgetdc:lab.example.com

# Ports: LDAP, Kerberos, SMB
Test-NetConnection dc01.lab.example.com -Port 389
Test-NetConnection dc01.lab.example.com -Port 88
Test-NetConnection dc01.lab.example.com -Port 445

If Resolve-DnsName fails, you're still on the wrong DNS — go back to step 1. If DNS works but ports fail, it's a firewall between the server and the DC. Part 1 lists the full port set.

Step 3: time

Kerberos tolerates 5 minutes of clock skew. A fresh VM restored from an old template can easily be further off than that.

w32tm /stripchart /computer:dc01.lab.example.com /samples:3 /dataonly

If the offset is minutes, fix it before joining. After the join, the member syncs from the domain hierarchy automatically — you don't configure NTP peers on member servers.

Step 4: the join

$cred = Get-Credential 'LAB\Administrator'

Add-Computer -DomainName 'lab.example.com' `
  -Credential $cred `
  -OUPath 'OU=Servers,OU=Computers,OU=Corp,DC=lab,DC=example,DC=com' `
  -Restart

What each piece does:

  • -DomainName — the DNS name of the domain, not the NetBIOS name. lab.example.com, not LAB.
  • -Credential — an account allowed to create computer objects in that OU. Domain Admin works, but see below.
  • -OUPath — where the computer object lands. Without it, the object goes into the default computers container, which in Part 2 got redirected to OU=Workstations via redircmp. Servers belong in OU=Servers, so I always pass -OUPath explicitly.
  • -Restart — the join only takes effect after a reboot. Leave it off if you want to reboot later, but don't forget.

Never hardcode the password into the script. Get-Credential prompts; in automation, pull a credential from a vault or use a pre-staged object (below).

Don't use Domain Admin for joins

Typing Domain Admin credentials into every new box is exactly the habit Part 3 — GPO hardening is trying to break. Two better options:

A delegated join account. Create a service account and give it "Create/Delete Computer objects" on OU=Servers only (GPMC/ADUC delegation wizard, or dsacls). It can join machines into that OU and do nothing else.

Pre-stage the computer object. On the DC, create the object first:

New-ADComputer -Name 'srv01' `
  -Path 'OU=Servers,OU=Computers,OU=Corp,DC=lab,DC=example,DC=com'

Then the join attaches to the existing object instead of creating one. The account doing the join needs rights on that object (you can grant them in the object's Security tab, or give your delegated group rights on the OU). The upside: the object is in the right OU with the right name before the server even exists, and GPOs linked there apply on first boot.

Side note: by default any authenticated domain user can join up to 10 machines to the domain (ms-DS-MachineAccountQuota). That's a default most hardening guides tell you to set to 0 once you have proper delegation. Do it after your join process works, not before.

After the reboot: verify

Log in with a domain account (LAB\youradmin), then:

# Am I in the domain?
(Get-CimInstance Win32_ComputerSystem).Domain

# Is the secure channel to the DC healthy?
Test-ComputerSecureChannel -Verbose

# Which DC am I talking to?
nltest /sc_verify:lab.example.com

# Did the GPOs from the Servers OU apply?
gpresult /r /scope:computer

And from the DC side:

Get-ADComputer srv01 | Select-Object Name, DistinguishedName, Enabled

The DistinguishedName should end in OU=Servers,OU=Computers,OU=Corp,.... If it's in CN=Computers or OU=Workstations, -OUPath didn't make it — move it with Move-ADObject and run gpupdate /force on the server.

The network profile should also flip to DomainAuthenticated:

Get-NetConnectionProfile | Select-Object InterfaceAlias, NetworkCategory

That matters for firewall rules scoped to the Domain profile — RDP rules from the RDP hardening post depend on it.

Server Core

Same commands. Core has powershell.exe and the Add-Computer cmdlet; sconfig also has a menu option for domain join if you prefer menus. More Core-specific stuff in Windows Server Core administration.

Gotcha: Add-Computer doesn't exist in PowerShell 7

Add-Computer and Remove-Computer were dropped from the built-in module in PowerShell 6+. In pwsh you get "The term 'Add-Computer' is not recognized". Open powershell.exe (5.1, ships with every Windows Server) and run it there. Same for anything else that leans on the old WMI-based computer cmdlets.

Gotcha: the usual error messages and what they mean

  • "An Active Directory Domain Controller for the domain could not be contacted" — DNS. Every time I've seen it. Step 1 and 2.
  • "The specified domain either does not exist or could not be contacted" — also DNS, or you used the NetBIOS name where it wants the FQDN.
  • "Access is denied" — the credential can't create objects in that OU, or the computer object already exists and the account doesn't have rights on it.
  • Join fails with an OU error — typo in the DN, or the OU doesn't exist. Copy the DN from Get-ADOrganizationalUnit -Filter 'Name -eq "Servers"' instead of typing it.
  • Join works, domain logon fails after reboot — time skew, or the secure channel broke. See below.

Gotcha: cloned VMs

If you clone a VM template, sysprep it (sysprep /generalize) before the clone gets joined. Microsoft only supports sysprepped images for domain members, and two machines that started as the same unprepped image can trip over each other in ways that don't show up on day one. Same rule as Part 1's "don't snapshot DCs": don't get clever with VM copies in AD.

When the trust breaks

"The trust relationship between this workstation and the primary domain failed." Usually after restoring a member server from an old backup or snapshot — its machine password no longer matches what AD has. Fix without leaving the domain:

Test-ComputerSecureChannel -Repair -Credential (Get-Credential 'LAB\Administrator')

Log in with a local admin account to run it (domain logon is what's broken). If that fails, the nuclear option is unjoin and rejoin:

Remove-Computer -UnjoinDomainCredential (Get-Credential 'LAB\Administrator') `
  -WorkgroupName 'WORKGROUP' -Restart

Then join again as above. Unjoining with domain credentials typically disables the computer object in AD rather than deleting it — check it with Get-ADComputer srv01 before the rejoin, and decide whether you want to reuse it (keeps group memberships) or delete it and start clean.

Where this fits

Promote DC, build OUs, join members, then harden. Once servers sit in OU=Servers, the policies from Part 3 and the SMBv1 / LLMNR / NTLMv1 lockdown actually have something to apply to. For the PowerShell basics underneath all of this, my PowerShell sysadmin cheatsheet covers the syntax.


Related posts