Windows Server for beginners. Part 23: Granting access across a trust in practice

All parts of the series “Windows Server for beginners”

The trust from the previous part lets a partner user authenticate in corp. From here on it is ordinary permission granting, with one correction: not every corp group will accept that user as a member, and that decides how to build the chain from the user to the folder. This part is four typical tasks that come up with every trust, and the order of checks when everything is done and still doesn't work.

The setup is the same as in the previous part: our forest corp.example.com with controllers dc1, dc2, dc3 and dc4, the partner forest partner.example.net, and a two-way forest trust between them without selective authentication; commands run from the workstation ws1 in PowerShell 7 with RSAT (part 15), and whatever works only on the server goes through Invoke-Command (part 14). Everything that reads the directory, logs and permissions is shown with real output. Commands that create groups, shares and permissions are given from the documentation, without output.


Who accepts whom

The group scope rules from part 3, applied to another forest:

Group Users and global groups of the other forest Domain local groups of your own domain
Domain local accepts accepts (from the same domain)
Global doesn't accept: only principals of its own domain doesn't accept
Universal doesn't accept: only principals of its own forest doesn't accept
Built-in (CN=Builtin: Administrators, Remote Desktop Users, Backup Operators — these are the local groups of domain controllers) accepts doesn't accept
Local on a member server (SAM) accepts accepts

Hence the only working scheme for another forest: partner principals → a domain local group of our domain → permissions on the resource. The scheme is called AGDLP: Accounts go into Global groups of their own domain, global groups go into Domain Local groups of the resource domain, and Permissions are granted to the domain local groups. The global group here is the partner's group, maintained by the partner's administrator; our universal groups take no part in this chain at all.

Built-in groups are a special case. The module shows them as domain local, but their groupType also has bit 0x1, BUILTIN_LOCAL_GROUP:

PS C:\> Get-ADGroup Administrators -Properties groupType | Format-List Name, GroupScope, GroupCategory, @{n='groupType';e={'0x{0:X8}' -f $_.groupType}}, DistinguishedName

Name              : Administrators
GroupScope        : DomainLocal
GroupCategory     : Security
groupType         : 0x80000005
DistinguishedName : CN=Administrators,CN=Builtin,DC=corp,DC=example,DC=com

0x80000005 is a security group (0x80000000), domain local (0x4) and built-in (0x1). According to the documentation, a domain local group can be a member of local groups on computers of its domain, except built-in groups with well-known SIDs; a domain controller answers such an attempt with error 8520 ERROR_DS_LOCAL_CANT_HAVE_CROSSDOMAIN_LOCAL_MEMBER ("A local group cannot have another cross domain local group as a member."). So partner principals get into Administrators and Remote Desktop Users of domain controllers only directly, as a user or the partner's global group. But foreign administrators on domain controllers are a question not of group scopes but of whether they belong there at all.

What happens when a user of another forest gets into our group: the domain controller receives from the client a reference to the member in the form of a SID (<SID=S-1-5-21-…>), doesn't find such an object locally and, since the domain of that SID isn't part of our forest, creates an object of class foreignSecurityPrincipal named after the SID in the CN=ForeignSecurityPrincipals container. That object becomes the group member. It stores only the SID; tools look up the display name through the trust every time, and if the trust is broken or removed, a bare SID remains in the group.

Creating the group and adding the partner's global group to it, from the documentation:

PS C:\> New-ADGroup -Name 'FS1-Project-RW' -GroupScope DomainLocal -Path 'OU=Groups,DC=corp,DC=example,DC=com' -Description 'Partner: read and write \\fs1\project'
PS C:\> Add-ADGroupMember -Identity 'FS1-Project-RW' -Members (Get-ADGroup 'Partner Project Team' -Server partner.example.net)

The module reads the partner's global group with -Server partner.example.net, through the trust, with our credentials. If the module refuses to accept an object from another forest, writing the reference by SID directly through ADSI gives the same result, and the domain controller creates the foreignSecurityPrincipal object just the same:

