Windows Server for beginners. Part 22: How does trust between forests work?

All parts of the series “Windows Server for beginners”

In the previous part, trust between the domains of one forest appeared by itself, was two-way and transitive, and could be neither configured nor switched off. Between forests everything is the other way round: there is no trust until someone creates it, and it has a direction, a type, a list of suffixes, filters and modes, and each of these settings answers the question "what will the other forest be able to do in mine". The partner forest in this part is partner.example.net: a company we need to share resources with without merging directories.

All commands below run from the administrator's workstation ws1 in PowerShell 7 with RSAT (part 15); whatever works only on the controller itself goes through Invoke-Command (part 14). Our forest is corp.example.com with controllers dc1, dc2, dc3 and dc4, the PDC is dc1; the partner has three controllers: dc1, dc2 and dc3.partner.example.net. A two-way forest trust between the forests already exists, and everything that reads it is shown with real output. Commands that create or change the trust are given from the documentation, without output.


Types, direction, transitivity

Direction. A trust is a relationship between two sides, and it is one-way by definition: the trusting domain (on the resource side) accepts the identities of users of the trusted domain (on the account side). "Corp trusts partner" means that partner users can get access to corp resources, but not the other way round. From the corp side this is an outgoing trust (we trust them), from the partner side an incoming one (they are trusted by us). A two-way trust is two one-way trusts.

Transitivity. A transitive trust extends beyond the two sides: trusting the partner forest, we trust all its domains. A non-transitive trust covers only the one domain it was created with.

Types:

Type With whom Transitive Authentication When
Forest the root domain of another forest yes, to all domains of both forests Kerberos and NTLM, name suffix routing the main type between two AD forests
External one domain of another forest (or an NT 4.0 domain) no Kerberos and NTLM, into that domain only, no suffix routing when one domain is needed, not a forest
Realm a non-Microsoft Kerberos realm (MIT, Heimdal, FreeIPA) optional Kerberos only single sign-on with Unix infrastructure
Shortcut a domain of your own forest yes Kerberos and NTLM to straighten the referral path in a deep tree

A reminder of what the protocols in the table do. Kerberos (part 6) is tickets: the client gets a service ticket from the KDC of its domain, and if the service is in another domain, a referral ticket (referral TGT, krbtgt/<other domain>) encrypted with a key shared by the two domains, which it takes to the KDC of that domain. NTLM is challenge-response: the server passes the client's response to its controller, and that controller passes it over the Netlogon secure channel to a controller of the user's domain (pass-through authentication).

A forest trust requires a forest functional level of at least Windows Server 2003 on both sides, that is, any modern one. It is created only between the root domains of two forests and doesn't extend to a third forest: if corp trusts partner and partner trusts third.example.org, corp doesn't trust third. From here on the subject is the forest trust; an external trust differs in scope and in how strictly it filters SIDs.


DNS

Before the corp KDC issues a referral ticket into partner, the client has to find partner controllers; before a corp controller creates a trust, it has to find partner controllers, and vice versa. Each forest has to resolve the names of the other — not only partner.example.net but also the SRV records of _msdcs.partner.example.net by which the Locator finds controllers (part 7).

The usual way is a conditional forwarder stored in the directory and replicated to all DNS servers of the forest:

PS C:\> Add-DnsServerConditionalForwarderZone -ComputerName dc1 -Name partner.example.net -MasterServers 198.51.100.10, 198.51.100.11 -ReplicationScope Forest

Another option that works too is a stub zone: it holds only the SOA and NS records of the partner's zone with the addresses of its DNS servers and refreshes them from those servers itself, so when the partner replaces its DNS servers, our list changes by itself. This is what a stub zone stored in the directory and replicated to the whole forest looks like:

PS C:\> Get-DnsServerZone -CimSession dc1 -Name partner.example.net | Format-Table ZoneName, ZoneType, IsDsIntegrated, ReplicationScope, MasterServers -AutoSize

ZoneName            ZoneType IsDsIntegrated ReplicationScope MasterServers
--------            -------- -------------- ---------------- -------------
partner.example.net Stub               True Forest           {198.51.100.10, 198.51.100.11}

It is created with Add-DnsServerStubZone -Name partner.example.net -MasterServers … -ReplicationScope Forest. A check from the workstation:

