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:

  1. First upgrade the domain level
  2. Then upgrade the forest level
  3. 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…