There are some projects that we put off for years. Not because they're impossible, but because there's always something getting in the way. In my case, it was Exchange 2013—that good old mail server that stood in the way of any attempt to modernize the AD. Since migrating to a lighter solution, the coast was clear. So here's the story of a busy night's work.
The context
The infrastructure runs on two separate Active Directory domains linked by a two-way trust relationship:
- One domain dedicated to servers and infrastructure
- One domain dedicated to workstations and users
This separation is intentional: if a user account is compromised on the workstation side, the attacker cannot automatically bounce back onto the critical infrastructure. This is common sense when it comes to segmentation.
Both domains had been running for years at the Windows Server 2012 R2 functional level, while the domain controllers were all running Windows Server 2016. This is the kind of technical debt that accumulates quietly—until the day you decide to deal with it.
Prerequisites before touching anything
1. Check the health of AD replication
Before performing any operations on functional levels, check that all DCs are healthy and replicating correctly:
repadmin /replsummary
The output should show 0 failures on all source and destination domain controllers. If errors appear, stop there and correct them first.
dcdiag /test:replications
This test must be successful on every DC. There is no tolerance for replication errors before a functional level upgrade.
2. Identify FSMO roles
netdom query fsmo
You need to know exactly who holds which roles. Important points:
- The Schema Master and Domain Naming Master are forest roles—they live in the root domain
- The command to raise the forest level must be executed from the DC that holds the Schema Master, not just any DC
This is a point that caused me to make a mistake during the process — I'll come back to this below.
3. Full backup first
Functional level upgrades are irreversible. You cannot downgrade without restoring a backup. So, make a full backup of all DCs before you start. There can be no compromise on this.
# Check current levels before proceeding
Get-ADDomain | Select DomainMode
Get-ADForest | Select ForestMode
Functional level upgrade
Domain and forest — in the right order
The procedure is simple, but the order matters:
- First upgrade the domain level
- Then upgrade the forest level
- Repeat for each domain
On the domain's PDC Emulator:
Set-ADDomainMode -Identity mydomain.local -DomainMode Windows2016Domain
PowerShell asks for confirmation — this is normal and welcome.
On the DC that holds the Schema Master (root domain):
Set-ADForestMode -Identity mondomaine.local -ForestMode Windows2016Forest
The classic error to avoid
If you run the Set-ADForestMode command from a DC that is not the Schema Master, you will get:
Set-ADForestMode: A reference was returned by the server
This is not a catastrophic error—it's just Windows telling you to go to the right DC. Log in to the Schema Master and rerun the command.
Special case: two domains, one forest
In an architecture with two domains in the same forest (or linked by intra-forest trust), the forest level is shared. When you raise the forest from the root domain, the change applies to the whole forest. When you try to raise the forest for the second domain, you will get:
Set-ADForestMode: Unable to lower the functional level of the domain (or forest)
Despite the misleading message, this is not an error—it has already been done. A check confirms this:
Get-ADDomain mydomain.lan | Select DomainMode
Get-ADForest mydomain.lan | Select ForestMode
Auditing AD approvals
Once the functional levels were up to date, I took the opportunity to audit the approval relationship between the two domains — set up more than ten years ago and never really rechecked since.
Reading the status of an approval
Get-ADTrust -Filter * | Format-List *
Important attributes to analyze:
| Attribute | Expected value | What it means |
|---|---|---|
Direction |
BiDirectional | Both domains trust each other |
ForestTransitive |
True/False | Forest trust or not |
SelectiveAuthentication |
False | Any account can authenticate cross-domain |
SIDFilteringQuarantined |
False | No strict SID filtering |
UsesAESKeys |
False* | See below |
*UsesAESKeys: False does not mean that RC4 is used — it is just an AD flag that indicates that AES encryption is not explicitly enforced at the trust object level. The reality of Kerberos traffic can be very different.
Checking the actual Kerberos encryption
The klist command lists active Kerberos tickets and their actual encryption type:
klist
For each ticket, look for the KerbTicket encryption type line. On a Windows Server 2016 infrastructure with accounts configured correctly, you should see:
KerbTicket encryption type: AES-256-CTS-HMAC-SHA1-96
This is what we want. AES-256 on all tickets, including cross-domain tickets to the infrastructure (LDAP, CIFS, inter-domain krbtgt). The UsesAESKeys: False flag in AD was therefore misleading—Kerberos traffic was already AES-256 in practice.
Security architecture: assessment and reflections
Domain segmentation: a good idea?
Separating workstations and servers into two domains with approval is a valid security approach. If a user account on the workstation side is compromised, the attacker does not automatically have rights to the server infrastructure.
But this barrier is only effective if:
- Service accounts are distinct per domain—no single account that authenticates on both sides
- Scripts and scheduled tasks do not use cross-domain accounts
- Access rights to cross-domain shares are managed by dedicated groups, not individual accounts
AGDLP: still relevant
For cross-domain access to file shares, the AGDLP method remains best practice:
- Account (user account in the workstations domain)
- Global group (global group in the workstations domain)
- Domain Local group (local domain group in the servers domain)
- Permission (NTFS rights on the share)
It takes more work to set up, but it provides clear visibility on who has access to what, and makes long-term management easier.
What the 2016 functional level brings
Beyond cleaning up technical debt, here are the concrete benefits:
Enhanced Kerberos security — native AES-256 support for all accounts, better session key management.
Native Windows LAPS — the 2016 level is a prerequisite for migrating from the old LAPS client (legacy MSI) to Windows LAPS integrated into Windows 11/Server 2022. On Windows 11 24H2, the legacy LAPS client is no longer necessary anyway.
Privileged Access Management (PAM) — ability to create group memberships with automatic expiration, useful for temporary privileged access.
Foundation for further development — prerequisite for a possible upgrade to functional levels 2019 or 2022.
Post-operation verification commands
# Check functional levels
Get-ADDomain | Select DomainMode
Get-ADForest | Select ForestMode
# Check approval status
Get-ADTrust -Filter * | Select Name, Direction, TrustType, ForestTransitive
# Check current Kerberos encryption
klist
One night's work to settle ten years of technical debt. The functional level upgrade itself is quick—a few PowerShell commands and it's done. The real work is in the preparation: making sure replication is healthy, correctly identifying FSMO roles, and making a solid backup before touching anything.
The approval audit was an opportunity to rediscover a configuration that had been set up a long time ago and to see that it was still working — which is reassuring. And the Kerberos check confirmed that AES-256 encryption was already in place in practice, despite an AD flag that suggested otherwise.
The next task: migrate legacy LAPS to native Windows LAPS now that the functional level allows it.
I'll also have to think about upgrading the OS on my controllers…