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.8 KiB
Recipe: GitHub-style HMAC-signed webhook
GitHub, Stripe, Slack, Shopify, and most SaaS providers sign their outbound webhooks with HMAC. The receiver computes the same HMAC over the request body using a shared secret and rejects the request if the signatures don't match. Webhook Server has this built in — you just point a real GitHub webhook at your endpoint.
What we're building
A webhook URL that GitHub calls on every push to a repo. The server runs a PowerShell script that pulls the latest commit and triggers a deployment. Authentication is HMAC-SHA256 over the request body, using the secret you configured in GitHub's webhook settings.
On the GitHub side
In your repo: Settings → Webhooks → Add webhook.
| Field | Value |
|---|---|
| Payload URL | https://hooks.contoso.com/hook/gh-deploy (yes, HTTPS — GitHub enforces it for public hosts) |
| Content type | application/json |
| Secret | Generate a long random string. Copy it for the next step. |
| SSL verification | Enable |
| Events | Just push |
Save. GitHub immediately delivers a ping event for testing. You'll see it in Recent Deliveries with whatever response code your server returns.
The PowerShell deployment script
C:\Scripts\gh-deploy.ps1:
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$payload = $input | ConvertFrom-Json
# Verify the event type via the X-GitHub-Event header passed as an env var
$event = $env:WEBHOOK_HEADER_X_GITHUB_EVENT
if ($event -eq 'ping') {
"got ping from $($payload.repository.full_name)"
return
}
if ($event -ne 'push') {
Write-Error "ignoring $event event"
}
$repo = $payload.repository.full_name
$branch = $payload.ref -replace '^refs/heads/', ''
$sha = $payload.after
if ($branch -ne 'main') {
"ignoring push to $branch"
return
}
$repoDir = "C:\Deploys\$($payload.repository.name)"
if (-not (Test-Path $repoDir)) {
git clone "https://github.com/$repo.git" $repoDir
}
Push-Location $repoDir
try {
git fetch --all
git reset --hard $sha
# ...your build/deploy steps here...
& npm ci
& npm run build
Restart-Service MyAppService
}
finally {
Pop-Location
}
"deployed $repo @ $sha"
Configure the endpoint
File → New endpoint:
| Section | Setting | Value |
|---|---|---|
| Identity | Slug | gh-deploy |
| Auth | Mode | HMAC |
| Auth | HMAC secret | paste the GitHub-side secret |
| Auth | HMAC header | X-Hub-Signature-256 (GitHub's default) |
| Allowed clients | 140.82.112.0/20, 192.30.252.0/22 (GitHub's webhook IP ranges; check docs.github.com for the live list) |
|
| Executor | Type | Windows PowerShell |
| Executor | Script path | C:\Scripts\gh-deploy.ps1 |
| Data passing | JSON body to stdin | ✓ |
| Data passing | Headers/query as env vars | ✓ (needed so WEBHOOK_HEADER_X_GITHUB_EVENT is set) |
| Run as | Identity | Service (default) — assumes the deployment is local |
| Response | Mode | Async (GitHub times out fast; don't make it wait for the build) |
| Response | Timeout (sec) | 600 |
Save.
What HMAC does for you here
GitHub computes sha256(body, secret) and sends it as sha256=<hex> in X-Hub-Signature-256. Webhook Server computes the same hash, verifies in fixed time, and rejects (401) on mismatch.
This means:
- A request with a tampered body fails the check
- A captured request can be replayed verbatim (the signature is valid for that body) — if that matters, GitHub also includes a
X-GitHub-DeliveryID and timestamp you can deduplicate against - The secret never travels over the network — only the digest does, so HTTPS is for confidentiality of the body, not the secret
Adapting for Stripe, Slack, etc.
Same pattern, different headers and signing details. The four HMAC fields in the editor cover all common variants:
| Provider | Header | Prefix | Encoding | Algorithm |
|---|---|---|---|---|
| GitHub | X-Hub-Signature-256 |
sha256= |
hex | SHA-256 |
| Stripe | Stripe-Signature |
(none — but Stripe's format is multipart, see below) | hex | SHA-256 |
| Slack | X-Slack-Signature |
v0= |
hex | SHA-256 |
| Generic / custom | configurable | configurable | configurable | SHA-1 / SHA-256 / SHA-512 |
Stripe is special: their Stripe-Signature header has the format t=<timestamp>,v1=<sig>,v0=<sig>, where v1 is HMAC-SHA256 of <timestamp>.<body>. Webhook Server's straight HMAC check doesn't match Stripe's signed-with-timestamp scheme. Workarounds:
- Use Bearer auth on Stripe webhooks instead, since you already control the secret
- Or do unauthenticated + IP allowlist + a script-side signature check using their official validation library
For everything that's "GitHub-shaped" (signed body, raw HMAC), the built-in HMAC mode is the right pick.