PS C:\> Resolve-DnsName _ldap._tcp.dc._msdcs.partner.example.net -Type SRV | Where-Object Type -eq SRV | Select-Object NameTarget, Port

NameTarget              Port
----------              ----
dc2.partner.example.net  389
dc1.partner.example.net  389
dc3.partner.example.net  389

PS C:\> Resolve-DnsName dc1.partner.example.net | Select-Object IPAddress

IPAddress
---------
198.51.100.10

Without Where-Object Type -eq SRV the table gets rows with an empty NameTarget: these are the A records of the controllers from the additional section of the answer, which have no such field.

The partner does the same for corp.example.com. Besides DNS you need time within five minutes between the forests (Kerberos makes no exceptions for other realms, part 12) and the network: between the controllers of both forests in both directions — LDAP 389, SMB 445, Kerberos 88, the RPC endpoint mapper 135 and dynamic RPC ports (LSA during trust creation and Netlogon go over them), and from the clients of each forest to the controllers of the other — Kerberos 88 and LDAP 389.


Creating

The ActiveDirectory module has only one trust cmdlet:

PS C:\> Get-Command -Module ActiveDirectory *Trust*

CommandType Name
----------- ----
     Cmdlet Get-ADTrust

There is no New-ADTrust and no Set-ADTrust. According to the documentation, netdom trust can't create a forest trust between two AD forests: it creates external and realm trusts, and for existing ones it verifies, resets the key and changes flags. A forest trust is created with the "Active Directory Domains and Trusts" snap-in or with the .NET class System.DirectoryServices.ActiveDirectory.Forest, which the module doesn't wrap. If you have the partner administrator's credentials, the trust is created on both sides with one command, and both sides get a random trust password:

PS C:\> $local  = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
PS C:\> $cred   = Get-Credential 'PARTNER\admin'
PS C:\> $ctx    = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', 'partner.example.net', $cred.UserName, $cred.GetNetworkCredential().Password)
PS C:\> $remote = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ctx)
PS C:\> $local.CreateTrustRelationship($remote, 'Bidirectional')

TrustDirection takes Inbound, Outbound, Bidirectional — from the point of view of your own forest. The usual case with a partner who has their own administrators and whose credentials nobody gives us is one side at a time, with a shared password that both administrators enter:

PS C:\> $local.CreateLocalSideOfTrustRelationship('partner.example.net', 'Outbound', (Read-Host 'Trust password'))

and on the partner's side CreateLocalSideOfTrustRelationship('corp.example.com', 'Inbound', <the same password>). This password is needed only to set the trust up: after that, the PDC of the trusting side changes it every 30 days (more on that in the section about the trust key). Domain Admins of the root domain or Enterprise Admins can create a trust — each side its own.

An external trust to one domain of the partner is created with netdom:

PS C:\> netdom trust corp.example.com /domain:partner.example.net /add /twoway /userD:PARTNER\admin /passwordD:* /userO:CORP\alice.admin /passwordO:*

The same class reads an existing trust:

PS C:\> $local.GetTrustRelationship('partner.example.net')

TopLevelNames            : {partner.example.net}
ExcludedTopLevelNames    : {}
TrustedDomainInformation : {partner.example.net}
SourceName               : corp.example.com
TargetName               : partner.example.net
TrustType                : Forest
TrustDirection           : Bidirectional

PS C:\> $local.GetSidFilteringStatus('partner.example.net')
True
PS C:\> $local.GetSelectiveAuthenticationStatus('partner.example.net')
False

SID filtering is on, selective authentication is off; what that means comes below. The same view from the module:

PS C:\> Get-ADTrust -Filter * | Format-Table Name, Direction, TrustType, ForestTransitive, SIDFilteringQuarantined, SIDFilteringForestAware, SelectiveAuthentication, TGTDelegation -AutoSize

Name                    Direction TrustType ForestTransitive SIDFilteringQuarantined SIDFilteringForestAware SelectiveAuthentication TGTDelegation
----                    --------- --------- ---------------- ----------------------- ----------------------- ----------------------- -------------
partner.example.net BiDirectional   Uplevel             True                   False                   False                   False         False

