Adds a docs/ folder under the repo root with full operator documentation aimed at sysadmins (not webhook developers). The Zerto pre/post script recipe is the canonical "why does this exist" walkthrough; the GitHub HMAC, AD password reset, and UI-on-desktop recipes round out common patterns. Pages: - README.md (index) - concepts.md (5-minute "what is a webhook" explainer) - installation.md (interactive + silent install) - upgrading.md (single-click upgrade flow + edge cases) - uninstalling.md (clean removal + wiping ProgramData) - runas-modes.md (Service / InteractiveUser / SpecificUser decision flow) - service-account-and-ad.md (gMSA setup, delegated rights) - network-and-security.md (bind addresses, allowlists, HTTPS, secret storage) - troubleshooting.md (symptom -> first check, common errors) - recipes/zerto-pre-post-scripts.md (canonical use case) - recipes/github-style-hmac.md (GitHub / Stripe-shaped webhooks) - recipes/ad-password-reset.md (gMSA-backed self-service reset) - recipes/ui-on-desktop.md (InteractiveUser pattern) Top-level README.md restructured to point at docs/ as the source of truth, dropping the duplicated installation snippets. Installer ships docs/ alongside the binaries so they're available offline at C:\Program Files\WebhookServer\docs\. GUI Help menu gains a "Documentation" item that opens the docs site in a browser. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.5 KiB
Recipe: AD password reset endpoint
A self-service password reset URL your help-desk tool can hit. Single endpoint, gMSA-backed, audited.
Architecture
- The webhook host is domain-joined
- The service runs as a gMSA with Reset Password + Write pwdLastSet delegated on the OUs containing target users
- The endpoint is HMAC-signed, IP-allowlisted to the help-desk app's server
- Every reset is logged in the daily log file with caller IP, target user, runId, and result
Prerequisites
- gMSA created and installed on the host. See Service account & Active Directory.
- Service installed with
-ServiceAccount 'CONTOSO\svc-webhookserver$' - Delegate the right permissions on the OU(s):
$ou = "OU=Standard Users,DC=contoso,DC=local"
dsacls $ou /I:S /G "CONTOSO\svc-webhookserver$:CA;Reset Password;user"
dsacls $ou /I:S /G "CONTOSO\svc-webhookserver$:WP;pwdLastSet;user"
The script
C:\Scripts\ad-password-reset.ps1:
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
Import-Module ActiveDirectory
$body = $input | ConvertFrom-Json
if (-not $body.samAccountName) { throw 'samAccountName is required' }
if (-not $body.newPassword) { throw 'newPassword is required' }
if (-not $body.requestedBy) { throw 'requestedBy is required (audit field)' }
# Refuse to touch privileged groups
$user = Get-ADUser -Identity $body.samAccountName -Properties MemberOf
$denyGroups = @('Domain Admins','Enterprise Admins','Schema Admins')
foreach ($g in $user.MemberOf) {
$name = ($g -split ',')[0] -replace '^CN='
if ($denyGroups -contains $name) {
throw "refusing to reset password for member of $name"
}
}
$secure = ConvertTo-SecureString $body.newPassword -AsPlainText -Force
Set-ADAccountPassword -Identity $user -NewPassword $secure -Reset
Set-ADUser -Identity $user -ChangePasswordAtLogon $true
# Audit line goes to the webhook log automatically (return value becomes stdout).
"reset $($user.SamAccountName) requested by $($body.requestedBy)"
Endpoint configuration
| Section | Setting | Value |
|---|---|---|
| Identity | Slug | ad-reset |
| Auth | Mode | HMAC with a strong secret shared with the help-desk app |
| Auth | HMAC header | X-Signature-256 |
| Auth | HMAC prefix | sha256= |
| Auth | HMAC encoding | hex |
| Allowed clients | 10.50.10.20 (the help-desk app's IP only) |
|
| Executor | Type | Windows PowerShell |
| Executor | Script path | C:\Scripts\ad-password-reset.ps1 |
| Data passing | JSON body to stdin | ✓ |
| Data passing | Headers/query as env vars | ✗ |
| Run as | Identity | Service (uses the gMSA) |
| Response | Mode | Sync |
| Response | Timeout (sec) | 30 |
| Response | Fail on non-zero exit | ✓ |
Calling it
$body = @{
samAccountName = 'jdoe'
newPassword = 'TempP@ssw0rd!2026'
requestedBy = 'helpdesk_user@contoso.local'
} | ConvertTo-Json
$bytes = [Text.Encoding]::UTF8.GetBytes($body)
$hmac = [Security.Cryptography.HMACSHA256]::new(
[Text.Encoding]::UTF8.GetBytes('your-shared-secret'))
$sig = ([BitConverter]::ToString($hmac.ComputeHash($bytes)) -replace '-','').ToLower()
Invoke-RestMethod -Method POST `
-Uri 'http://webhooks.contoso.local:8080/hook/ad-reset' `
-Headers @{ 'X-Signature-256' = "sha256=$sig" } `
-ContentType 'application/json' -Body $body
Operational notes
Audit log: every call lands in C:\ProgramData\WebhookServer\logs\webhook-YYYYMMDD.log with one line per run including the runId, slug, caller IP, exit code, and the script's stdout (the "reset jdoe requested by helpdesk_user" line). Ship those logs to your SIEM via the usual file-collector flow.
Rotating the HMAC secret: edit the endpoint in the GUI, replace the secret, save. The help-desk app needs the new secret too — coordinate the cutover. There's no overlap window built in; if you need a soft rollover, create a second endpoint with the new secret and switch caller traffic over.
Privileged-group guard: the script's denyGroups check is a basic guard. If a more sophisticated guard is needed (target user attribute, OU-based logic), add it in the script — that's the right place, not the webhook server.
Self-service from the user side: don't expose this endpoint to end users directly. Front it with a help-desk app that authenticates the user (preferably with MFA), then makes the call to the webhook with its bearer/HMAC credentials. The webhook server is the plumbing; not the front door.