Identity, Authentication, and Access Management Questions
Designing and operating identity and access control systems. Covers authentication protocols and standards (OAuth, SAML, OIDC, MFA), authorization models (RBAC, ABAC), identity lifecycle and privilege management, IAM architecture and automation, and access control across cloud and on-premises environments. The 'who can do what' control plane, distinct from cryptographic key management.
Explain the difference between NTFS permissions and share permissions on a Windows file server. Include: which permissions are evaluated first, how they combine, typical mistakes administrators make, and guidance for designing permission strategies for shared data.
Sample Answer
Direct answer
NTFS (New Technology File System) permissions and share permissions are two independent access-control layers on a Windows file server: NTFS permissions live on the file or folder itself and apply to every access path, local or over the network, while share permissions apply only to the network share definition and are irrelevant to someone logged on locally. For a network request, the share permission is evaluated first as a gate at the SMB (Server Message Block, the file-sharing network protocol) layer, then NTFS permission is evaluated on the underlying object, and the effective result is whichever of the two is more restrictive, never the more permissive.
Structured elaboration
| NTFS permissions | Share permissions | |
|---|---|---|
| Enforced on | The file or folder's own access control list | The share definition only |
| Applies to | Local access and network access | Network (SMB) access only |
| Granularity | Fine-grained (Read, Write, Modify, Full Control, and special permissions) | Coarse (Read, Change, Full Control) |
| Inheritance model | Rich: explicit vs. inherited entries with a defined precedence | Simple; not layered the same way |
| Typical production role | Carries the real, fine-grained access-control logic | Usually left wide open, acting as a pass-through gate |
Which is evaluated first, and how they combine. For a request over \\server\share\path, the server checks the share's access control list first; if that already denies the requested access, NTFS is never even consulted. If the share allows it, NTFS permission on the actual file or folder is evaluated next. The final effective access for that network request is the intersection, the more restrictive of the two results, not an average and not "whichever came first wins." A local logon directly on the server skips the share layer entirely, since there is no SMB negotiation involved; only NTFS applies.
flowchart TD
A[Client requests network access to a UNC path] --> B{Access path}
B -->|Network SMB| C[Evaluate share ACL first]
B -->|Local logon on the server| D[Share layer skipped entirely]
C --> E[Evaluate NTFS ACL on the underlying file or folder]
D --> E
E --> F[Effective access: more restrictive of share and NTFS, or NTFS alone if local]
How permissions combine within NTFS itself (a distinct evaluation from the share/NTFS intersection above). Within the NTFS access control list, Windows evaluates access control entries (ACEs) in a fixed canonical order: explicit Deny, then explicit Allow, then inherited Deny (nearest parent first), then inherited Allow. The access check walks this order and stops as soon as every requested access bit has been resolved. This produces a genuinely counter-intuitive but real behavior: an explicit Allow set directly on a folder is evaluated, and can fully satisfy the requested access, before the check ever reaches an inherited Deny coming down from a parent folder. "Deny always wins" is therefore only a half-truth: it is true when comparing two entries at the same explicit-or-inherited level, but an explicit Allow beats an inherited Deny, because explicit entries are evaluated first.
How this behaves through a clustered file server. On a Windows failover cluster hosting a file-server role (including a Scale-Out File Server serving SMB from multiple nodes at once), both the share definition and the NTFS access control list live on the shared storage itself, not on any individual node, so a failover moving the resource to another node does not change which permissions apply. What can change during a failover is client-visible continuity: SMB 3.0's transparent failover lets an open file handle survive the transition without an application-visible error, but only if the share is configured for continuous availability and both client and server support it; without that, in-flight connections drop and reconnect, which simply forces a fresh permission check on reconnect rather than changing the permission model itself. The practical implication is to avoid making permission or ACL changes in the middle of a planned failover, since a change that has not fully replicated to the shared storage before the transition can be applied inconsistently.
Diagnosing a real access-denied ticket with logs, rather than hand-tracing ACEs. Rather than guessing which of the two layers is at fault, enable object-access auditing (a System Access Control List, or SACL, on the folder, and the "File System" advanced-audit subcategory) and check the Security event log for the layer-specific events: event ID 4656 for a handle request at the NTFS layer (which, since Windows Server 2012, includes an access-reasons expansion naming exactly which ACE produced a denial) and event ID 5145 for the share-layer access check over SMB. Where auditing was not already enabled before the incident, or as a faster first pass, the Advanced Security Settings "Effective Access" tab computes the same combined-precedence result described above for a chosen user or group without requiring anyone to hand-trace ACE order from memory, which is exactly where mistakes happen under ticket pressure.
Typical administrator mistakes. Assuming a permission granted at one layer ("they have Full Control on the share") automatically means access at the other layer, when the two are independent and only their intersection matters. Leaving the share layer wide open and relying entirely on NTFS, which is the standard, recommended pattern (below), but only works as intended if NTFS is actually kept correct, since the share layer then provides no backstop at all if an NTFS entry is ever wrong. The reverse misconfiguration, NTFS wide open and access controlled only at the share, is equally common and worse: it looks correctly restricted over the network but is fully open to any local logon path on the server, such as an administrator's RDP session, since NTFS is the only thing that would apply there. And adding a broad explicit Deny near the top of a folder tree to "lock everyone out by default," then being confused when a more specific group further down still cannot get access, without realizing an inherited Deny from above only loses to a more specific EXPLICIT Allow, not to another inherited entry.
Guidance for designing a permission strategy. Grant Everyone (or Authenticated Users) a permissive share permission (Change or Full Control) and do all real, fine-grained access control at the NTFS layer; this avoids maintaining two separately drifting permission models and concentrates auditing, effective-access tooling, and troubleshooting on the one layer that actually has rich tooling for it. Assign NTFS permissions to security groups, never individual accounts, consistent with a group-based access model. Avoid explicit Deny ACEs except for genuinely exceptional cases; an explicit Deny is a blunt override that is easy to forget about later and is a common source of the confusing tickets described above, so structuring group membership so someone simply is not in an Allow-granting group is usually the safer way to exclude them. Leave inheritance enabled by default and only break it deliberately, with the exception documented, since disabled inheritance on one folder is itself a frequent source of "why does this one folder behave differently from everything around it" surprises.
Worked example
Consider D:\Data, shared as \\FS01\Data with the recommended wide-open share permission (Everyone: Full Control at the share layer). On D:\Data itself, an administrator previously added an inherited-down NTFS access control entry, "Domain Users: Deny Full Control," intending to lock the whole tree down by default. Later, a subfolder D:\Data\ProjectX had an explicit (not inherited) NTFS entry added directly on it: "ProjectX-Team: Allow Modify."
Can a member of ProjectX-Team reach \\FS01\Data\ProjectX over the network?
- Share layer: Everyone has Full Control at the share, so the share check allows the request.
- NTFS layer on
D:\Data\ProjectX: the relevant entries for this user are an explicit Allow (ProjectX-Team: Modify, set directly on this folder) and an inherited Deny (Domain Users: Deny Full Control, inherited from the parent). Per the canonical evaluation order, explicit Allow is checked before inherited Deny, and the requested Modify-level access bits are fully satisfied by the explicit Allow before the check ever reaches the inherited Deny entry. NTFS effective result: Allow, at the Modify level. - Combine the two layers: share allows Full Control, NTFS allows Modify; the effective access is the intersection, the more restrictive of the two, which is Modify.
So the computed answer is yes: a ProjectX-Team member gets Modify access over the network, despite the broad Domain Users Deny sitting on the parent folder, precisely because an explicit Allow beats an inherited Deny. An administrator who assumes "Deny always wins, full stop" would expect this user to be locked out, and the mismatch between that expectation and the actual computed result is exactly the kind of confusing ticket the Effective Access tab or the 4656/5145 event pairing above is meant to resolve without guesswork.
Trade-offs and pitfalls
The wide-open-share, NTFS-does-the-work pattern is the industry-standard approach and the one this answer recommends, but it is a genuine trade-off, not a free win: it centralizes enforcement in one place, which is simpler to audit and reason about, at the cost of having zero backstop at the share layer if an NTFS entry is ever misconfigured, so it depends entirely on NTFS being kept correct rather than providing defense in depth through two independent layers. Explicit Deny ACEs are powerful exactly because they override inheritance in one direction but not the other, which is precisely what makes them easy to misuse and hard to debug months later; treat them as a last resort, not a default tool for restricting a folder. Finally, log-based diagnosis only works retroactively if the relevant SACL auditing was already enabled before the incident happened; a common and costly pitfall is only turning on object-access auditing after a confusing access-denied ticket arrives, by which point the historical evidence for that specific event is already gone, so enabling baseline auditing on sensitive shares proactively, before there is a ticket to investigate, is what actually makes the log-based method useful when it is needed.
Walk me through the steps you would use (ADUC GUI or PowerShell) to create a new domain user account for a contractor who needs file-share access only: create the account, set initial password and 'User must change password at next logon' flag, place the account in an 'Contractors' OU, and add the account to a security group that grants the proper share access. List exact ADUC steps or the PowerShell commands you would use.
Sample Answer
Direct answer
Both paths, the Active Directory Users and Computers (ADUC) graphical console and PowerShell, do the same four things in the same order: create the account directly inside the Contractors organizational unit (OU) rather than somewhere else and moving it later, set a temporary password with the "user must change password at next logon" flag so the administrator's password is never the contractor's real working credential, enable the account, and finally add it to the existing security group that already carries the needed file-share access, rather than attaching permissions to the individual account.
Structured elaboration
Why create the account directly in the Contractors OU, rather than create it elsewhere and move it. An account's OU location determines which Group Policy Objects (GPOs) apply to it. If the account is briefly created in a default location and moved afterward, it inherits whatever policy applies there first, even if only for a few minutes, which for a contractor OU commonly carries stricter baselines (tighter logon-hour restrictions, contractor-specific security settings) than the default. Setting the location at creation time avoids that gap entirely.
Why the password flag matters. "User must change password at next logon" forces the contractor to set their own password on first sign-in, so the temporary value the administrator typed never becomes the account's actual long-term credential, limiting the window in which two people know the same password to essentially zero.
Why group membership, not a direct permission grant. Adding the account to the security group that already has the file-share access (rather than granting the contractor's individual account a permission entry on the share) keeps access removal simple at offboarding: removing group membership, or disabling the account outright, is enough, instead of hunting down a one-off access control entry tied to a single person on a shared resource somewhere.
Why the order matters. Doing group membership last, after the account is created, correctly placed, and has its real password policy configured, avoids a brief window where an account already has file-share access but is still sitting on a default or predictable password.
Worked example
ADUC steps:
- Open Active Directory Users and Computers (
dsa.msc). - Expand the domain, right-click the "Contractors" OU, choose New, then User.
- Enter the first name, last name, and user logon name (the
sAMAccountName), for examplej.contractor. - Click Next, enter the initial temporary password, check "User must change password at next logon," leave "Account is disabled" unchecked so the account is active, click Next, then Finish.
- Confirm the account now appears under the Contractors OU (it will, since it was created there directly).
- Locate the existing security group that grants the required file-share access, for example "FileShare-ProjectX-ReadOnly," open its Properties, go to the Members tab, click Add, type the contractor's account name, and click OK.
Equivalent PowerShell (ActiveDirectory module):
$SecurePassword = ConvertTo-SecureString "TempP@ssw0rd!23" -AsPlainText -Force
New-ADUser -Name "J. Contractor" `
-SamAccountName "j.contractor" `
-UserPrincipalName "j.contractor@corp.example.com" `
-Path "OU=Contractors,DC=corp,DC=example,DC=com" `
-AccountPassword $SecurePassword `
-ChangePasswordAtLogon $true `
-Enabled $true
Add-ADGroupMember -Identity "FileShare-ProjectX-ReadOnly" -Members "j.contractor"
-Path sets the organizational unit (as a distinguished name) at the moment of creation, which is what avoids the brief-wrong-policy window described above. -AccountPassword takes a SecureString, which is why the plaintext value is first passed through ConvertTo-SecureString; the plaintext literal shown here is only for walkthrough clarity, and in a real rollout the value would be a randomly generated password delivered out of band, never typed into a script. -ChangePasswordAtLogon $true is the exact scripted equivalent of the ADUC checkbox. -Enabled $true is easy to forget: New-ADUser creates the account in a disabled state unless this switch is explicitly set, which otherwise leaves a silently unusable account and a confused first-day helpdesk ticket. Add-ADGroupMember is run last, after the account already has its real password policy and is enabled, so it is never in a state where it holds file-share access without also having its password properly configured.
Trade-offs and pitfalls
The most direct pitfall in the script above is illustrative only: hard-coding a plaintext password, even a temporary one, into a script or ticket is bad practice in a real rollout; the password should be generated randomly and handed to the contractor through an out-of-band channel (verbally, a sealed one-time link, a password manager share), never stored in a script file, ticketing system, or chat log in plaintext. A second common mistake is omitting -Enabled $true and assuming the account works once created, since New-ADUser defaults to a disabled account without it. A third is creating the account somewhere other than the Contractors OU and moving it afterward with Move-ADObject; this works, but it is an extra step that reintroduces the brief-wrong-policy window the direct-creation approach avoids for free. A fourth, easy to overlook because it's not explicit in this walkthrough's steps, is assuming the target group's own permissions on the file share are already correctly scoped to exactly "read-only on this project" before adding the contractor to it; adding someone to a group only grants what that group's own access control entry actually says, so the group's effective permissions are worth a quick check the first time this workflow is used against it, rather than assuming its name matches its actual grant. Finally, a contractor account with no expiration date relies entirely on someone remembering to disable it at contract end; setting -AccountExpirationDate at creation time is a natural hardening step beyond the letter of this question that turns offboarding into something that happens automatically rather than something that depends on a person not forgetting.
You discover a scheduled task on a file server running under a domain user that has not had its password changed in years and is a member of multiple groups. Outline how you'd assess whether this account is a security risk, how you'd rotate credentials safely, and how you'd minimize service disruption during rotation.
Sample Answer
Direct answer
A scheduled task running under a domain account with a password that has never rotated is a genuine risk regardless of what the task does, because that account's credential has had years to leak (into a script, a config file, a backup, or a former administrator's notes) with nobody watching for it. Assessing the risk means finding out exactly what that account can reach (its group memberships, not just its job), and rotating it safely means changing the password and every place that consumes it in the same maintenance window, with the actual scheduled task as the last thing you touch, not the first.
Structured elaboration
1. Assess whether the account is a risk
- Pull the account's full group membership, including nested groups, not just what a directory GUI shows on the first screen. A service account that is a member of "Backup Operators" or a broad file-share access group because someone added it once for convenience is a much bigger risk than one scoped to a single share.
- Check
lastLogon/lastLogonTimestampand password-last-set age. A password untouched for years combined with broad group membership is the classic "nobody remembers why this has access" account. - Search for where the credential is actually used: the Task Scheduler action (the task itself, plus any other scheduled tasks or services on other hosts running under the same account), IIS application pools, SQL Server Agent jobs, and any script or config file with the password embedded in cleartext or a reversible format. Domain controllers and file servers commonly have more than one consumer of the same forgotten account.
- Check for interactive logon rights: does the account have a normal user profile and login history, suggesting a person also uses it, or is it purely a service identity? That changes the blast radius of a compromise and who you need to notify.
2. Rotate the credential safely
- Generate a new, long random password and stage it in a secrets store (or a sealed change-management ticket) before touching anything live.
- Update every consumer identified in step 1 with the new password in the same maintenance window: the scheduled task's "Run as" credential, any other host's scheduled task or service using the same account, and any script with a stored credential. Missing one consumer is the most common cause of a rotation "working" on the file server but silently breaking a second host nobody remembered.
- If the account also has interactive-logon capability, force a re-authentication path (or disable interactive logon entirely if it is not actually needed) so the rotation covers both the service usage and any human usage.
- After rotation, confirm via the domain controller's authentication logs that the account is no longer authenticating with the old password anywhere, which surfaces any consumer you missed.
3. Minimize service disruption
- Rotate outside the task's normal run window, and check the task's schedule and any dependent downstream jobs so you are not rotating credentials moments before the task fires.
- Update the credential in Task Scheduler's "Run as" configuration directly rather than deleting and recreating the task, which preserves the task's history, triggers, and any dependency wiring untouched.
- Do a single test run of the task immediately after rotation (Task Scheduler's "Run" action) to confirm the new credential actually has the permissions the task needs before you consider the change complete, rather than waiting for the next scheduled fire time to find out.
- Keep the old password valid but flagged for revocation for a short overlap window only if you have discovered consumers you have not yet finished updating; otherwise disable it immediately once every known consumer is confirmed updated, since a long overlap window defeats the point of the rotation.
Trade-offs and pitfalls
The single most common failure in this exact scenario is rotating the password everywhere you can find it, declaring victory, and then having a second host's scheduled task or a stored script credential fail hours or days later because it was not on anyone's inventory. Rotating a credential you cannot fully enumerate the consumers of is not actually safe, it just moves the failure later and makes it harder to attribute. If you genuinely cannot find every consumer with confidence, the safer sequence is: create a brand-new dedicated service account scoped to only the permissions this specific task needs, migrate the task to it, and disable (not delete) the old account after a monitored grace period, rather than trying to rotate a credential whose full blast radius is unknown. Also worth flagging to the team: an account with years-old credentials and broad group membership discovered this way is rarely unique; treat this as a signal to audit for siblings, not a one-off fix.
Write (or outline) a PowerShell function that, given a username and a filesystem path, calculates the user's effective NTFS permissions for that path by evaluating direct ACEs, inherited ACEs, and group memberships. The solution should be efficient and account for nested groups. Describe thought process and key API calls or cmdlets used.
Sample Answer
Direct answer
Effective NTFS (Windows' native filesystem, which stores its access control list directly on each file and folder) permission for a user comes down to three steps: expand the user's full group membership including nested groups, gather every access control entry (ACE, one "allow X to do Y" or "deny X from doing Y" rule) that applies to the path, then evaluate those ACEs in Windows' canonical precedence order: explicit Deny beats explicit Allow, and any explicit ACE on the object beats any inherited ACE regardless of how the inherited one arrived. A bit that no ACE ever mentions is denied by default.
Structured elaboration
Step 1: expand nested group membership. A naive script that only checks (Get-ADUser $user -Properties memberOf).memberOf against Active Directory (AD, Windows' enterprise directory service that stores users, groups, and their memberships) misses indirect membership (user in Group A, Group A nested inside Group B). Get-ADPrincipalGroupMembership does not fix this either: it returns only direct membership, with no built-in recursive expansion. Two options actually give you the full transitive closure, the same set of groups Windows itself burns into the user's access token at logon:
- The
tokenGroupsconstructed attribute: a computed (not stored) attribute you can query in a single request, e.g.(Get-ADUser $user -Properties tokenGroups).tokenGroups, which returns every security-group SID (security identifier, the unique ID Windows uses instead of a name) the user carries, nesting already resolved. [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($ctx, $user).GetAuthorizationGroups(), a .NET method with the same fully-expanded semantics, usable directly from PowerShell.
Either is a single, efficient call. Avoid hand-rolled recursive Get-ADGroupMember -Recursive walks over every group in the domain; they are what makes a naive version of this script slow.
Step 2: gather the ACE list. Call Get-Acl $Path (or (Get-Item $Path).GetAccessControl()) exactly once. NTFS does not compute inheritance dynamically at access-check time; when inheritance is enabled, Windows copies each ancestor's inheritable ACEs down onto every descendant's own security descriptor at the time the ACL is set or propagated. That means the single Access collection returned by Get-Acl on the target file already contains both kinds of entry, distinguished by the .IsInherited boolean on each FileSystemAccessRule: explicit ACEs (IsInherited = $false) defined directly on that file, and inherited ACEs (IsInherited = $true) copied down from ancestors. You do not need to manually walk up the parent chain in the common case; you only need the ordering already reflected in that one collection, which Windows keeps canonical (explicit block first, inherited block after, nearer ancestors before farther ones within the inherited block).
Step 3: evaluate in precedence order. For each requested right, walk the ACEs in canonical order and let the first ACE that mentions the intersection of "this right" and "one of the user's resolved principals" decide it. Once a bit is decided it cannot be overridden by a later ACE. This is exactly why explicit Deny is the most dangerous kind of entry: it wins over every inherited Allow no matter how deep the inheritance chain, and it is invisible unless you inspect the target object directly.
Putting it together: the function signature. In production this is a single entry point taking exactly the two inputs the question asks for:
# Production shape (illustrative, not executed here: no live NTFS/AD environment
# is available in this authoring sandbox; the engine it calls IS executed below).
function Get-EffectiveNtfsPermission {
param([string]$UserName, [string]$Path)
# Step 2: real ACE list, both explicit and (already-propagated) inherited entries.
$aces = (Get-Acl -Path $Path).Access | ForEach-Object {
[Ace]::new($_.IdentityReference.Value, [string[]]$_.FileSystemRights.ToString().Split(','),
$_.AccessControlType.ToString(), (-not $_.IsInherited), 0)
}
# Step 1: real nested-membership expansion, e.g. via the tokenGroups attribute.
$membership = (Get-ADUser -Identity $UserName -Properties tokenGroups).tokenGroups
# Step 3: the precedence engine, unchanged from what is executed below.
Get-EffectivePermission -UserName $UserName -AllAces $aces -Membership $membership
}
Get-Acl -Path supplies the real ACE list; tokenGroups supplies the real, transitively-resolved principal set; Get-EffectivePermission is the precedence engine, executed below against pinned, hand-built data standing in for those two real calls. The algorithm is identical either way; only where the inputs come from changes.
Worked example
The script below models this precisely (a self-contained scenario, not a live NTFS volume, so the algorithm is checkable anywhere PowerShell runs): jdoe is nested three levels deep (Interns -> Contractors -> AllStaff). An explicit Deny on the target file blocks Contractors from writing, while an inherited Allow from the parent folder grants AllStaff both Read and Write, and a farther-ancestor inherited Allow grants Everyone Read.
class Ace {
[string]$Principal; [string[]]$Rights; [string]$AceType; [bool]$Explicit; [int]$Level
Ace([string]$p, [string[]]$r, [string]$t, [bool]$e, [int]$l) {
$this.Principal = $p; $this.Rights = $r; $this.AceType = $t; $this.Explicit = $e; $this.Level = $l
}
}
function Expand-GroupMembership {
# Stand-in for tokenGroups / GetAuthorizationGroups(): resolves nested membership.
param([string]$UserName, [hashtable]$Membership)
$principals = New-Object System.Collections.Generic.HashSet[string]
[void]$principals.Add($UserName); [void]$principals.Add('Everyone')
$queue = New-Object System.Collections.Generic.Queue[string]
$queue.Enqueue($UserName)
$visited = New-Object System.Collections.Generic.HashSet[string]
while ($queue.Count -gt 0) {
$current = $queue.Dequeue()
if (-not $visited.Add($current)) { continue } # cycle guard
foreach ($groupName in $Membership.Keys) {
if ($Membership[$groupName] -contains $current) {
if ($principals.Add($groupName)) { $queue.Enqueue($groupName) }
}
}
}
return $principals
}
function Get-EffectivePermission {
param([string]$UserName, [Ace[]]$AllAces, [hashtable]$Membership)
$principals = Expand-GroupMembership -UserName $UserName -Membership $Membership
# Canonical order: explicit Deny, explicit Allow, then inherited Deny/Allow nearest-level-first.
$ordered = @()
$ordered += $AllAces | Where-Object { $_.Explicit -and $_.AceType -eq 'Deny' }
$ordered += $AllAces | Where-Object { $_.Explicit -and $_.AceType -eq 'Allow' }
$levels = ($AllAces | Where-Object { -not $_.Explicit } | ForEach-Object { $_.Level } | Sort-Object -Unique)
foreach ($lvl in $levels) {
$ordered += $AllAces | Where-Object { -not $_.Explicit -and $_.Level -eq $lvl -and $_.AceType -eq 'Deny' }
$ordered += $AllAces | Where-Object { -not $_.Explicit -and $_.Level -eq $lvl -and $_.AceType -eq 'Allow' }
}
$decided = @{}; $trace = @()
foreach ($ace in $ordered) {
if (-not $principals.Contains($ace.Principal)) { continue }
foreach ($right in $ace.Rights) {
if (-not $decided.ContainsKey($right)) {
$decided[$right] = $ace.AceType
$scope = if ($ace.Explicit) { 'explicit' } else { "inherited(level $($ace.Level))" }
$trace += " $right -> $($ace.AceType) via $scope ACE on principal '$($ace.Principal)'"
}
}
}
$effective = @('Read','Write','Execute','Delete','ChangePermissions','TakeOwnership') |
Where-Object { $decided.ContainsKey($_) -and $decided[$_] -eq 'Allow' }
[pscustomobject]@{ User=$UserName; Principals=($principals -join ', '); Effective=($effective -join ', '); Trace=$trace }
}
$Membership = @{ 'Interns' = @('jdoe'); 'Contractors' = @('Interns'); 'AllStaff' = @('Contractors') }
$Aces = @(
[Ace]::new('Contractors', @('Write'), 'Deny', $true, 0) # explicit, on the file itself
[Ace]::new('AllStaff', @('Read','Write'), 'Allow', $false, 1) # inherited from parent folder
[Ace]::new('Everyone', @('Read'), 'Allow', $false, 2) # inherited from grandparent
)
$result = Get-EffectivePermission -UserName 'jdoe' -AllAces $Aces -Membership $Membership
Write-Output "User: $($result.User)"
Write-Output "Resolved principal set (direct + nested groups): $($result.Principals)"
Write-Output "Effective rights: $($result.Effective)"
Write-Output "Decision trace:"
$result.Trace | ForEach-Object { Write-Output $_ }
Output (actually run with pwsh, unmodified):
User: jdoe
Resolved principal set (direct + nested groups): jdoe, Everyone, Interns, Contractors, AllStaff
Effective rights: Read
Decision trace:
Write -> Deny via explicit ACE on principal 'Contractors'
Read -> Allow via inherited(level 1) ACE on principal 'AllStaff'
jdoe ends up with Read only. Despite AllStaff (which jdoe reaches through two levels of nesting) being granted Write by inheritance, the explicit Deny on Contractors (also reached through nesting) wins, because explicit always outranks inherited regardless of level. This is the exact trap a naive "just check if any Allow ACE matches" script misses: it would wrongly report Write access.
Complexity. With g total groups in the directory and d the nesting depth, tokenGroups/GetAuthorizationGroups() resolve membership in one call rather than the O(g⋅d) a hand-rolled recursive walk would cost. ACE evaluation itself is O(a⋅p) for a ACEs on the path and p resolved principals (a simple membership test per ACE), which is small in practice (single-digit to low-double-digit ACE counts per object). Edge cases: a broken inheritance chain (-Protected set somewhere in the ancestry) truncates the inherited block below that point, an ACE that names a right the algorithm never evaluates should be ignored, and a user removed from AD mid-session still holds their old token's groups until re-authentication, so a live effective-permission check and a currently-open session's actual access can legitimately disagree.
Trade-offs and pitfalls
The single biggest pitfall is treating "is there an Allow ACE that matches" as sufficient; every explicit Deny anywhere on the object must be checked first, and it is easy to miss because it does not show up when you only look at inherited permissions in the folder's parent. The second is under-resolving group membership: memberOf and Get-ADPrincipalGroupMembership both under-report nested access, which silently produces false "access denied" conclusions.
Policy-simplification recommendation. The scenario above is also a demonstration of why relying on scattered explicit Deny ACEs is bad policy design, not just hard to audit: this one Deny only exists because someone needed a narrow, ad hoc exception to an otherwise-correct inherited Allow. The structural fix is to remove the need for the Deny at all, by restructuring group membership (pull the users who need the exception out of the inheriting chain into a separate security group scoped to a sub-folder with its own, narrower inherited Allow) rather than layering a Deny on top. Microsoft's own long-standing guidance is the same: prefer restructuring group nesting or removing a principal from the granting group over adding a Deny ACE, because every explicit Deny added is one more rule a future admin (or this exact algorithm) has to evaluate first, forever, to get the right answer.
Describe best practices for UID/GID planning across a fleet of Linux servers and shared resources (NFS, Ceph). How would you reserve ranges for system accounts, service accounts, human users, and containerized workloads to avoid collisions? Explain how you would document, enforce, and audit these assignments at scale.
Sample Answer
Direct answer
Reserve non-overlapping UID/GID ranges by IDENTITY CLASS, not by team or project: one range for OS/system accounts (typically already fixed low, below 1000, by the distribution itself), one for human users, one for service accounts, and one dedicated range for containerized workloads, each large enough to never realistically run out and each recorded in one authoritative source of truth that every host, NFS export, and Ceph cluster reads from rather than assigns independently.
Structured elaboration
Why collisions happen without explicit ranges
Linux and most NFS setups (NFSv3, and NFSv4 without Kerberos) identify a file's owner purely by a numeric UID, not a name; two different systems that independently assign "the next free UID" with no coordination will eventually assign the same number to two different identities. On a single host this is invisible until you share storage: mount the same NFS export or Ceph filesystem from two hosts that assigned UID 2001 to different people, and each host's user now silently has read/write access to the other's files, with no error and no obvious symptom until someone notices unexpected data.
A practical range design
- System/service accounts created by packages: leave alone, these are already managed by the distribution's own low, reserved range.
- Human users: a wide range (for example 10000 to 59999) assigned centrally, one UID per person for their entire tenure, never reused after they leave.
- Application/service accounts: a separate range (for example 60000 to 69999) so a service account's UID can never collide with a human user's, even accidentally.
- Containerized workloads: a distinct range again, especially important if any workload uses Linux user namespaces, since a namespaced container's remapped UID (via subuid/subgid) needs its own reserved block so it cannot collide with a real human or service UID on the underlying host.
Documenting, enforcing, and auditing at scale
- Documenting: a single authoritative registry (an LDAP/AD schema, or a version-controlled allocation file if identity is not centralized) that is the ONLY place a new UID/GID gets assigned from; nobody hand-picks a number locally.
- Enforcing: centralize identity through LDAP or an equivalent directory wherever possible so hosts resolve the same UID for the same identity by construction rather than by convention; where local accounts are unavoidable, a pre-commit or provisioning-time check that rejects any UID outside its assigned range class.
- Auditing at scale: a periodic fleet-wide sweep (for example
getent passwdon every host, or a query against the central directory) that flags any UID appearing with two different usernames, any UID outside its assigned range, and any gap in the expected range utilization that suggests drift between the documented registry and what hosts actually have.
Trade-offs and pitfalls
The pitfall that causes real incidents is treating UID/GID assignment as a per-host or per-project decision made independently, which works fine until the day two teams share an NFS export or migrate onto shared Ceph storage and discover their ranges overlap. The fix is establishing the range design and the single source of truth BEFORE shared storage exists, since retrofitting non-overlapping ranges onto a fleet that already has widespread collisions means renumbering live UIDs on running systems and re-chowning potentially enormous amounts of existing data, which is a much more disruptive, higher-risk project than reserving the ranges correctly from the start.
Unlock Full Question Bank
Get access to all 37 Identity, Authentication, and Access Management interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.