This is an easy place to go wrong: SIDFilteringForestAware: False doesn't mean there is no filtering. This property shows the TREAT_AS_EXTERNAL bit (more on it below), which relaxes filtering on a forest trust; False means full filtering, and GetSidFilteringStatus above honestly answers True. SIDFilteringQuarantined is the quarantine bit set on external trusts; on a forest trust it's False. TGTDelegation: False means TGT delegation across the trust isn't explicitly allowed, that is, it's forbidden.

This is how Netlogon sees the trust:

PS C:\> nltest /trusted_domains
List of domain trusts:
    0: PARTNER partner.example.net (NT 5) (Direct Outbound) (Direct Inbound) ( Attr: foresttrans )
    1: CORP corp.example.com (NT 5) (Forest Tree Root) (Primary Domain) (Native)
The command completed successfully

nltest /domain_trusts /all_trusts /v prints the same list and adds the SIDs and GUIDs of the domains. Each controller keeps a secure channel to the partner's domain, and it has to be looked at on a controller, not on the workstation:

PS C:\> nltest /server:dc1 /sc_query:partner.example.net
Flags: 30 HAS_IP  HAS_TIMESERV  Authentication Service: Netlogon
Trusted DC Name \\dc3.partner.example.net
Trusted DC Connection Status Status = 0 0x0 NERR_Success
The command completed successfully

PS C:\> nltest /sc_query:partner.example.net
I_NetLogonControl failed: Status = 1355 0x54b ERROR_NO_SUCH_DOMAIN

A workstation has a secure channel only to its own domain, so without /server: the answer is "no such domain". /sc_query only reads the channel state (more on it in part 18); according to the documentation, nltest /sc_verify:partner.example.net and netdom trust corp.example.com /domain:partner.example.net /verify /kerberos check the trust actively and rebuild a broken channel, so they are run when something is already broken.


The trusted domain object

Each trust is a trustedDomain object in CN=System, as in the previous part, but with other bits in trustAttributes. In parentheses are the Get-ADTrust properties that the module computes from these bits:

Bit Name What
0x1 NON_TRANSITIVE non-transitive (external) (DisallowTransivity)
0x2 UPLEVEL_ONLY only for Windows 2000 and newer clients (UplevelOnly)
0x4 QUARANTINED_DOMAIN quarantine: strict SID filtering on an external trust (SIDFilteringQuarantined)
0x8 FOREST_TRANSITIVE forest trust (ForestTransitive)
0x10 CROSS_ORGANIZATION selective authentication (SelectiveAuthentication)
0x20 WITHIN_FOREST within the forest (IntraForest)
0x40 TREAT_AS_EXTERNAL forest trust, but SID filtering relaxed to the external level (SIDFilteringForestAware)
0x80 USES_RC4_ENCRYPTION realm trust with an MIT Kerberos that supports RC4 (UsesRC4Encryption)
0x100 USES_AES_KEYS TGTs across the trust are encrypted with AES (UsesAESKeys)
0x200 CROSS_ORGANIZATION_NO_TGT_DELEGATION TGT delegation forbidden; after the 2019 updates 0x800 works instead
0x400 PIM_TRUST together with 0x40, a trust with a privileged access forest (PIM), with its own SID filtering rules
0x800 CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION TGT delegation explicitly allowed (TGTDelegation)
0x1000 DISABLE_AUTH_TARGET_VALIDATION domain name validation during NTLM pass-through authentication is off

PIM (Privileged Identity Management) is a scheme in which administrator accounts live in a separate protected forest and get rights in the production forest through such a trust for a limited time.

PS C:\> $t = Get-ADObject -Filter 'objectClass -eq "trustedDomain" -and Name -eq "partner.example.net"' -SearchBase 'CN=System,DC=corp,DC=example,DC=com' -Properties trustPartner, flatName, trustType, trustDirection, trustAttributes, msDS-SupportedEncryptionTypes
PS C:\> $t | Format-List trustPartner, flatName, trustType, trustDirection, trustAttributes, msDS-SupportedEncryptionTypes

trustPartner                  : partner.example.net
flatName                      : PARTNER
trustType                     : 2
trustDirection                : 3
trustAttributes               : 8
msDS-SupportedEncryptionTypes : 24