PS C:\> $sid = ([System.Security.Principal.NTAccount]'PARTNER\Partner Project Team').Translate([System.Security.Principal.SecurityIdentifier]).Value
PS C:\> ([ADSI]'LDAP://CN=FS1-Project-RW,OU=Groups,DC=corp,DC=example,DC=com').Add("LDAP://<SID=$sid>")

What it looks like in the directory can be seen on a group that already has a foreign principal. In our domain that's the built-in Administrators: it contains the administrator PARTNER\alice.admin, who has accounts in both forests. The member attribute stores a reference to the foreignSecurityPrincipal:

PS C:\> (Get-ADGroup Administrators -Properties member).member
CN=S-1-5-21-1711390224-2037633311-3961243089-1105,CN=ForeignSecurityPrincipals,DC=corp,DC=example,DC=com
CN=Administrator,CN=Users,DC=corp,DC=example,DC=com
CN=Domain Admins,CN=Users,DC=corp,DC=example,DC=com
CN=Enterprise Admins,CN=Users,DC=corp,DC=example,DC=com

Get-ADGroupMember, on the other hand, shows not that object but the partner's object itself: Active Directory Web Services (ADWS) on our domain controller resolves the SID through the trust and reads the object from the partner's directory:

PS C:\> Get-ADGroupMember Administrators | Select-Object Name, ObjectClass, DistinguishedName

Name              ObjectClass DistinguishedName
----              ----------- -----------------
Alice Admin       user        CN=Alice Admin,OU=Members,DC=partner,DC=example,DC=net
Administrator     user        CN=Administrator,CN=Users,DC=corp,DC=example,DC=com
Domain Admins     group       CN=Domain Admins,CN=Users,DC=corp,DC=example,DC=com
Enterprise Admins group       CN=Enterprise Admins,CN=Users,DC=corp,DC=example,DC=com

So Get-ADGroupMember depends on the trust: when a SID doesn't resolve, the cmdlet may fail with an error instead of a list, while the member attribute can always be read.


A file share

Task: the partner's project group reads and writes \\fs1\project. The chain: PARTNER\Partner Project Team (global) → CORP\FS1-Project-RW (domain local, created above) → permissions on the folder. From the documentation:

PS C:\> Invoke-Command fs1 {
    New-Item C:\Shares\Project -ItemType Directory -Force | Select-Object FullName
    $acl = Get-Acl C:\Shares\Project
    $acl.SetAccessRuleProtection($true, $false)
    $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule('CORP\Domain Admins', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
    $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule('CORP\FS1-Project-RW', 'Modify', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
    $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule('NT AUTHORITY\SYSTEM', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
    Set-Acl C:\Shares\Project $acl
    (Get-Acl C:\Shares\Project).Access | Select-Object IdentityReference, FileSystemRights
    New-SmbShare -Name Project -Path C:\Shares\Project -FullAccess 'CORP\Domain Admins' -ChangeAccess 'CORP\FS1-Project-RW'
}

Both the share and the NTFS permissions go to the domain local group; the partner's group doesn't appear in the ACL and shouldn't: when the project team changes, you change the membership of FS1-Project-RW, not permissions on a thousand folders. SetAccessRuleProtection($true, $false) turns off inheritance and throws away the inherited entries, and with them Users and Authenticated Users from the C: drive leave the ACL; with a trust without selective authentication, Authenticated Users includes the whole other forest. For the same reason the permissions of New-SmbShare are listed explicitly: without them the share gets Everyone with read access.

Share permissions are read with Get-SmbShareAccess; this is its output on a real share, the NETLOGON share of a domain controller:

PS C:\> Get-SmbShareAccess -CimSession dc1 -Name NETLOGON

Name     ScopeName AccountName            AccessControlType AccessRight PSComputerName
----     --------- -----------            ----------------- ----------- --------------
NETLOGON *         Everyone               Allow             Read        dc1
NETLOGON *         BUILTIN\Administrators Allow             Full        dc1

For Project it will show two lines: CORP\Domain Admins with Full and CORP\FS1-Project-RW with Change. Through -CimSession the output gets a PSComputerName column.

If the trust has selective authentication enabled, one more step is needed: the Allowed to Authenticate right on the fs1 computer object for the partner's group, as in the previous part. Without it the partner's user won't get through authentication on fs1, and the error will be ERROR_AUTHENTICATION_FIREWALL_FAILED (1935): "The computer you are signing into is protected by an authentication firewall. The specified account is not allowed to authenticate to the computer."


Server administration

Task: the partner's engineers administer app1, which runs their application. The chain: PARTNER\Partner Engineers → CORP\App1-Admins (domain local) → the local Administrators group on app1.

The local group can be filled directly, Invoke-Command app1 { Add-LocalGroupMember -Group Administrators -Member 'CORP\App1-Admins' }, but the right way is a group policy on the servers' OU, so that the set of local administrators is defined centrally and restored after a manual edit: Restricted Groups or Group Policy Preferences → Local Users and Groups, with the action "add CORP\App1-Admins to Administrators". We'll get to that in part 24. The group's membership is read with Get-LocalGroupMember; this is what it looks like on our member server before the addition:

PS C:\> Invoke-Command app1 { Get-LocalGroupMember Administrators } | Format-Table ObjectClass, Name, PrincipalSource -AutoSize

ObjectClass Name               PrincipalSource
----------- ----               ---------------
User        APP1\Administrator Local
Group       CORP\Domain Admins ActiveDirectory

After the addition the list gets the line Group CORP\App1-Admins ActiveDirectory. The columns are listed explicitly here: objects from Invoke-Command arrive deserialized, without their default view, and without Format-Table the table shows all properties together with SID, PSComputerName and RunspaceId.

A domain local group on a member server works like this: when our domain's KDC issues a service ticket for app1, it adds to the PAC the corp domain local groups that the user or the user's groups belong to, including partner groups that sit in them through a foreignSecurityPrincipal. The server sees the SID of CORP\App1-Admins in the ticket and expands membership in its local group from it.

What the partner's engineers get: full rights on app1 and, through them, the ability to extract from its memory the credentials of everyone who signed in to it. That's why corp administrators sign in to app1 with Remote Credential Guard or not at all (part 15), and app1 lives at the partner's trust level, not ours. For domain controllers this scenario isn't considered; if the partner needs something on a domain controller, that's JEA from part 14 with a limited set of commands, and the role in the session configuration is granted to a group that contains the partner's group.


RDP

Task: partner users connect to app1 over Remote Desktop. Two conditions are needed: the right to sign in over RDP, which is membership in the local Remote Desktop Users group (through the same chain: CORP\App1-RDP, domain local), and, with selective authentication, Allowed to Authenticate on app1. Network Level Authentication (NLA, checking the user before the server creates a session) works across the trust: the client gets a ticket for TERMSRV/app1.corp.example.com with a referral ticket, as for any other service. From the documentation:

PS C:\> New-ADGroup -Name 'App1-RDP' -GroupScope DomainLocal -Path 'OU=Groups,DC=corp,DC=example,DC=com'
PS C:\> Add-ADGroupMember 'App1-RDP' -Members (Get-ADGroup 'Partner Engineers' -Server partner.example.net)
PS C:\> Invoke-Command app1 { Add-LocalGroupMember -Group 'Remote Desktop Users' -Member 'CORP\App1-RDP'; Get-LocalGroupMember 'Remote Desktop Users' }

On a partner workstation the user enters PARTNER\bob or bob@partner.example.net, and the RDP client finds the right KDC by itself. RDS licensing is a separate question: every user or device connecting to a server with the Remote Desktop Session Host role needs a client access license (CAL). For the license server to issue Per User licenses to users of another forest, the forests need a two-way trust, and the license server must be a member of the Terminal Server License Servers group in those users' domain, that is, in the partner's domain, where the partner's administrator adds it. Administrative connections to a server without the Session Host role (two sessions) need no licenses.


SQL Server

Task: a partner application running as PARTNER\svc-app, or the partner's analysts with their own accounts, connect to SQL Server on sql1. SQL Server accepts Windows names from trusted domains, and a group is more convenient here than individual names. From the documentation:

CREATE LOGIN [CORP\SQL1-Analysts] FROM WINDOWS;
USE Sales;
CREATE USER [CORP\SQL1-Analysts] FOR LOGIN [CORP\SQL1-Analysts];
ALTER ROLE db_datareader ADD MEMBER [CORP\SQL1-Analysts];
EXEC xp_logininfo 'PARTNER\bob', 'all';

xp_logininfo shows through which group the user gets access, which is handy for checking the chain. Authentication to SQL across the trust uses Kerberos if sql1 has the SPN MSSQLSvc/sql1.corp.example.com:1433 registered (part 6) and the client can get a referral ticket; otherwise NTLM, which also works across a forest trust, but without delegation and with every logon checked through the secure channels of the domain controllers of both forests. With selective authentication, the Allowed to Authenticate right for Kerberos is granted on the account SQL Server runs as (if it's a domain account rather than the computer itself), for NTLM on the sql1 computer object. How the client signed in can be seen in SQL:

SELECT c.session_id, s.login_name, c.auth_scheme
FROM sys.dm_exec_connections AS c
JOIN sys.dm_exec_sessions AS s ON c.session_id = s.session_id;

auth_scheme = KERBEROS means everything is right; NTLM for a user from the trusted forest usually means that the client can't reach the KDC on port 88, the SPN isn't registered, or the corp.example.com suffix isn't routed in the partner forest.


When access is denied

Access is granted, and the partner's user still gets a denial. The order of checks goes from the network to permissions:

  1. Is the trust alive? nltest /server:dc1 /sc_query:partner.example.net shows the state of the secure channel to the partner on our domain controller (it only reads, part 18); the partner does the same in the other direction. nltest /sc_verify and netdom trust … /verify check the trust actively and recreate a broken channel, so they are run once it's already clear what is broken.
  2. Does the partner's client find our forest? On their machine, nltest /dsgetdc:corp.example.com; if it fails, the partner's DNS doesn't know _msdcs.corp.example.com, or a firewall closed 88 and 389 from their network to our domain controllers. The fallback to NTLM comes from here too.
  3. Selective authentication. Is there an Allowed to Authenticate right for a group the user really belongs to? The right can be granted on its own (ObjectType = the GUID of the right) or be part of full control (GenericAll) and of "all extended rights" (ExtendedRight with an empty ObjectType). This is what it looks like on our server, where there is no selective authentication and no separate entries:
PS C:\> (Get-Acl 'AD:\CN=APP1,OU=Servers,DC=corp,DC=example,DC=com').Access |
    Where-Object { $_.ObjectType -eq '68b1d179-0d15-4d4f-ab71-46152e79a7bc' -or ($_.ActiveDirectoryRights -match 'ExtendedRight|GenericAll' -and $_.ObjectType -eq [Guid]::Empty) } |
    Format-Table IdentityReference, AccessControlType, IsInherited, @{n='Rights'; e={"$($_.ActiveDirectoryRights)"}} -AutoSize

IdentityReference         AccessControlType IsInherited Rights
-----------------         ----------------- ----------- ------
NT AUTHORITY\SYSTEM                   Allow       False GenericAll
S-1-5-32-548                          Allow       False GenericAll
CORP\Domain Admins                    Allow       False GenericAll
CORP\Enterprise Admins                Allow        True GenericAll
BUILTIN\Administrators                Allow        True CreateChild, Self, WriteProperty, ExtendedRight, Delete, GenericRead, WriteDacl, WriteOwner

Only administrators have the right, through full control and all extended rights. S-1-5-32-548 is Account Operators: a built-in group of the domain that doesn't exist on the workstation, so the SID stays unresolved. With selective authentication, a line with the partner's group and ExtendedRight should appear here.

  1. The group chain. Is the user in the partner's global group? Is that group in our domain local group? Is ours in the resource's permissions? At every step, Get-ADGroupMember, with -Server partner.example.net for the partner's group. A separate trap: the partner's group must be global or universal; our group won't accept the partner's domain local group, because it's visible only in the partner's domain.
  2. A stale ticket. The membership was added after the user got the ticket. Our domain local groups are added to the PAC by our KDC when it issues a service ticket, so klist purge on the client and a new connection to the resource pick them up. The partner's groups are in the TGT issued by the partner's KDC, and here signing out and in is needed.
  3. SID filtering. If permissions are granted to a SID from sIDHistory (after a migration), event 4675 on our domain controller shows that it was filtered out.
  4. The resource itself. Get-SmbShareAccess, Get-Acl, and on the resource, whoami /groups as the user once they do sign in: does the token contain our domain local group?

The Security log on the resource: 4624 means the user signed in, so the problem is in permissions; 4625 with a status code means the user didn't, and the code says why: 0xC000015B, the user hasn't been granted this logon type (for example, no membership in Remote Desktop Users); 0xC0000413, the authentication firewall, that is, selective authentication; 0xC000006D, a bad user name or authentication information. The Account Domain field of 4624 depends on the protocol: with Kerberos it holds the partner's realm in capitals (PARTNER.EXAMPLE.NET), with NTLM the NetBIOS name (PARTNER), and a filter on only one of them misses half the logons.


Seeing it for yourself

All foreign principals that were granted something in our domain, and where exactly:

PS C:\> Get-ADObject -SearchBase 'CN=ForeignSecurityPrincipals,DC=corp,DC=example,DC=com' -Filter 'objectClass -eq "foreignSecurityPrincipal"' -Properties memberOf |
    ForEach-Object {
        $name = try { (New-Object System.Security.Principal.SecurityIdentifier $_.Name).Translate([System.Security.Principal.NTAccount]).Value } catch { '<unresolved>' }
        [PSCustomObject]@{ SID = $_.Name; Name = $name; MemberOf = ($_.memberOf -replace '^CN=([^,]+),.*', '$1') -join ', ' }
    } | Format-Table -AutoSize

SID                                            Name                                       MemberOf
---                                            ----                                       --------
S-1-5-21-1711390224-2037633311-3961243089-1105 PARTNER\alice.admin                        Administrators
S-1-5-17                                       NT AUTHORITY\IUSR
S-1-5-11                                       NT AUTHORITY\Authenticated Users           Users, Pre-Windows 2000 Compatible Access, Certificate Service DCOM Access
S-1-5-9                                        NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERS Windows Authorization Access Group
S-1-5-20                                       NT AUTHORITY\NETWORK SERVICE               IIS_IUSRS
S-1-5-4                                        NT AUTHORITY\INTERACTIVE                   Users

Five of the six objects are standard: well-known SIDs such as Authenticated Users and INTERACTIVE are stored here too when they are members of the domain's built-in groups, and an object without memberships, like IUSR, just sits there. There is one foreign principal: the partner's administrator in Administrators, that is, an administrator on all our domain controllers. The name resolved through the trust without any credentials: Translate asks the workstation's LSA, and the LSA asks a domain controller that has the trust. A line with <unresolved> would mean a principal that no longer exists at the partner (or a broken trust): a bare SID in our group that has to be removed; Translate throws "Some or all identity references could not be translated." on such a SID. This report is worth getting regularly: it's the complete list of what the other forest can do in ours.

Sessions on a file server are shown by Get-SmbSession; a partner user would have PARTNER\… in ClientUserName. This is the output on our domain controller:

PS C:\> Get-SmbSession -CimSession dc1 | Select-Object ClientComputerName, ClientUserName, NumOpens

ClientComputerName                     ClientUserName   NumOpens
------------------                     --------------   --------
[2001:db8:10:6c01:7288:e2bc:80b1:f718] CORP\alice.admin        0
[fe80::215:5dff:fe00:10]               CORP\alice.admin        0
[2001:db8:10:6c01:7288:e2bc:80b1:f718] CORP\WS1$               1
[::1]                                  CORP\alice.admin        0

Only our own sessions here: the administrator from the workstation, from another machine and from the domain controller itself ([::1]), and the WS1$ computer account with an open file. The command shows an IPv6 client address in square brackets.

Logons from the partner forest on our servers are 4624 events whose Account Domain is the partner. They are best selected with an XPath filter by field name rather than by index in Properties (part 18), and parsed inside Invoke-Command: deserialized events arrive at the workstation without Properties values:

PS C:\> Invoke-Command dc1, dc2, dc3, dc4 {
    $xp = "*[System[EventID=4624] and EventData[Data[@Name='TargetDomainName']='PARTNER.EXAMPLE.NET' or Data[@Name='TargetDomainName']='PARTNER']]"
    Get-WinEvent -LogName Security -FilterXPath $xp -ErrorAction SilentlyContinue | ForEach-Object {
        $d = @{}; ([xml]$_.ToXml()).Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
        [PSCustomObject]@{ Domain = $d.TargetDomainName; Package = $d.AuthenticationPackageName; LogonType = $d.LogonType; Computer = $d.TargetUserName.EndsWith('$') }
    } | Group-Object Domain, Package, LogonType, Computer -NoElement
} | Sort-Object PSComputerName | Format-Table PSComputerName, Count, Name -AutoSize

PSComputerName Count Name
-------------- ----- ----
dc2                1 PARTNER.EXAMPLE.NET, Kerberos, 3, True
dc3                4 PARTNER.EXAMPLE.NET, Kerberos, 3, True
dc4               10 PARTNER.EXAMPLE.NET, Kerberos, 3, True

All logons are network logons (type 3), over Kerberos, and all are partner computers (name ending in $): the same calls from the partner's domain controllers to ours for which, in the previous part, SID filtering dropped S-1-5-9. Their Account Domain is the realm PARTNER.EXAMPLE.NET, not the NetBIOS name: a filter on PARTNER alone would find nothing. On dc1 there are no such logons within the time its log keeps. A partner user who opened \\fs1\project would give the same line with False in the last column in the fs1 log.

That user's token on the server, if they sign in over RDP and run whoami /groups, is put together from three parts: the groups of their domain (PARTNER\Domain Users, PARTNER\Partner Engineers) from the TGT issued by the partner's KDC; the corp domain local groups (CORP\App1-RDP, CORP\App1-Admins) added by our KDC to the service ticket; and the local groups of app1 that those belong to (BUILTIN\Remote Desktop Users, BUILTIN\Administrators). With selective authentication it contains NT AUTHORITY\Other Organization (S-1-5-1000), without it NT AUTHORITY\This Organization (S-1-5-15). If CORP\App1-Admins isn't in the token, the chain is broken on our side, and step 4 of the list above is where to look.


Sources

Groups and foreignSecurityPrincipals:

Selective authentication:

Resources:

Commands and events:


Trust, groups, permissions on resources. What remains is a tool that was mentioned twice in this part and never explained, although no domain does without it: central configuration of machines.

Next — part 24: group policies. What a policy object consists of and where it lives, linking, processing order and inheritance, filtering by groups and WMI, gpupdate and gpresult, the Central Store and administrative templates, Group Policy Preferences, backing up policies, and the GroupPolicy module for all of it.

Only registered users can leave a comment.

Sign in or register to leave a comment.