Skip to content

Delegation model and AD rights

Steward lets operators change Active Directory only where you delegated it. Two independent checks stand between an operator and an AD object, and both must agree:

flowchart LR
    op["Operator<br/>(AD account or Teleport user)"] --> c{"1. Steward decides<br/>role → delegation:<br/>action + attribute + OU"}
    c -- denied --> x1["Refused, audited"]
    c -- allowed --> ad{"2. AD decides<br/>ACEs of the service account<br/>(Grant-StewardDelegation.ps1)"}
    ad -- denied --> x2["Refused by AD, reported"]
    ad -- allowed --> ok["Change made, audited"]
  1. Steward decides. A request is allowed only when one single delegation covers the action, the attribute (for attribute changes) and the target's OU, all at once. Everything else is denied, and any doubt (unreadable object, unresolved group, internal error) is a denial too.
  2. AD decides. Steward never writes with an administrator account. It uses dedicated, non-privileged service accounts whose AD rights (ACEs) cover exactly what your roles need. A domain administrator applies those rights with a script; Steward itself can never change permissions in AD. If the two disagree, AD refuses and the console says so, and the diagnostics show the gap before anyone tries.

So even a stolen administrator session of the console cannot reach more than the AD rights you granted, and a mistake in the AD rights cannot be used without a matching delegation.

Building blocks

Concept What it is Where
Managed OUs The outer boundary of the instance, set in the setup wizard. Nothing outside them is ever reachable. Configuration › Directory
Role A named set of application rights and delegations, assigned to operators. Delegations › Roles
Delegation One self-contained grant: actions + attributes (if any) + scope (OUs). Never merged with another delegation. Inside a role
Operator A person allowed to sign in: an AD account (first.last@domain or DOMAIN\user) or a Teleport user name. A new operator has no role, so no rights. Delegations › Operators
Teleport mapping Gives a role automatically to the Teleport users who have a given Teleport role (and optional trait). Delegations › Teleport (Teleport)

Scope

A delegation's scope is a list of OUs from the managed OUs (up to 100), each one:

Option Covers
This OU only Objects directly in the OU.
OU and sub-OUs The whole subtree.
Exclude this OU / Exclude OU and sub-OUs Removes those objects from this delegation only.

OUs are stored by GUID: renaming or moving an OU in AD does not break a delegation. For group actions, the scope is where the groups live; the members added must also be users the operator may view.

Exclusions exist only in Steward

An exclusion is enforced by the console. The AD rights are set on the OU and cover all of it: AD cannot express "this OU except that sub-OU" for inherited rights.

Actions

Directory actions are granted per delegation:

Action (code) Label in the console Notes
users.read Search and view Basis of every user action.
users.attributes.read Read attributes Choose the attributes.
users.attributes.write Change attributes Choose the attributes. Write account.
users.password.reset Reset password Helpdesk only (password service).
users.unlock Unlock account Helpdesk only.
users.disable / users.enable Disable / Enable an account Helpdesk only.
users.create Create an account (template) Needs users.read and the helpdesk (first password, enable). See Onboarding.
users.move Move an account to another OU Both the account's OU and the destination must be in scope.
users.rename Rename an account (name, display name)
users.rename.login Change the login (sAMAccountName, UPN) Only together with users.rename.
vip.manage VIP override Lets the other actions of the same delegation reach VIP accounts. Never alone; never reaches privileged accounts.
groups.read View groups
groups.members.write Manage group members Needs groups.read.
groups.owned.members.write Change members of groups the operator manages (managedBy) Only groups of the scope whose managedBy is the operator's own AD account.
groups.create / groups.delete Create / Delete a group Never a privileged, protected or built-in group.
groups.attributes.write Edit group properties description, managedBy, mail, info.
computers.read View computers Never the password.
computers.laps.read / computers.laps.expire Reveal / Expire the LAPS password Need computers.read and the password service. See LAPS.

Attributes come from a fixed catalog: givenName, sn, displayName, title, department, company, description, mail, telephoneNumber, manager, accountExpires, thumbnailPhoto (photo), extensionAttribute1 to 15 (when the schema has them), and the read-only lastLogonTimestamp. There is deliberately no "write everything" action.

A delegation can also grant some actions with approval (reset, unlock, disable, enable, attribute changes, group members): the operator then files a request that another operator approves. See Approval requests.

Application rights

Granted per role, not tied to OUs: audit.read (View audit), delegations.read (View delegations), delegations.manage (Manage delegations), settings.manage (Manage configuration), accessreviews.manage (access reviews).