PS C:\> '0x{0:X}' -f $t.trustAttributes
0x8

trustType 2 is an Active Directory domain (1 is an NT 4.0 domain, 3 an MIT realm); trustDirection 3 is two-way (1 inbound, 2 outbound); trustAttributes 0x8 is FOREST_TRANSITIVE only. There's no NO_TGT_DELEGATION bit, and that's fine: the 2019 updates don't set it but change the default — without the 0x800 bit, forwarded TGTs don't cross the trust. msDS-SupportedEncryptionTypes 24 (0x18) is AES128 and AES256; more on the trust key below. A forest trust also has the binary attribute msDS-TrustForestTrustInfo with the partner's list of suffixes — the next section.

The trust key lives in the trustAuthIncoming and trustAuthOutgoing attributes of this object; there's no need to read them, and better not to. An incoming trust also has a paired account named after the partner's domain with a $ at the end:

PS C:\> Get-ADObject -LDAPFilter '(sAMAccountName=PARTNER$)' -Properties sAMAccountType, userAccountControl, pwdLastSet | Format-List DistinguishedName, @{n='sAMAccountType'; e={'0x{0:X}' -f $_.sAMAccountType}}, @{n='userAccountControl'; e={'0x{0:X}' -f $_.userAccountControl}}, @{n='pwdLastSet'; e={[datetime]::FromFileTime($_.pwdLastSet)}}

DistinguishedName  : CN=PARTNER$,CN=Users,DC=corp,DC=example,DC=com
sAMAccountType     : 0x30000002
userAccountControl : 0x820
pwdLastSet         : 28.10.2026 16:26:12

sAMAccountType 0x30000002 is a trust account, and userAccountControl has 0x800, INTERDOMAIN_TRUST_ACCOUNT (and 0x20, password not required by policy). Don't delete or rename it by hand: it's part of the trust.


Name suffixes

User bob@partner.example.net types his UPN on a corp workstation, or a corp client asks for a ticket to cifs/fs1.partner.example.net. The corp controller has to figure out which forest to send the request to. For this a forest trust has name suffix routing: when the trust is created, each forest collects the partner's namespaces — domain tree names, UPN and SPN suffixes, domain SIDs — and stores them in msDS-TrustForestTrustInfo of the trust object. Trust objects replicate to the global catalog (part 10), and when the KDC doesn't find an SPN in its own forest, the global catalog compares its suffix with the trusts' lists and returns a hint about which forest to send the client to.

PS C:\> $local.GetTrustRelationship('partner.example.net').TopLevelNames

Name                 Status
----                 ------
partner.example.net Enabled

Our partner has one unique suffix, the name of its forest; child domains and names like *.partner.example.net are routed together with it. A suffix's status is one of four: Enabled, NewlyCreated, AdminDisabled, ConflictDisabled. When a trust is created, all unique suffixes are enabled, while a new unique suffix that the partner adds later (a UPN suffix, a new tree) appears after the trust is verified but isn't routed until an administrator of the trusting side enables it.

A suffix conflict occurs when the same DNS name, NetBIOS name or domain SID already exists in our forest or in another trusted forest: then the suffix is disabled in the new trust and won't be routed into the partner's forest. For example, if corp uses the alternative UPN suffix example.org and the partner has it too, a sign-in as alice@example.org from corp won't go to the partner. Hence a common picture: sign-in by UPN across the trust doesn't work, while sign-in as PARTNER\bob does. The list of suffixes in netdom:

PS C:\> netdom trust corp.example.com /namesuffixes:partner.example.net
PS C:\> netdom trust corp.example.com /namesuffixes:partner.example.net /togglesuffix:2

The first command numbers the suffixes, the second enables or disables the suffix with that number. The numbering may change, so /togglesuffix is run right after /namesuffixes, and a conflicting suffix can be enabled only after it has been disabled in the other trust. A disabled suffix is a disabled path: names under it aren't routed across the trust.


SID filtering

