Helpdesk¶
The helpdesk lets operators reset a password, unlock an account, and disable or enable an account, only on the accounts their delegation covers. It also powers account creation (Onboarding and leavers), several bulk actions and LAPS.
It is off by default. While it is off, the helpdesk endpoints do not exist, the menu entries are hidden and the password service does not run.
How it works¶
Changing a password or an account's enabled state needs strong rights in AD. Steward keeps those rights out of the console: they belong to a dedicated password-reset account that only a separate container, the password service, knows.
operators ──HTTPS──> steward ──(internal network "helpdesk", HMAC-signed)──> steward-pwd ──LDAPS──> DCs
read + write accounts password-reset account only
/data (read-write), /secrets /data (READ-ONLY), no /secrets
Both containers run from the same image:
steward (console) |
steward-pwd (password service) |
|
|---|---|---|
| Command | default | password-service |
| AD accounts | read, write-attributes | password-reset only (and the optional LAPS account) |
| Secrets | master key and Data Protection keys (/secrets), link key |
link key, reset account's password, its pinned LDAPS CA |
| Trust anchors | the DCs, CA and managed OUs stored by the setup wizard | its own pins (DCs, CA, managed OUs, Tier OUs, sensitive groups) from .env and secrets/ on the host |
| Database | read/write | mounted read-only: domain name, VIP groups, and the settings it compares with its pins |
| Network | published HTTPS port; helpdesk network |
helpdesk network (internal) and an egress network for LDAPS; no published port |
The console never holds the reset account's password, and the password service cannot read the master key or the other accounts.
The link. Every request from the console carries an HMAC-SHA256 signature made with a shared link key, a timestamp (±60 seconds) and a nonce that is never accepted twice. Any failure gets a plain 401. A new password travels encrypted (AES-256-GCM) inside the request body.
Pinned trust anchors. The password service signs in to AD by simple bind with the reset (and LAPS) account: the server it
connects to receives that password. So it never takes the DCs, the LDAPS CA or the managed OUs from the console's database, which
a compromised console could rewrite. They are pinned on the host (.env and secrets/pwd-ca.pem of the instance, see
Pin the password service). It connects to the pinned DCs only, trusts the pinned CA only, and when
the database names other DCs, another CA or other managed OUs it refuses every operation (trust_mismatch), before any bind.
Two checks before anything changes. The console authorizes each request: delegation, VIP and tier gates, a recent step-up for a reset, a disable or an enable, and an explicit confirmation (never the operator's own account for disable / enable). Then the password service checks again, so a compromised console alone is not enough:
- the console's configuration matches its pins (DCs, CA, managed OUs);
- the target, looked up by GUID, is under a pinned managed OU;
- it is still at the DN the console decided on;
- the shared classifier says it is a standard account (privileged or unclassifiable targets are refused);
- a per-target rate limit: at most 5 operations per 10 minutes, shared by reset, unlock, disable and enable
(
PasswordService:MaxOperationsPerTarget,PasswordService:RateWindow).
Finally AD checks the reset account's own rights. A password never appears in a log, an audit row or an error. Each operation is audited by the console and logged as one JSON line on the password service's output.
Turn the helpdesk on¶
- Create the reset account in AD: a dedicated, standard user account (not the read or write account), with a long random password that does not expire. Do not add it to any group.
-
Install or update the instance with the helpdesk. The guided install asks for it ("Enable the helpdesk?", then the account's UPN and its password, typed hidden). With options:
sudo install -m 600 /dev/null /root/steward-pwd.txt && sudo vi /root/steward-pwd.txt # the password, one line sudo deploy/install.sh --name acme --hostname steward.acme-client.ch \ --helpdesk --reset-upn svc-steward-reset@corp.acme-client.ch --reset-password-file /root/steward-pwd.txt sudo shred -u /root/steward-pwd.txtThe script:
- generates the link key once (32 random bytes) in
<instance>/secrets/pwd-link; - copies the password to
<instance>/secrets/ad-pwd-reset; - makes both files mode
0600, owned by the container user, in a0700directory; - writes
COMPOSE_PROFILES=helpdesk,STEWARD_HELPDESK_ENABLED=trueand the reset UPN to<instance>/.env; - starts both containers and waits until both are healthy.
Re-running the script keeps the link key, the password and the helpdesk state. Pass
--reset-password-fileagain only to change the password. - generates the link key once (32 random bytes) in
-
Give operators the actions. In Delegations, give the operators who need them a delegation with
users.password.reset,users.unlock,users.disableand/orusers.enable(users.disabledoes not implyusers.enable). -
Apply the AD rights. In Configuration > Diagnostics, export the AD delegation and have a domain administrator apply it (Delegation model and AD rights). For the reset account, on each OU of those delegations, it adds:
- the Reset Password extended right and Write property on
pwdLastSet("must change at next logon"), forusers.password.reset; - Write property on
lockoutTime, forusers.unlock; - Write property on
userAccountControl, forusers.disable/users.enable(user objects only, see below).
The script refuses a privileged reset account, and a reset account that is also the write account. 5. Pin the password service once the setup wizard is completed (domain, DCs, LDAPS CA, managed OUs): see Pin the password service. Until then every reset, unlock, disable, enable and LAPS read answers not pinned (
trust_not_pinned). 6. Check the diagnostics. With the helpdesk on, they add: - Password service: reachable, and the reset account signs in; - per OU:pwdLastSet/lockoutTimewritable by the reset account (read fromallowedAttributesEffective); - per OU: Reset Password right present. AD offers no read-only way to prove an extended right is effective, so this line says "present, not proven". The proof is a real reset on a test account. - the Reset Password extended right and Write property on
Pin the password service¶
The password service's trust anchors and scope are pinned on the host, by an administrator, never by the console:
sudo deploy/install.sh --name acme --pin-password-service
It reads the console's current configuration (read-only, with the password service image), shows it for review: the DCs, the SHA-256 fingerprint of the LDAPS CA, each managed OU with its DN and GUID, the Tier OUs and the extra sensitive groups, and asks for confirmation (in a non-interactive run the option itself is the confirmation). Then it writes them to the instance:
| Pin | Where | Password service setting |
|---|---|---|
| Domain (DNS name; fixes the naming context, LDAPS port 636 only) | .env: STEWARD_PWD_DOMAIN |
PasswordService:DomainFqdn |
| Domain controllers | .env: STEWARD_PWD_DC_FQDNS |
PasswordService:DcFqdns |
| LDAPS CA | secrets/pwd-ca.pem (Docker secret of steward-pwd only) |
PasswordService:CaPemFile |
| Managed OUs (objectGUIDs) | .env: STEWARD_PWD_MANAGED_OU_GUIDS |
PasswordService:ManagedOuGuids |
| Tier OUs (objectGUIDs) | .env: STEWARD_PWD_TIER_OU_GUIDS |
PasswordService:TierOuGuids |
| Extra sensitive groups (SIDs) | .env: STEWARD_PWD_SENSITIVE_SIDS |
PasswordService:SensitiveGroupSids |
and recreates steward-pwd. Compare what is shown with what you configured: pinning values that someone else stored would hand
them the pins. You can also give the four required pins yourself, e.g. before the setup wizard:
--pwd-domain corp.acme-client.ch --pwd-dcs "dc1.corp.acme-client.ch dc2.corp.acme-client.ch" --pwd-ca-file ldaps-ca.pem --pwd-managed-ous <guid>,<guid>.
Run it again after every change of domain, LDAPS port, DCs, LDAPS CA or managed OUs on the console's Configuration page: until then the
password service refuses to work (trust_mismatch), the operators see the directory configuration differs from the one pinned
on the server, the audit row says failed / trust_mismatch, and the service logs which part differs (domain, port, dcs, ca,
managed_ous). Tier OUs and sensitive groups added in the console apply at once (they only protect more); removing one takes
effect in the password service after the next pin.
Reset a password¶
- Find the account: Users, or press Ctrl+K (⌘K on a Mac) anywhere and type a name or login (see Find the caller fast).
- Verify the caller before anything else and record how (How did you verify the caller?): callback on a number already known (the directory number shown next to the choices, never the one the caller gives), confirmation by the manager (shown when you may read it), in person with an identity document, or Other (then explain it in the ticket). Only the method is stored in the audit. Required by default (Configuration > Helpdesk).
- Choose the new password:
- Random (16 characters, the strongest), or
- Passphrase: four common words and three digits, for example
Tigre-Jardin-Piano-Nuage-472(about 48 bits): easy to dictate, always to be changed at the next sign-in. The words follow your console language; - or type one (AD's password policy applies).
- Enter the ticket (
INC-12345, "call from the user"...), required if the instance says so. Never put a password or personal data in it. - Confirm. A step-up is asked when the last one is older than 5 minutes: AD operators type a code from their authenticator app
(set up once on the Security page); Teleport operators are not asked (their Teleport session is the step-up, audited
stepup:teleport). - The result shows the password once, large and grouped. Spell it reads it in the radio alphabet; Copy the message copies a ready-to-send text (editable first) with the password and the must-change notice, or without the password if the instance turned that off (then dictate it). Send it only over a channel you trust.
Unlock an account and find the lockout source¶
Unlock (profile header, users.unlock) clears the lockout.
Before unlocking, find what keeps locking the account: on a locked (or failing) profile, the Lockout source card asks
each domain controller for its bad-password counter and time (these are not replicated between DCs). The DC with the most
recent failure is the one nearest to the device that still uses the old password: phone mail profile, mapped drive, saved RDP or
Wi-Fi credentials, scheduled task or service. Fix that device, then unlock. Needs users.read and users.unlock on the
account. A DC that does not answer within a few seconds is shown as such.
Disable and enable an account¶
Disable on a profile blocks the account's sign-in; Enable lifts it. Nothing else changes (password, groups and
attributes stay). Both need a delegation with the action, a recent step-up and an explicit confirmation, and exist only while the
helpdesk is on (the profile says so otherwise). A disabled account that is also a console operator loses its console sessions at
once. Audited as users.disable / users.enable with the state before and after.
To close an account completely (disable, random password, expiry, groups...), use the leaver flow.
Why it goes through the password service. Write property on userAccountControl lets its holder set ANY account flag,
for example "Kerberos pre-authentication not required" (AS-REP roasting) or "password not required". AD has no right for "only
the disable bit". So the right goes to the reset account in the isolated steward-pwd container, never to the console's write
account (the delegation script refuses that), and the service:
- reads
userAccountControlas the reset account, by GUID, at the DN the console decided on; - computes the new value with ONLY the disable bit (0x2) changed, and refuses anything else;
- writes it as a compare-and-set: one modify that deletes the value read and adds the new one. If anything changed meanwhile, AD refuses it and the console reports changed in the meantime; nothing is written. An account already in the wanted state is left alone.
Find the caller fast¶
- Press Ctrl+K (⌘K on a Mac), or use the search box of the top bar, anywhere in the console. Type a name or login: the
palette lists the users you may read (the same authorized search as the Users page), your recently viewed users, groups (with
groups.read) and pages. ↑ ↓ move, Enter opens, → (or Tab) on a user lists the actions the server allows on that user: open, unlock, reset, disable / enable, expiry date, history. Esc goes back, then closes. - An action from the palette opens the profile and the same dialog as the profile button: confirmation, ticket and step-up are never skipped.
- The recently viewed list (palette and dashboard) is kept in this browser only, per operator (at most 10 accounts) and is erased at sign-out.
Tickets on every change¶
Reset, unlock, disable / enable, expiry date, profile edits and group membership changes accept a ticket (at most 100 characters, one line). It is stored with the audit row, searchable on the Audit page, shown in the profile's History and in the CSV export. With Ticket required for helpdesk actions (Configuration > Helpdesk), the console refuses those changes without one.
Helpdesk settings¶
Configuration > Helpdesk (settings.manage, identity confirmation, audited settings.helpdesk):
- ticket required;
- caller verification required (default on) and the methods offered;
- generated password proposed by default (random or passphrase);
- password included in the ready-to-send message (default on).
Move or rename an account¶
These use the write account, not the password service: they work with the helpdesk off.
- Move (profile header,
users.move): pick the destination among the OUs offered (only those you may move this account into). If an object with the same name already exists there, the console proposes a free name such as "Jane Doe (2)"; accepting it needsusers.rename. Group memberships follow the account (AD keeps them by reference). - Rename (profile header,
users.rename): change the name shown in AD (cn), the display name, first and last name; the preview shows the resulting DN. Changing the sign-in name (sAMAccountName, UPN) needsusers.rename.loginand a step-up: tell the person their new sign-in name, and remember that saved credentials, mapped drives and some applications use the old one. - Groups (
groups.create,groups.attributes.write,groups.delete): New group on the Groups page; Edit / Delete on a group. Deleting a group that still has members lists them first and asks for confirmation. A delegation withgroups.owned.members.writelets a person manage the members of the groups whose manager they are (useful for team leads).
More on moves, renames and groups: Operations.
Temporary group membership¶
Adding a member (Groups page, a user's Groups tab, a bulk Add to group, or the groups of an onboarding template) accepts an optional until date and time. It uses the write account, not the password service: it works with the helpdesk off.
- Bounds: at least one minute ahead, at most 365 days (
TemporaryMemberships:MaxDays, 1 to 3650). A date out of bounds is refused before anything is decided or written (until_too_soon,until_too_far). - Where it is kept: in the console's database (group and member by objectGUID, expiry, who added it, ticket), never in AD.
The add is audited with the expiry (
membership.until). An "until" on someone who already is a member changes nothing: a permanent membership is never turned into a temporary one behind your back. - What you see: an "until ..." badge on the member (group page) and on the group (user's Groups tab), and the list of
upcoming expirations (Groups > Temporary memberships). Change date and Make permanent need exactly what adding the
member needs (
groups.members.writeon the group, orgroups.owned.members.writeon a group you manage, and the member gate); both are audited with the old and new date. -
At expiry: a background service (every minute,
TemporaryMemberships:CheckInterval) removes the member through the same checked path as a manual removal, on fresh facts, with the write account. The removal is audited asgroups.members.remove, providersystem, attributed to the operator who added it, with the basis:operator_grant: that operator's current delegation still covers the change;temporary_grant: it no longer does (role changed, operator disabled or deleted, group or member moved out of their scope...): the member is removed anyway, because taking back a temporary grant only reduces access. The audit row says why.
A failed removal (AD refused, DC unavailable...) is retried after 1 min, 5 min, 15 min, 1 h, then every 6 h, and notified (
temporary_membership_failed) after 3 attempts (TemporaryMemberships:NotifyAfterAttempts). A success is notified astemporary_membership_expired. - Removed elsewhere: removing the member through the console closes the record. If someone removes the member directly in AD, the record is closed at the next presence check (every 15 minutes,TemporaryMemberships:PresenceInterval), audited asgroups.members.temporary.closed. If the console is down at the expiry time, the removal happens when it is back. - Why not AD's own TTL: forests with the Privileged Access Management optional feature can hold a membership with a TTL in AD itself. Enabling that feature is irreversible and Samba does not support it, so Steward does not use it: temporary memberships work on every supported DC and stay visible, changeable and audited in one place.
Turn the helpdesk off¶
sudo deploy/install.sh --name acme --hostname steward.acme-client.ch --no-helpdesk
The script stops and removes the password service and turns the feature off in the console. The secret files stay in
<instance>/secrets; delete them if you do not plan to turn the helpdesk on again. Then remove the reset account's rights in AD
(or disable the account): the delegation script never removes rights.
Good to know¶
- Old password grace (Windows): after a reset, a Windows DC may keep accepting the previous password for a short time (NTLM/Kerberos grace period). This is AD behavior, not a failed reset.
- Uncertain outcome: if the password service changed the password but its answer was lost (timeout, restart), the console
shows Outcome uncertain and the audit row says
unknown/outcome_unknown. A generated password is still displayed, because AD may already hold it. Check the user before resetting again. - Rate limit: each account accepts a limited number of resets, unlocks, disables and enables per window. Attempts that AD refused (password policy, missing right, directory unavailable) do not count.
Residual risks¶
| Risk | Mitigation | Remaining exposure |
|---|---|---|
| Console compromised | The service re-checks OU, DN, tier and rate with its own pinned DCs, CA and scope; AD checks the reset account's rights | An attacker can reset passwords of, disable or enable standard accounts in the pinned managed OUs, within the rate limit, until detected. The service log and the audit show every operation. It cannot set any other account flag, point the service at another LDAPS server (the reset password would leak in the bind) or widen its scope: a changed database is refused |
| Reset account password stolen (root on the host) | File permissions; no published port | With Write property on userAccountControl, the holder can set any flag on accounts of the delegated OUs (e.g. no pre-authentication). Grant users.disable / users.enable only where needed; monitor userAccountControl changes in AD |
| Link key stolen | Only useful from inside the helpdesk network; replay refused |
Rotate it (below) |
| Root on the host | File permissions | Root can read every secret, including the reset account's password |
Write account password stolen, users.create granted |
Create user objects only on the delegated OUs; OWNER RIGHTS removes the creator's implicit permission changes (Windows) | The holder can create accounts there. See Onboarding |
| Reset Password right present but not effective (deny ACE, AdminSDHolder) | Diagnostics say "present, not proven"; AD refuses and the console reports ad_denied |
Found at the first reset |
Protected accounts (adminCount=1) |
Classified privileged: always refused, by the console and by the service | None by design |
Rotation¶
- Reset account password: change it in AD, write it to a root-only file, then re-run
install.shwith--reset-password-file <file>. The password service restarts with the new password. Run the diagnostics. - Link key: delete
<instance>/secrets/pwd-link(as root) and re-runinstall.shwith--helpdesk. A new key is generated and both containers restart with it.