The Administrators role created by the setup wizard has exactly settings.manage, delegations.manage, delegations.read and audit.read, and no delegation: administering the console gives no rights on AD objects. The last delegation administrator can never be removed or disabled, and changing your own roles always asks for confirmation.

The Super administrator role

A built-in, read-only role: every application right and every directory action (VIP override included) on every managed OU and its sub-OUs. It is computed at each decision, so new managed OUs, attributes and actions are covered at once. Privileged accounts stay out of reach like for any role. Granting or removing it always asks for confirmation (step-up) and is audited; the setup wizard offers it to the first administrator as an unchecked option. Keep it for one or two named people; give narrow roles for daily work.

What is never reachable

Whatever the roles say, the engine refuses:

  • Privileged accounts (Tier 0/1): members (direct or nested) of the built-in privileged groups (Domain Admins, Enterprise Admins, Schema Admins, Administrators, Account/Server/Backup/Print Operators, Replicator, Group Policy Creator Owners, the domain controller groups, Key Admins, Enterprise Key Admins, Cert Publishers, Incoming Forest Trust Builders, Hyper-V Administrators, Storage Replica Administrators, DnsAdmins and, when present, the Exchange groups Exchange Windows Permissions, Exchange Trusted Subsystem and Organization Management) or of the additional privileged groups, accounts with adminCount=1, service accounts (with an SPN), accounts below a Tier 0/1 OU, and anything whose classification is uncertain. Manage the lists in Configuration › Account protection.
  • VIP accounts (members of VIP groups), unless the delegation includes the VIP override.
  • Objects outside the managed OUs, read-only attributes, and the operator's own AD account for group membership, reset, unlock, disable and enable.

The target's facts are read from AD at every sensitive operation; nothing sent by the browser is trusted. Privileged accounts also cannot sign in to the console.

Designing roles

  1. Delegations › Roles › New role. Name it after a job (Helpdesk L1, HR, Client power users).
  2. Add one delegation per coherent grant: for example Search and view + Change attributes (title, department, telephoneNumber) on OU=Users and its sub-OUs, excluding OU=Board.
  3. Delegations › Operators › Add an operator, then Roles… to assign roles (AD login or Teleport user name; privileged accounts are refused). Teleport users can also get roles from mappings.
  4. Delegations › Simulator: choose an operator, a target user (or group), an action and an attribute, and see the real engine's answer: allowed or denied, the reason, the role that grants it, the target's classification, and whether AD would allow the service account too. "The application would allow it, but Active Directory would refuse" means the AD delegation must be exported and applied again. Simulations change nothing and are audited.

Roles can also be imported from a JSON file on the host (automation), with the same validation as the console: docker exec <container> dotnet Steward.Api.dll import-roles /tmp/roles.json [--replace] (see Operations).

Apply the AD rights

The console computes, from the current roles, the exact AD rights its service accounts need, and exports them. A domain administrator applies them with deploy/delegation/Grant-StewardDelegation.ps1.

  1. In the console: Configuration › Diagnostics › Export AD delegation. This downloads steward-delegation.json (no passwords; only identities, OUs, rights and attributes).
  2. Give the file and the deploy/delegation/ folder to a domain administrator. On a machine with Windows PowerShell 5.1 and the RSAT ActiveDirectory module, preview:

    $ou  = 'OU=Staff,DC=corp,DC=example'                     # every managed OU (Configuration > Directory)
    $ids = @{ read  = 'svc-steward-read@corp.example'        # every service account of the instance
              write = 'svc-steward@corp.example'
              reset = 'svc-steward-reset@corp.example' }     # + laps = '...' when LAPS is used
    .\Grant-StewardDelegation.ps1 -ModelPath .\steward-delegation.json -ManagedOu $ou -Identity $ids -TierOu 'OU=Tier0,DC=corp,DC=example' -WhatIf
    
  3. Apply (the script prints the resolved plan and asks for confirmation first):

    .\Grant-StewardDelegation.ps1 -ModelPath .\steward-delegation.json -ManagedOu $ou -Identity $ids -TierOu 'OU=Tier0,DC=corp,DC=example'
    

    Parameters: -ModelPath (required, the exported file), -ManagedOu (required: the instance's managed OUs, DNs or objectGUIDs), -Identity (required: role → account, roles read, write, reset, laps; UPN, sAMAccountName or DOMAIN\sAMAccountName), -TierOu (optional: your administrative-tier OUs, refused with everything above and below them), -ReportPath (CSV report; default .\steward-delegation-report-<timestamp>.csv), -Server (a DC to use; default: the AD module's choice), and the standard -WhatIf / -Confirm (ConfirmImpact=High: it asks before the plan and before each OU; -Confirm:$false for automation).

    The file is not trusted. Type -ManagedOu and -Identity from what you configured, not from the file: they must match the model exactly (same OUs by objectGUID, same roles, same accounts by SID, both ways), otherwise nothing changes. A tampered file that adds an OU or swaps an account is refused.

  4. Back in the console, Configuration › Diagnostics › Run diagnostics: every line should be green. The diagnostics read the effective rights from AD and change nothing.