From the previous part: inside a forest, controllers don't check where a SID in sIDHistory or in a user's groups came from. Between forests they do, and this is called SID filtering: a controller of the trusting forest that receives a ticket with a PAC or a pass-through NTLM response across the trust strikes out of the SID list those that the trusted forest has no right to claim. A forest trust lets through only the SIDs of the domains of the partner's forest (those recorded in msDS-TrustForestTrustInfo). A SID in sIDHistory pointing to the Enterprise Admins group of our forest will be removed, even if the partner's administrator diligently wrote it there; SIDs of third forests and service SIDs like S-1-5-9 (Enterprise Domain Controllers) are removed too: the other forest's controllers don't become our controllers.

The controller writes each such case to the Security log as event 4675 "SIDs were filtered" (the Logon audit subcategory). We see it regularly:

PS C:\> Invoke-Command -ComputerName dc3, dc4 -ScriptBlock {
    Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4675} -ErrorAction SilentlyContinue |
        ForEach-Object { ([xml]$_.ToXml()).Event.EventData.Data | Where-Object Name -eq SidList | ForEach-Object { $_.'#text'.Trim() } } |
        Group-Object -NoElement
} | Format-Table PSComputerName, Count, Name -AutoSize

PSComputerName Count Name
-------------- ----- ----
dc3                8 %{S-1-5-9}
dc4               14 %{S-1-5-9}

In all events one SID was struck out, S-1-5-9, and the account is a computer from the partner's domain: a partner controller connected to ours, and its membership in the Enterprise Domain Controllers of its own forest was discarded on our side. This is exactly how the filter should work; an event 4675 with other SIDs, especially SIDs of our groups, is a reason to find out who put them there.

An external trust filters more strictly or more loosely depending on the QUARANTINED_DOMAIN bit. With quarantine (netdom trust … /quarantine:Yes) only SIDs of the trusted domain itself get through; controllers starting with Windows Server 2003 and Windows 2000 SP4 put all new external trusts into quarantine themselves.

Filtering on a forest trust can be relaxed with the TREAT_AS_EXTERNAL bit: netdom trust corp.example.com /domain:partner.example.net /EnableSIDHistory:Yes. Then the trust is filtered like an external one, and SIDs in the sIDHistory of partner users that were created by administrators (RID 1000 and up) get through. The built-in privileged groups of our forest — Domain Admins, Enterprise Admins and others with a RID below 1000 — are cut off even in this mode, but any group of our own, for example a group with administrator rights on servers, becomes available to the partner's administrator: he can write its SID into the sIDHistory of his account. There is one reason to turn this on: migrating users from the partner's forest with their old SIDs preserved, so that resource permissions keep working until they are reassigned. For the duration of the migration, and back (/EnableSIDHistory:No) right after. The sign of a relaxed trust is SIDFilteringForestAware: True in Get-ADTrust.


Selective authentication

By default a forest trust gives forest-wide authentication: any partner user can authenticate on any corp machine. That doesn't give him rights to resources — but the server he authenticated on adds Authenticated Users of its own forest to his token, and this group shows up in share permissions, in the default permissions on directory objects and in the Pre-Windows 2000 Compatible Access group, which is allowed to read accounts. A partner forest with a hundred people who are guilty of nothing gets the ability to read our whole directory.

Selective authentication changes the default: a partner user can authenticate only on those corp machines on whose accounts he or his group has been explicitly granted the extended right Allowed to Authenticate. It is turned on on the trusting side, for the outgoing trust:

PS C:\> netdom trust corp.example.com /domain:partner.example.net /SelectiveAUTH:Yes

The same is $local.SetSelectiveAuthenticationStatus('partner.example.net', $true). The right is granted on the computer object in the directory, not on the server itself. Its GUID comes from CN=Extended-Rights of the configuration partition:

PS C:\> Get-ADObject -SearchBase "CN=Extended-Rights,$((Get-ADRootDSE).configurationNamingContext)" -LDAPFilter '(displayName=Allowed to Authenticate)' -Properties rightsGuid | Format-List Name, rightsGuid

Name       : Allowed-To-Authenticate
rightsGuid : 68B1D179-0D15-4d4f-AB71-46152E79A7BC
PS C:\> $acl = Get-Acl 'AD:\CN=FS1,OU=Servers,DC=corp,DC=example,DC=com'
PS C:\> $sid = (Get-ADGroup -Identity 'Partner Project Team' -Server partner.example.net).SID
PS C:\> $allowedToAuth = [Guid]'68b1d179-0d15-4d4f-ab71-46152e79a7bc'
PS C:\> $acl.AddAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule($sid, 'ExtendedRight', 'Allow', $allowedToAuth)))
PS C:\> Set-Acl 'AD:\CN=FS1,OU=Servers,DC=corp,DC=example,DC=com' -AclObject $acl