The script writes a CSV report every time, also with -WhatIf. Each line names the OU, the identity, the right, the attribute and an action: Added, WouldAdd (preview or declined), Present (already there, left alone), Info (for example an OU renamed since the export), Skipped (OU not found).

What the script grants

Only what the roles use, on the OUs of their scopes, inherited by the right kind of object:

Service account Rights When a delegation has
Write account Write property on each delegated attribute (user objects) users.attributes.write
Write account Write property on member (group objects) groups.members.write, groups.owned.members.write
Write account Create user objects on the OU + an OWNER RIGHTS ACE (read permissions only) users.create
Write account Create user objects, Delete on user objects, Write property on cn and name users.move
Write account Write property on cn, name, displayName, givenName, sn (+ sAMAccountName, userPrincipalName) users.rename (+ users.rename.login)
Write account Create group objects (+ OWNER RIGHTS), Delete on group objects, Write property on description, managedBy, mail, info groups.create, groups.delete, groups.attributes.write
Reset account Reset Password and Write property on pwdLastSet; lockoutTime; userAccountControl helpdesk on, and users.password.reset; users.unlock; users.disable/users.enable (also users.create)
LAPS account Read of msLAPS-Password (and legacy ms-Mcs-AdmPwd); Write property on the LAPS expiration time (computer objects) computers.laps.read; computers.laps.expire

The read account gets nothing: authenticated users can read user objects by default. On a hardened domain, grant read access to the read account on the managed OUs yourself. The full reference, with the reasoning behind each right, is in deploy/delegation/README.md.

What the script never does

  • Remove or change existing ACEs: it only adds the missing ones, so running it twice changes nothing.
  • Delegate on an OU holding privileged objects: a managed OU whose sub-tree holds an adminCount=1 object, a domain controller or RODC account, a sensitive group or a (nested) member of one is refused, as is a managed OU at, above or below the Domain Controllers OU, the System container (AdminSDHolder) or any -TierOu. Delete is granted on the objects (never Delete child on the OU), so an object protected later can never be deleted or moved by the write account.
  • Accept a privileged service account: every identity is checked first, and an account with adminCount=1, a non-empty sIDHistory, or a member (even nested, or through its primary group) of any group the console classifies as sensitive (Domain/Enterprise/Schema Admins, Administrators, Account/Server/Print/Backup Operators, Replicator, Cert Publishers, Key Admins, DC groups, DnsAdmins and any principal that can write the DNS containers, the Exchange groups found by name and by the Exchange well-known objects, ...) is refused. The reset account must differ from the write account; the LAPS account must differ from the read, write and reset accounts.
  • Grant outside the instance: the model (steward.delegation/3) lists the managed OUs (which must match -ManagedOu), and a grant on any other OU is refused. Whether an OU is below another is decided by AD (objectGUID subtree search) and by parsed RDNs, never by comparing DN text. Older /1 and /2 files are refused: export again.
  • Grant an attribute outside its hard allow-list, whatever the file says: the write account only the console's catalog attributes plus cn, name, sAMAccountName, userPrincipalName (users) and member, description, managedBy, mail, info (groups); the reset account only pwdLastSet, lockoutTime, userAccountControl and Reset Password. So msDS-KeyCredentialLink, servicePrincipalName, altSecurityIdentities, scriptPath... are never granted.
  • Run on Samba: it needs Windows Server and the ActiveDirectory module.

When to export and apply again

The diagnostics compare the model of the last export with the model the current settings produce, and say "Roles changed since the export…: export and apply the AD delegation again" as soon as they differ. Typical cases:

  • a role gains an action, an attribute or an OU. A removal also changes the model, but applying it is harmless: the script only adds rights, and Steward stops using a removed right at once. Remove the now-unused ACE in AD yourself if you want it gone;
  • the helpdesk is enabled (reset, unlock, disable, enable, account creation);
  • LAPS actions are delegated;
  • the managed OUs change, or someone receives the Super administrator role.

Until the new file is applied, AD refuses the changes the service accounts lack, the simulator shows "AD would refuse", and the diagnostics list each missing right per OU (for example "AD refuses writing title (apply the AD delegation)").