Now the partner's group authenticates on fs1 and nowhere else; permissions on fs1 folders are granted to it the usual way, through a corp domain local group, as in the next part. If a service runs not as the computer but under a domain account (SQL Server under svc-sql), for Kerberos the right is granted on that account; for NTLM, always on the computer.

How it works: a controller of the corp root domain that receives a request across a trust with selective authentication adds the SID S-1-5-1000, Other Organization, to the user's authorization data. The controller issuing the service ticket to fs1 sees this SID, checks the Allowed to Authenticate right on the fs1 object and doesn't issue the ticket without it. Our own users and users of trusts without selective authentication get S-1-5-15, This Organization, on servers instead:

PS C:\> whoami /groups /fo csv | ConvertFrom-Csv | Where-Object 'Group Name' -match 'Organization' | Select-Object 'Group Name', SID

Group Name                     SID
----------                     ---
NT AUTHORITY\This Organization S-1-5-15

A partner user under selective authentication will have NT AUTHORITY\Other Organization in this place — rights can be granted or denied by it separately.

For a trust with a partner you share two servers with, selective authentication is the right default. For a trust between two forests of one company, where users go anywhere, it's extra work.


Delegation across the trust

The third security setting is TGT delegation. Unconstrained delegation from part 6 forwards the user's TGT to a server; across a trust this would mean that the partner's server gets our user's TGT and can act on his behalf anywhere in our forest. After the 2019 updates (KB4490425), forwarded TGTs across incoming forest and external trusts aren't issued by default — for new and existing trusts alike, with no configuration. Only the ENABLE_TGT_DELEGATION bit allows them; it's set by netdom trust corp.example.com /domain:partner.example.net /EnableTGTDelegation:Yes, and Get-ADTrust shows it as TGTDelegation: True. The client knows about the ban too: in klist, tickets into the partner's forest carry the cache flag DISABLE-TGT-DELEGATION (output in "Looking at it by hand").

If a partner application really requires unconstrained delegation for users of another forest, that's a reason to rework it to resource-based delegation, which works across trusts and forwards no TGT. Treat /EnableTGTDelegation:Yes as switching off a protection; on a quarantined trust it can't be enabled at all.


The trust key and AES

The trust key is a password shared by the two sides from which the Kerberos keys for referral tickets are derived, and like any Kerberos principal it has encryption types. The KDC chooses the encryption type of a referral ticket by the client and the trust, not by the client and the service. Historically a trust supported RC4, and a client left with AES only couldn't reach services in the partner's forest: the KDC answered KDC_ERR_ETYPE_NOSUPP. The documentation disagrees here: the article about this error says that by default a trust supports RC4, while the description of event 4769 says that after the updates AES is supported by default for controllers and trusts unless the attribute was changed by hand. To not depend on defaults, the encryption types are set on the trust object explicitly, on both sides — each administrator on his own:

PS C:\> ksetup /setenctypeattr partner.example.net AES128-CTS-HMAC-SHA1-96 AES256-CTS-HMAC-SHA1-96

ksetup works with the trust object through the controller's local LSA, so it runs on a controller; from a workstation it answers with an error:

PS C:\> ksetup /getenctypeattr partner.example.net
Query of attributes on partner.example.net failed with 0xc0000034
Failed /GetEncTypeAttr : 0xc0000034

PS C:\> Invoke-Command -ComputerName dc1 -ScriptBlock { ksetup /getenctypeattr partner.example.net }
Enctypes for domain partner.example.net: AES128-CTS-HMAC-SHA1-96 AES256-CTS-HMAC-SHA1-96

The same is the "The other domain supports Kerberos AES Encryption" checkbox in the trust properties, and in the directory the value 24 (0x18) in msDS-SupportedEncryptionTypes of the trust object, which we saw above. If clients still use RC4, RC4-HMAC-MD5 is added to the list.

The trust password changes by itself: every 30 days the PDC of the trusting side (part 17) creates a new one, keeps the previous one next to it and asks a controller of the trusted side over the secure channel to store the new one. For a two-way trust this happens twice, separately for each direction. You can see it in the replication metadata of the trust object:

PS C:\> Get-ADReplicationAttributeMetadata -Object $t -Server dc1 -Properties trustAuthIncoming, trustAuthOutgoing, msDS-SupportedEncryptionTypes, trustAttributes | Format-Table AttributeName, LastOriginatingChangeTime, Version -AutoSize

AttributeName                 LastOriginatingChangeTime Version
-------------                 ------------------------- -------
msDS-SupportedEncryptionTypes 09.09.2026 15:21:17             1
trustAttributes               20.12.2022 19:33:09             1
trustAuthOutgoing             28.10.2026 16:26:12            47
trustAuthIncoming             24.10.2026 19:51:04            47

The trust is almost four years old, and each key is at version 47 — one change a month. The outgoing key (we trust the partner, the PDC is ours) and the incoming one (changed by the partner's PDC) were updated on different days. trustAttributes hasn't changed once since creation, and AES was turned on later, as the date of msDS-SupportedEncryptionTypes shows. The cmdlet doesn't show the key values themselves: metadata is only who, when and which version.


Realm

A trust with a Kerberos realm without Active Directory — MIT Kerberos or Heimdal on Linux, FreeIPA (a directory with Kerberos for Linux) — is created with netdom as an exchange of a shared key:

PS C:\> netdom trust corp.example.com /domain:EXAMPLE.ORG /add /realm /passwordT:*
PS C:\> ksetup /addkdc EXAMPLE.ORG kdc1.example.org
PS C:\> ksetup /setenctypeattr EXAMPLE.ORG AES256-CTS-HMAC-SHA1-96

On the MIT side, kadmin addprinc krbtgt/CORP.EXAMPLE.COM@EXAMPLE.ORG with the same password. Realm users don't exist in the directory, and for alice@EXAMPLE.ORG to open a folder in corp, her principal is mapped to an account: Set-ADUser alice-ext -Add @{ altSecurityIdentities = 'Kerberos:alice@EXAMPLE.ORG' }. The other KDC doesn't put a PAC into the ticket, there are no groups from the other realm, and NTLM doesn't go across such a trust — only Kerberos, and only what is mapped. In part 30 we'll come back to this mechanism from the Linux side.


Maintenance

If a trust password change didn't reach the partner (the network was down, the secure channel didn't come up), the trusting controller keeps using the old password and tries again in 15 minutes; the trust breaks when one side is left with neither the new nor the previous password of the other. Then users get error 1788 "The trust relationship between the primary domain and the trusted domain failed.", and the System log of the controllers gets Netlogon events: 5719 ("This computer was not able to set up a secure session with a domain controller in domain …") on the one that can't bring up the secure channel, and 5805 or 5722 ("The session setup from the computer … failed to authenticate") on the one it failed to connect to.

Checking and repairing, from the documentation: netdom trust corp.example.com /domain:partner.example.net /verify and /reset with the credentials of both sides (/userD, /passwordD, /userO, /passwordO), and for a forest trust also $local.RepairTrustRelationship($remote). Without the partner's credentials, each side sets the shared password anew on its own: $local.UpdateLocalSideOfTrustRelationship('partner.example.net', <new password>) on ours and the same on the partner's.

Removal is $local.DeleteTrustRelationship($remote) (both sides) or netdom trust corp.example.com /domain:partner.example.net /remove. If the partner has already removed its side and a trustedDomain object is left on ours, it's removed with $local.DeleteLocalSideOfTrustRelationship('partner.example.net') or netdom … /remove /oneside:trusting.


Looking at it by hand

The path of a ticket across the trust, through the eyes of a corp client that needs a share on a partner controller:

PS C:\> klist get cifs/dc1.partner.example.net | Select-String 'retrieved'
A ticket to cifs/dc1.partner.example.net has been retrieved successfully.

PS C:\> klist | Select-String 'Server: (krbtgt/PARTNER|cifs/dc1.partner)' -Context 1,9
  #0>   Client: alice.admin @ CORP.EXAMPLE.COM
>       Server: krbtgt/PARTNER.EXAMPLE.NET @ CORP.EXAMPLE.COM
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40a10000 -> forwardable renewable pre_authent name_canonicalize
        Start Time: 10/29/2026 15:03:45 (local)
        End Time:   10/30/2026 1:03:45 (local)
        Renew Time: 11/5/2026 13:28:12 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0x200 -> DISABLE-TGT-DELEGATION
        Kdc Called: dc1.corp.example.com

  #3>   Client: alice.admin @ CORP.EXAMPLE.COM
>       Server: cifs/dc1.partner.example.net @ PARTNER.EXAMPLE.NET
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
        Start Time: 10/29/2026 15:03:45 (local)
        End Time:   10/30/2026 1:03:45 (local)
        Renew Time: 11/5/2026 13:28:12 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0x200 -> DISABLE-TGT-DELEGATION
        Kdc Called: dc3.partner.example.net

klist get itself prints the whole cache after the success line, so the tickets we need are picked out separately. The referral ticket krbtgt/PARTNER.EXAMPLE.NET was issued by our KDC (@ CORP.EXAMPLE.COM, Kdc Called: dc1) and is encrypted with the trust key — AES-256, because AES is enabled on the trust. The service ticket was issued by the partner's KDC (dc3.partner.example.net), and it has the ok_as_delegate flag: the partner's service is trusted for delegation. But both tickets carry the cache flag DISABLE-TGT-DELEGATION: the client won't forward its TGT to the partner's services.

On our controller this request is event 4769, where the service is the partner's domain itself:

PS C:\> Invoke-Command -ComputerName dc1 -ScriptBlock {
    $e = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4769; StartTime=(Get-Date).AddMinutes(-60)} |
        Where-Object { $_.Properties[2].Value -eq 'PARTNER.EXAMPLE.NET' } | Select-Object -First 1
    $d = [ordered]@{}; ([xml]$e.ToXml()).Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
    'ServiceName','TicketEncryptionType','Status','ServiceSupportedEncryptionTypes','ServiceAvailableKeys' | ForEach-Object { '{0,-32}: {1}' -f $_, $d[$_] }
}
ServiceName                     : PARTNER.EXAMPLE.NET
TicketEncryptionType            : 0x12
Status                          : 0x0
ServiceSupportedEncryptionTypes : 0x18 (AES128-SHA96, AES256-SHA96)
ServiceAvailableKeys            : AES128-SHA96, AES256-SHA96

0x12 is AES256, 0x18 the types allowed on the trust, and it has AES keys. If only RC4 were left on the trust, this would be 0x17, and a client without RC4 would be refused.

All trusts of the forest with their settings in one table:

PS C:\> Get-ADTrust -Filter * -Properties * | Format-Table Name, Direction, ForestTransitive, SelectiveAuthentication, SIDFilteringForestAware, TGTDelegation, @{n='EncTypes'; e={$_.'msDS-SupportedEncryptionTypes'}}, Created -AutoSize

Name                    Direction ForestTransitive SelectiveAuthentication SIDFilteringForestAware TGTDelegation EncTypes Created
----                    --------- ---------------- ----------------------- ----------------------- ------------- -------- -------
partner.example.net BiDirectional             True                   False                   False         False       24 20.12.2022 19:33:09

In a forest with child domains the trusts within the forest show up here too; they are told apart by IntraForest: True. This table is worth getting once a month: a trust whose SIDFilteringForestAware or TGTDelegation became True, an external trust whose SIDFilteringQuarantined became False, or a partner trust that lost selective authentication is a question for whoever changed it.


Sources

Trusts:

Specifications and schema:

Commands:


The trust is created, the filters are on, the suffixes are routed. A partner user can authenticate on fs1 — but he still has no rights there, and granting them is not as simple as it seems.

Next — part 23: access across a trust in practice. Which groups accept members from another forest and which don't, and why the built-in Administrators group won't accept a domain local one; a scheme for granting rights to file resources, servers, SQL Server and RDP to users of another forest; what foreign users look like in the directory — foreignSecurityPrincipals; and what to check when "access is there, but it won't let me in".

Only registered users can leave a comment.

Sign in or register to leave a comment.