Documentation: install/upgrade/uninstall guides + recipes incl. Zerto

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>
This commit is contained in:
2026-05-08 10:34:47 -04:00
parent 7d94535d5d
commit c49a2a12cb
17 changed files with 1477 additions and 87 deletions
+66 -87
View File
@@ -1,111 +1,90 @@
# webhook-server # Webhook Server
A Windows-native webhook server that runs PowerShell, PowerShell Core, cmd / `.bat`, or arbitrary executables in response to incoming HTTP requests. Endpoints are configured in a desktop GUI; the actual server runs as a Windows Service so it survives reboots and works without anyone logged in. A Windows-native webhook server that runs PowerShell, cmd / `.bat`, or any executable in response to incoming HTTP requests. Endpoints are configured in a desktop GUI; the actual server runs as a Windows Service so it survives reboots and works without anyone logged in.
**Status:** planning complete, implementation pending. See [PLAN.md](PLAN.md) for the full design. Designed for sysadmins who want to wire up tools like **Zerto pre/post scripts**, GitHub webhooks, monitoring alerts, or backup jobs to Windows-side automation — without writing a custom listener every time.
## Quickstart
1. **Download** the latest installer: <https://github.com/recklessop/webhook-server/releases/latest>
2. **Run it.** UAC accept → next, next, finish. Adds a Start Menu entry, registers and starts the Windows Service.
3. **Open Webhook Server** from the Start Menu (auto-elevates).
4. **File → New endpoint**, configure a slug + script, save, hit the URL.
Full first-time walkthrough: [docs/installation.md](docs/installation.md)
## Highlights ## Highlights
- **Many endpoints, one service.** Each webhook is a configured URL slug mapped to a script or command. - **Many endpoints, one service.** Each webhook is a configured URL slug mapped to a script or command.
- **Per-endpoint auth.** Pick HMAC signature (GitHub/Stripe-style), bearer token, or none. - **Per-endpoint auth** HMAC signature (GitHub / Stripe / Slack style), bearer token, or none.
- **Per-endpoint IP allowlist.** Restrict by IP or CIDR (IPv4 + IPv6). Empty list = open. Checked before auth. - **Per-endpoint IP allowlist.** Restrict by IP or CIDR. Empty list = open. Checked before auth so blocked IPs get a fast 403.
- **Per-endpoint Run As** — run the hook as the service account (default), the user logged in at the keyboard (for UI hooks), or a named domain/local user via password.
- **Flexible execution.** Windows PowerShell 5.1, PowerShell 7+, cmd / `.bat`, or any `.exe`. - **Flexible execution.** Windows PowerShell 5.1, PowerShell 7+, cmd / `.bat`, or any `.exe`.
- **Flexible input.** Any combination of: JSON body to stdin, query/headers as env vars, `{{template}}` arg expansion. - **Flexible input** — any combination of: JSON body to stdin, query / headers as env vars, `{{body.foo.bar}}` template expansion into argv.
- **Sync or async per endpoint.** Sync returns exit code + stdout/stderr; async returns 202 immediately. - **Sync or async per endpoint.** Sync returns exit code + stdout / stderr to the caller; async returns 202 immediately.
- **Outbound callbacks.** Optional per-endpoint callback URL service POSTs the run result back after the script finishes. Required for async callers who want to know what happened. HMAC-signed, retried with backoff. Pre-configured only (no SSRF surface from caller-supplied URLs). - **Outbound callbacks.** Optional per-endpoint URL the service POSTs run results to after the script finishes. HMAC-signed, retry-with-backoff. Required for async callers who want to know what happened.
- **Service-first.** Always-on Windows Service. The WPF GUI is a thin config/monitor client over a named pipe. - **Configurable network** — bind to specific NICs, set the URL host shown in the GUI, configure trusted reverse proxies.
- **HTTPS optional.** Bind a `.pfx` or cert-store thumbprint from the GUI; HTTP works out of the box. - **HTTPS optional.** Bind a `.pfx` or cert-store thumbprint from the GUI.
- **Secrets at rest.** Tokens and HMAC secrets are encrypted via DPAPI (LocalMachine scope) in `config.json`. - **Secrets at rest** — bearer tokens, HMAC keys, RunAs passwords, and PFX passwords are DPAPI-encrypted (LocalMachine scope) in `config.json`.
- **Auto-snapshots.** Every config save writes a Config Checkpoint; restore to any point with one click. Last 30 retained.
## Architecture ## Architecture
``` ```
+------------------+ named pipe +------------------------------+ +------------------+ named pipe +-------------------------------+
| WPF GUI app | <----------> | Windows Service | | GUI (WPF) | <-------------> | Windows Service |
| (config/monitor)| | - Kestrel: webhook listener | | add / edit / | SYSTEM+admin | - Kestrel: hook listener |
+------------------+ | - Named-pipe admin server | | view logs | ACL'd | - Admin pipe server |
| - Executor pool | +------------------+ | - Executor (process runner) |
| - Serilog file logging | | - Callback dispatcher |
+------------------------------+ | - Serilog file logging |
^ +-------------------------------+
C:\ProgramData\WebhookServer\ |
- config.json (DPAPI-encrypted secrets) C:\ProgramData\WebhookServer\
- logs\*.log - config.json (DPAPI-encrypted)
- backups\ (auto-snapshots)
- logs\ (daily rolling)
``` ```
## Project layout (planned) ## Documentation
``` Everything you need to operate the server:
WebhookServer.sln
src/ - [Concepts](docs/concepts.md) — what a webhook is and how this server uses one
WebhookServer.Core/ class lib: models, auth, execution, storage, IPC - [Installation](docs/installation.md) — interactive and silent install
WebhookServer.Service/ .NET 8 Worker Service (hosts Kestrel + admin pipe) - [Upgrading](docs/upgrading.md) — single click; what's preserved
WebhookServer.Gui/ WPF (.NET 8) MVVM config/monitor client - [Uninstalling](docs/uninstalling.md) — clean removal
scripts/ - [Run As modes](docs/runas-modes.md) — Service / InteractiveUser / SpecificUser
install-service.ps1 - [Service account & Active Directory](docs/service-account-and-ad.md) — gMSA + delegated rights
uninstall-service.ps1 - [Network & security](docs/network-and-security.md) — bind addresses, allowlists, HTTPS, secrets
``` - [Troubleshooting](docs/troubleshooting.md) — common errors and where to look
Recipes:
- [Zerto pre/post scripts → AD / DNS update](docs/recipes/zerto-pre-post-scripts.md) ← **canonical use case**
- [GitHub-style HMAC-signed webhook](docs/recipes/github-style-hmac.md)
- [AD password reset endpoint](docs/recipes/ad-password-reset.md)
- [Pop UI on the user's desktop](docs/recipes/ui-on-desktop.md)
## Requirements ## Requirements
- Windows 10 / 11 or Windows Server 2019+ - Windows 10 / 11 / Server 2019+
- .NET 8 SDK to build, .NET 8 Runtime (or self-contained publish) to run - x64
- Administrator rights to install the service and to run the GUI (the admin named pipe is ACL'd to SYSTEM + Administrators) - .NET 8 SDK to build (the released installer includes everything else)
## Building (on Windows) ## Building from source
```powershell ```powershell
dotnet restore git clone https://github.com/recklessop/webhook-server.git
dotnet build -c Release cd webhook-server
dotnet publish src/WebhookServer.Service -c Release -r win-x64 --self-contained
dotnet publish src/WebhookServer.Gui -c Release -r win-x64 --self-contained # Dev install (publishes + copies to C:\Program Files\WebhookServer + registers service)
powershell -ExecutionPolicy Bypass -File scripts\deploy.ps1
# Or build the installer locally (requires Inno Setup 6: winget install JRSoftware.InnoSetup)
powershell -ExecutionPolicy Bypass -File scripts\build-installer.ps1
``` ```
## Installing the service (on Windows)
```powershell
# from an elevated PowerShell prompt
sc.exe create WebhookServer binPath= "C:\Program Files\WebhookServer\WebhookServer.Service.exe" start= auto
sc.exe start WebhookServer
```
`scripts/install-service.ps1` will wrap this once implemented and will accept a `-ServiceAccount` parameter.
## Service account & Active Directory
The service runs as `LocalSystem` by default — fine for local-only scripts and read-only AD queries (it authenticates to the domain as the computer account). If your webhook scripts need to **modify** AD (password resets, group changes, etc.), run the service under an account with the right delegated rights:
- **Recommended: gMSA** — Active Directory generates and rotates the password automatically.
```powershell
# on a DC, once
New-ADServiceAccount -Name svc-webhookserver -DNSHostName host.domain.local `
-PrincipalsAllowedToRetrieveManagedPassword "DOMAIN\WebhookHosts"
# on the webhook host
Install-ADServiceAccount svc-webhookserver
sc.exe create WebhookServer binPath= "..." obj= "DOMAIN\svc-webhookserver$" start= auto
```
Note the trailing `$` and the absence of `password=`.
- **Plain domain user** — works on older domains, but you own password rotation:
```powershell
sc.exe create WebhookServer binPath= "..." obj= "DOMAIN\svc-webhookserver" password= "..." start= auto
```
Don't use `LocalService` — it has no network identity and cannot talk to a domain controller.
> Heads up: any account the service runs under is the account your hook scripts run under. `LocalSystem` is the most powerful local account on the machine — treat webhook script contents as privileged.
## Configuration
The service reads `C:\ProgramData\WebhookServer\config.json`. Edit it through the GUI rather than by hand — the GUI handles DPAPI encryption of secrets and validation of IP allowlist entries.
## Out of scope for v1
- Importing/exporting config across machines (DPAPI LocalMachine scope ties decryption to the host).
- Per-endpoint rate limiting.
- Multi-user RBAC for the GUI.
- Auto-update.
## License ## License
Not yet chosen. TBD.
+31
View File
@@ -0,0 +1,31 @@
# Webhook Server documentation
Webhook Server is a Windows service that runs a script (PowerShell, cmd, or any executable) when an HTTP request hits a URL you choose. It's designed for sysadmins who want to wire a tool like **Zerto pre/post scripts**, GitHub Actions, a monitoring system, or a backup tool into a Windows-side automation step — without writing a custom listener every time.
## New here? Start with these
1. [Concepts](concepts.md) — five-minute read on what a webhook is and how this server uses one
2. [Installation](installation.md) — download, install, first endpoint
3. [Recipe: Zerto pre/post scripts → AD / DNS update](recipes/zerto-pre-post-scripts.md) — the canonical reason this exists
## Topical
- [Upgrading](upgrading.md)
- [Uninstalling](uninstalling.md)
- [Run As modes — when to use which](runas-modes.md)
- [Service account & Active Directory](service-account-and-ad.md)
- [Network & security](network-and-security.md)
- [Troubleshooting](troubleshooting.md)
## Recipes (cookbook style)
- [Zerto pre/post scripts → AD / DNS update](recipes/zerto-pre-post-scripts.md)
- [GitHub-style HMAC-signed webhook](recipes/github-style-hmac.md)
- [AD password reset endpoint](recipes/ad-password-reset.md)
- [Pop UI on the user's desktop](recipes/ui-on-desktop.md)
## Reference
- [GitHub repo](https://github.com/recklessop/webhook-server)
- [Latest release](https://github.com/recklessop/webhook-server/releases/latest)
- [Issue tracker](https://github.com/recklessop/webhook-server/issues)
+77
View File
@@ -0,0 +1,77 @@
# Concepts
If you've never used a webhook before, this is where to start. Five minutes, no surprises.
## What is a webhook?
A webhook is just **an HTTP URL that runs something when it gets called.** Some other tool — Zerto, GitHub, your monitoring system, a backup job — does an `HTTP POST` to that URL when an event happens. Whatever's listening on the URL takes that request and does work in response.
Concretely, a Zerto pre-script might do:
```powershell
Invoke-WebRequest -Method POST -Uri http://webhooks.contoso.local:8080/hook/start-failover `
-Body (@{ vmName = $env:ZertoVPGName } | ConvertTo-Json) `
-ContentType application/json
```
…and the server at `webhooks.contoso.local:8080` would receive that POST and run a PowerShell script you wrote.
## What does this server give you that you don't already have?
You could write a tiny ASP.NET listener, or run a PowerShell script behind IIS, or hand-craft `HttpListener` plumbing. People do, all the time. The trade-off is that **you then own the listener** — auth, retries, logging, restarts, a service wrapper, secret storage, an admin UI. That's where Webhook Server saves you a weekend.
What you get out of the box:
- A real **Windows Service** that survives reboots and runs without anyone logged in
- Per-endpoint **authentication**: Bearer token, HMAC-signed (GitHub / Stripe / Slack style), or none
- Per-endpoint **IP allowlist** (single IPs or CIDR ranges)
- **Run-as identity**: the service runs as `LocalSystem` by default, but each individual hook can run as a domain account, the logged-in user, or whoever — without needing Task Scheduler in the middle
- **Logging** (Serilog, daily-rolling files) plus a GUI tail
- A WPF **GUI** for adding / editing / testing endpoints. No JSON file editing required.
- **Outbound callbacks**: when a hook finishes, the server can POST the result to another URL, signed with HMAC, with retry-and-backoff
- **HTTPS** via `.pfx` or a cert thumbprint from the local cert store
- **Auto-snapshots** of your config on every save, with point-in-time restore from the GUI
## How the moving parts fit together
```
+------------------+ named pipe +-------------------------------+
| GUI (WPF) | <------------> | Windows Service |
| add / edit / | SYSTEM+admin | - Kestrel: hook listener |
| view logs | ACL'd | - Admin pipe server |
+------------------+ | - Executor (process runner) |
| - Callback dispatcher |
| - Serilog file logging |
+-------------------------------+
|
C:\ProgramData\WebhookServer\
- config.json (DPAPI-encrypted secrets)
- backups\ (auto-snapshots)
- logs\ (daily rolling)
```
- The **Windows Service** does the actual work: listens for HTTP requests, runs your scripts, writes logs.
- The **GUI** is purely a config + monitoring tool. It talks to the service over a named pipe ACL'd to `SYSTEM` and `Administrators`. You can launch and close the GUI as you like; the service keeps running.
- **Config + secrets** live in `C:\ProgramData\WebhookServer\config.json`. Secrets (bearer tokens, HMAC keys, run-as passwords, PFX passwords) are DPAPI-encrypted with the `LocalMachine` scope, so the same machine can decrypt them under any account but they don't travel to other machines.
## What's an "endpoint"?
An endpoint is one URL slug (the part after `/hook/`) plus a configuration: who's allowed to call it, how it's authenticated, what to run when it fires, and what to do with the result. Add as many as you want.
| Field | What it controls |
|---|---|
| **Slug** | The URL path. `deploy``http://host:8080/hook/deploy` |
| **Auth** | None / Bearer / HMAC. None means anyone who can reach the URL can fire it. |
| **Allowed clients** | List of IPs or CIDRs allowed to hit this slug. Empty = anyone reachable. |
| **Executor** | What to run: Windows PowerShell 5.1, PowerShell Core (7+), `cmd` / `.bat`, or a path to any `.exe` |
| **Run As** | Who the script runs as. See [Run As modes](runas-modes.md). |
| **Data passing** | How request data reaches the script — JSON to stdin, headers / query as env vars, `{{template}}` arg expansion |
| **Response mode** | Sync (the HTTP caller waits for the script to finish and gets its output) or Async (returns 202 immediately, runs in background) |
| **Callback** | Optional outbound URL the server POSTs to with the run result. Required for async hooks if the original caller wants the result. |
## What it isn't
- **Not an HTTP server for serving static files or pages.** Just hook URLs and a `/healthz`.
- **Not a queue.** No durable persistence of inbound requests; if the service crashes mid-execution that run is lost (the inbound caller will see the connection drop or a timeout).
- **Not multi-tenant.** It's one config, one set of endpoints, one machine. Run multiple instances on different ports / different machines if you need separation.
- **Not an internet-facing public-API server out of the box.** Lock down with HTTPS + auth + IP allowlist + a reverse proxy if you're going to expose it publicly. See [network & security](network-and-security.md).
+108
View File
@@ -0,0 +1,108 @@
# Installation
This page covers a fresh install. If you already have Webhook Server installed, see [Upgrading](upgrading.md). To remove it, see [Uninstalling](uninstalling.md).
## Requirements
- Windows 10, Windows 11, or Windows Server 2019 / 2022 / 2025
- Administrator rights to install the service and to run the GUI
- (Optional, only if you publish from source) .NET 8 SDK
The installer is **x64 only**. There is no x86 build.
## 1. Download
Grab the latest installer from the GitHub Releases page:
> https://github.com/recklessop/webhook-server/releases/latest
Look for the asset named `WebhookServer-Setup-X.Y.Z.exe`.
## 2. Run the installer
Double-click the `.exe`. UAC will prompt — accept. The wizard:
- Copies the binaries to `C:\Program Files\WebhookServer\`
- Creates a Start Menu folder named **Webhook Server** with a GUI shortcut + Uninstall shortcut
- Optionally creates a desktop shortcut (checkbox; off by default)
- **Registers the Windows Service** named `WebhookServer`, runs it as `LocalSystem`, sets it to start automatically at boot, and configures it to restart on failure
- Starts the service
- Offers to launch the GUI when finished — leave the checkbox ticked
The first time the GUI opens, you'll see UAC prompt again because the GUI requires elevation (it talks to the service over a named pipe restricted to `SYSTEM` and the `Administrators` group). Accept it.
If the GUI's status bar shows a green dot and `Connected — HTTP 8080`, you're done.
> **Default ports**: HTTP on `8080`, HTTPS off. Both can be changed under **Server → Settings**. Port `8080` is rarely in use on a fresh server but conflicts with some other tools — if you see `Connection refused` later, this is the first thing to check.
## 3. Add your first endpoint
In the GUI:
1. **File → New endpoint**
2. Slug: `ping`
3. Auth → Mode: **None**
4. Executor → Type: **Windows PowerShell**
5. Executor → Inline command: `Write-Output 'pong'`
6. Click **Save**
The endpoint appears in the grid. Right-click it → **Copy URL**, paste into a browser. You should get back something like:
```json
{ "runId": "...", "exitCode": 0, "durationMs": 134, "stdout": "pong\r\n", ... }
```
That's it. Real-world recipes start with [Zerto pre/post scripts → AD / DNS update](recipes/zerto-pre-post-scripts.md).
## Silent / unattended install
For deploying to many machines via Group Policy, SCCM, Intune, Ansible, etc. — the installer is built with [Inno Setup](https://jrsoftware.org/isinfo.php) and supports its standard silent-mode flags:
```powershell
WebhookServer-Setup-0.1.1.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
```
Useful flags:
| Flag | What it does |
|---|---|
| `/SILENT` | Show progress, no questions |
| `/VERYSILENT` | No UI at all |
| `/SUPPRESSMSGBOXES` | Suppress info / error popups (use with `/SILENT` or `/VERYSILENT`) |
| `/NORESTART` | Don't restart automatically — there's nothing here that needs it, but pair with `/SUPPRESSMSGBOXES` for total quiet |
| `/DIR="C:\Tools\WebhookServer"` | Override the install location |
| `/LOG="C:\Temp\install.log"` | Write a verbose installer log |
| `/TASKS="desktopicon"` | Pre-tick the optional desktop-icon task |
The post-install service install runs the same `install-service.ps1` script regardless of silent flags.
## Manual install from source (if you don't want to trust the prebuilt installer)
```powershell
# clone (or your fork)
git clone https://github.com/recklessop/webhook-server.git
cd webhook-server
# from an elevated PowerShell:
powershell -ExecutionPolicy Bypass -File scripts\deploy.ps1
```
`deploy.ps1` publishes both projects, copies the binaries to `C:\Program Files\WebhookServer\`, registers the service, and starts it. Re-run after a `git pull` to upgrade.
To run the service under a non-default account (e.g. a gMSA for AD operations), pass `-ServiceAccount`:
```powershell
.\scripts\deploy.ps1 -ServiceAccount 'CONTOSO\svc-webhookserver$'
```
See [Service account & Active Directory](service-account-and-ad.md) for the full picture.
## Where things live after install
| Path | What |
|---|---|
| `C:\Program Files\WebhookServer\` | Binaries (`WebhookServer.Service.exe`, `WebhookServer.Gui.exe`, the icon, install/uninstall scripts) |
| `C:\ProgramData\WebhookServer\config.json` | The configuration. Backups in `backups\`, daily-rolling logs in `logs\`. **Don't edit by hand** — secrets are DPAPI-encrypted and the service won't pick up your changes without a reload. Use the GUI. |
| `\\.\pipe\WebhookServerAdmin` | The named pipe the GUI uses to talk to the service. ACL'd to `SYSTEM` + `Administrators` only. |
The installer never touches `C:\ProgramData\WebhookServer\`. Uninstalling preserves your config and logs by default; see [Uninstalling](uninstalling.md) for how to wipe them too.
+131
View File
@@ -0,0 +1,131 @@
# Network & security
This page covers what's exposed by Webhook Server, how to lock it down, and what's safe to change vs. leave alone.
## What's listening
By default the service binds Kestrel to **all interfaces on TCP 8080**. There are two endpoints relevant to outsiders:
- `GET|POST /hook/<slug>` — fires a configured endpoint
- `GET /healthz` — returns `{"ok": true}` for monitoring
- `GET /favicon.ico` — returns 204 to keep browser logs clean
Plus the admin named pipe `\\.\pipe\WebhookServerAdmin`, which is **only available locally** to processes running as SYSTEM or in the Administrators group.
## Reducing the network exposure
### Bind only to specific NICs
By default the server listens on every IP the host has — useful on a single-NIC desktop, dangerous on a multi-NIC server where one NIC faces the internet.
In the GUI: **Server → Settings → Network**. Untick "Listen on all interfaces" and tick the specific addresses you want. Save. The service restarts automatically and rebinds.
Common patterns:
- **Internal-only**: tick the LAN IP(s), leave loopback ticked too if anything on the box itself calls the hook
- **Loopback-only**: tick `127.0.0.1` and `::1`. Useful when a reverse proxy on the same host fronts the public traffic.
- **One specific IP for hooks**: tick a single IP that you've documented as the webhook endpoint. Helps when you have a multi-homed server and want clear network segmentation.
### Per-endpoint IP allowlist
Each endpoint has an **IP allowlist** field. Empty means anyone reachable can call it. Non-empty means deny-by-default — only the listed IPs / CIDRs are allowed:
```
192.168.1.0/24
10.42.0.5
fd00::/8
```
Mixing IPv4 and IPv6 entries is fine. The check runs **before authentication**, so a blocked IP gets a fast 403 without burning CPU on HMAC validation.
### Trusted proxies (X-Forwarded-For)
If the server sits behind a reverse proxy (nginx / IIS / Caddy / Cloudflare Tunnel), the inbound `RemoteIpAddress` will always be the proxy. To make the IP allowlist evaluate the original client instead, configure **Server → Settings → Trusted proxies** with the IP(s) of the proxy:
```
10.0.0.5
```
When the inbound connection comes from that IP and includes an `X-Forwarded-For` header, the leftmost entry of the header is treated as the effective client IP for the allowlist check.
If `Trusted proxies` is empty (default), `X-Forwarded-For` is **ignored entirely**. This is the safe default — it prevents anyone from spoofing their IP by adding the header themselves.
## Authentication options
| Mode | When to use | What the caller sends |
|---|---|---|
| **None** | Internal-only on a trusted LAN, or a hook that's safe to fire repeatedly with no side effects | Nothing |
| **Bearer** | Simple authentication. Pick a long random secret and treat it as a password. | `Authorization: Bearer <secret>` |
| **HMAC** | Anything where the body matters and you want tamper-evidence: GitHub webhooks, Stripe events, signed callbacks | A header (default `X-Hub-Signature-256`) containing `sha256=<hex digest>` of the request body keyed by your shared secret |
For **None**, lean hard on the IP allowlist — that's your only defense.
For **Bearer**, generate the secret with `[Convert]::ToBase64String((1..32 | %{ Get-Random -Maximum 256 }))` or any password manager. 32+ bytes of entropy. The token sits in `Authorization` headers; HTTPS is **strongly recommended** so it doesn't traverse the network in clear text.
For **HMAC**, the secret never traverses the network — only the digest does. This is what GitHub / Stripe / Slack use, and it's the right pick for inbound webhooks from internet-facing services. Configure the four fields to match the sender:
- **Algorithm**: usually SHA256
- **Header name**: e.g. `X-Hub-Signature-256` (GitHub), `X-Slack-Signature` (Slack), `Stripe-Signature` (Stripe — needs different format)
- **Prefix**: `sha256=` for GitHub-style, none for raw hex
- **Encoding**: hex (most senders) or base64 (some Slack-derived implementations)
## HTTPS
HTTP-only is fine for fully-internal use. For anything reachable beyond a trusted LAN, enable HTTPS.
In **Server → Settings → HTTPS**:
- **PFX file**: path to a `.pfx` and its password. Easiest if you got a cert from your internal CA or generated a self-signed one with `New-SelfSignedCertificate`.
- **Cert store thumbprint**: the SHA-1 thumbprint of a certificate already imported into `LocalMachine\My`. Best for production where IT manages the cert lifecycle (auto-renewal, revocation).
The **HTTPS port** defaults to 8443. Both HTTP and HTTPS can be active simultaneously — change `HTTP port` and `HTTPS port` independently.
After saving HTTPS settings the service restarts and rebinds. There is briefly a "Disconnected" state in the GUI while that happens (13 seconds).
### Using Let's Encrypt
The server doesn't speak ACME directly. Two practical options:
1. **Reverse proxy approach** — run nginx / Caddy / IIS in front of Webhook Server. The proxy handles Let's Encrypt; Webhook Server stays HTTP-only on loopback. Configure `Trusted proxies` so allowlists still work on the original client IP.
2. **External cert renewal** — use [`win-acme`](https://www.win-acme.com/) to obtain certs and place them in `LocalMachine\My`. Configure HTTPS by **thumbprint** in the GUI. When `win-acme` rotates the cert it produces a new thumbprint, so you'll need to update the GUI; or have a small scheduled task that calls the admin pipe to update the binding (advanced, undocumented for now).
## Secrets at rest
All secrets — bearer tokens, HMAC keys, PFX passwords, RunAs passwords — are encrypted in `config.json` using **DPAPI with the `LocalMachine` scope**:
- The same machine can decrypt them under any account (so changing the service account doesn't break secret access).
- Copying `config.json` to a different machine **doesn't carry the secrets** — DPAPI LocalMachine binds to the host's machine key. This is by design and protects against config exfiltration.
- The GUI displays decrypted secrets in plaintext for an admin user. This is intentional. Anyone who can connect to the admin pipe is already SYSTEM-equivalent on the host; pretending otherwise just makes secret recovery harder.
For backup-and-restore across machines, you'd need to either:
- Re-enter all secrets on the new host (use the **Export config → manual secret re-entry** flow)
- Bind a custom DPAPI scope (not currently supported — would require a v0.x feature request)
## The admin pipe
`\\.\pipe\WebhookServerAdmin` carries the GUI's commands to the service. Its security descriptor allows full control to:
- `NT AUTHORITY\SYSTEM`
- `BUILTIN\Administrators`
Everyone else gets denied at the OS level — there's no auth layer in the protocol itself because the ACL is the auth layer. UAC token splitting means a non-elevated process owned by an Admin user is **also denied** (because the user's standard token has Admins as deny-only). That's why the GUI exe is manifested with `requireAdministrator` — it auto-elevates so the pipe accepts the connection.
If you ever need to grant pipe access to another local group (e.g., a custom `WebhookOperators` group), edit `src/WebhookServer.Core/Ipc/PipeSecurityFactory.cs` and add an `AddAccessRule` for that group. Currently no GUI configures this.
## Threat model summary
What you're protected against, by default:
- **Random scanners hitting your hooks** — solved by IP allowlists (when configured), auth (when configured), and HTTPS (when configured)
- **Replay of inbound requests** — HMAC signs the body, so a captured request can't be modified, but it CAN be replayed. If that matters, include a timestamp in the body and reject old timestamps in your script.
- **Credential leaks** — secrets at rest are DPAPI-encrypted, machine-bound; they don't travel with `config.json`
- **Privilege escalation via the admin pipe** — pipe ACL excludes non-admins
- **Local user spoofing the source IP** — `X-Forwarded-For` is ignored unless you explicitly trust a proxy
What you're NOT protected against — these are out of scope for this server:
- Compromise of an admin account on the host (game over — they own everything)
- A malicious script you configured (you wrote it; the server just runs it)
- DoS via volume of requests — there's no rate limiting in v0.x
- Memory dump of the running service revealing decrypted secrets — DPAPI protects at-rest only
+105
View File
@@ -0,0 +1,105 @@
# 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-account-and-ad.md).
- Service installed with `-ServiceAccount 'CONTOSO\svc-webhookserver$'`
- Delegate the right permissions on the OU(s):
```powershell
$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`:
```powershell
[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
```powershell
$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*.
+122
View File
@@ -0,0 +1,122 @@
# 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`:
```powershell
[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](https://api.github.com/meta) 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-Delivery` ID 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.
+68
View File
@@ -0,0 +1,68 @@
# Recipe: Pop UI on the user's desktop
The classic "fire a hook from your phone, see a calculator window appear on your PC." Useful for:
- Triggering interactive installers / wizards
- Opening browser tabs to specific dashboards on demand
- Playing a sound / showing a toast notification
- Demos and party tricks
## Why this is non-trivial on Windows
The Webhook Server service runs as `LocalSystem` in **session 0**. Anything launched normally from a Service-mode endpoint also lands in session 0, which has no visible desktop — UI runs but nobody sees it. To put a window on the desktop of whoever is logged in at the keyboard, the service has to:
1. Find the active console session ID (`WTSGetActiveConsoleSessionId`)
2. Get a primary token for the user in that session (`WTSQueryUserToken`)
3. Spawn the new process with `CreateProcessAsUser` against that token, targeting `winsta0\default`
Webhook Server does all of this for you when the endpoint's **Run as** is set to **InteractiveUser**.
## Configure the endpoint
| Section | Setting | Value |
|---|---|---|
| Identity | Slug | `calc` |
| Identity | Description | "Pop calculator on the logged-in user's desktop" |
| Auth | Mode | None / Bearer — your call |
| Allowed clients | | restrict; this is interactive UI |
| Executor | Type | **Executable** |
| Executor | Executable path | `C:\Windows\System32\calc.exe` |
| Run as | Identity | **InteractiveUser** |
| Response | Mode | **Async** *(calc never exits on its own; sync would 30-second-timeout-kill it every time)* |
| Response | Fail on non-zero exit | unticked |
Save. Hit `http://localhost:8080/hook/calc` from anywhere — calc.exe pops up on your desktop.
## Limits
- **Service must run as LocalSystem.** Only SYSTEM has the `SeTcbPrivilege` required for `WTSQueryUserToken`. If you switched the service to a gMSA (e.g. for AD-write hooks), this mode stops working. Run two instances of Webhook Server on different ports if you need both.
- **Someone must be logged in** at the console. If the desktop is at the lock screen with no user signed in, the hook fails with `No active console session - is anyone logged in at the keyboard?`.
- **RDP sessions complicate things.** `WTSGetActiveConsoleSessionId` always returns the *console* session, not RDP sessions. If only RDP users are connected and no one is at the physical keyboard, this mode fails. (A separate API, `WTSQueryUserToken` against an enumerated session ID, can target RDP — that'd be a v0.x feature request.)
- **Multiple users logged in via fast-user-switching** — the hook lands in whichever session is currently active (the foreground desktop), not all of them.
## Variations
### Notification toast instead of a window
Use a PowerShell script that emits a Windows 10/11 toast via `BurntToast` (third-party module) or the built-in WinRT API:
```powershell
# requires: Install-Module BurntToast
New-BurntToastNotification -Text 'Webhook fired',$($input | Out-String)
```
Configure the endpoint as InteractiveUser + WindowsPowerShell + inline command. The toast appears as the logged-in user — same as if they fired it themselves.
### Open a URL in the user's default browser
```powershell
Start-Process ($input | ConvertFrom-Json).url
```
Body: `{ "url": "https://contoso.servicenow.com/incident/123" }`
This opens the URL in whatever the user has set as default. Handy for "page on-call → they reply on their phone with a link → URL opens on their workstation when they sit down."
### Run a setup wizard / installer that needs UI
Some installers refuse to run silently or have steps that require human input. Wrap them as InteractiveUser hooks so the operator can trigger them from a help-desk console without having to RDP in.
+220
View File
@@ -0,0 +1,220 @@
# Recipe: Zerto pre/post scripts → AD / DNS update
This is the canonical reason Webhook Server exists. Zerto's failover, move, and clone operations support pre- and post-scripts — but those scripts run on the Zerto Virtual Manager (ZVM), not on the destination domain controller or DNS server. To touch AD or DNS during a failover you need either:
- A bastion / utility host with the right modules and credentials installed (and you accept the maintenance burden of keeping its scripts in sync)
- **A webhook on a Windows host** — Zerto's pre/post calls a single URL, and the webhook server runs the right PowerShell on the right machine with the right identity. This page is about that.
## What we're building
A Zerto pre/post script POSTs to `http://webhooks.contoso.local:8080/hook/dr-failover-prep` with a JSON body identifying the VPG and target VMs. The webhook server, running on a domain-joined utility host as a gMSA with delegated AD rights, runs PowerShell that:
1. Updates AD computer object descriptions to indicate they're now at the DR site
2. Updates DNS A records to point `app01.contoso.local` and friends at the new (DR) IPs
3. Posts a result line to a Teams channel
4. Returns 200 with the summary so it shows up in Zerto's pre/post script log
It's about ~30 lines of PowerShell on the server side and 3 lines of script in Zerto.
## Prerequisites
On the webhook host:
- Webhook Server installed (see [Installation](../installation.md))
- The host is domain-joined
- The service account has the **AD permissions** it needs. We'll configure this two ways below — the simple way (LocalSystem + delegated rights to the machine account) and the production way (gMSA).
- DNS PowerShell module installed if you'll modify DNS: `Install-WindowsFeature RSAT-DNS-Server` (Server) or RSAT installed (Win 10/11).
- AD PowerShell module: `Install-WindowsFeature RSAT-AD-PowerShell` (Server).
On the Zerto side:
- ZVM 8.x or 9.x (this works with both)
- A Virtual Protection Group (VPG) you want to wire up
## 1. Plan the script and the inputs
What does the script need to know? At minimum:
- **VPG name** — Zerto exposes this as a parameter to the pre/post script
- **VM names** — likewise
- **Target IPs** — depending on your failover topology, these may be static (DR network has known IPs) or known after Zerto reconfigures the IP
Decide what travels in the request body and what's hardcoded. A pragmatic split:
- Hardcoded (in the PowerShell script on the webhook host): zone name, AD OU, Teams webhook URL, mapping table from VM hostname → target IP
- Sent in the body: VPG name, list of VM names, an "operation" field (`failover`, `move`, `failback`, etc.)
Example body the Zerto script will send:
```json
{
"operation": "failover",
"vpg": "App-Production",
"vms": ["app01", "app02", "db01"]
}
```
## 2. Write the PowerShell script on the webhook host
Save this as `C:\Scripts\dr-failover-prep.ps1` on the webhook host:
```powershell
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
# Read the body from stdin (the webhook server pipes the JSON in for us when
# StdinJson is enabled).
$body = $input | ConvertFrom-Json
# Hardcoded site config - edit for your environment.
$dnsServer = 'dc01.contoso.local'
$forwardZone = 'contoso.local'
$adOu = 'OU=Servers,DC=contoso,DC=local'
$teamsWebhook = 'https://contoso.webhook.office.com/...' # one-way, no secret to leak
$drIpMap = @{
'app01' = '10.42.10.11'
'app02' = '10.42.10.12'
'db01' = '10.42.10.21'
}
$summary = @()
foreach ($vm in $body.vms) {
if (-not $drIpMap.ContainsKey($vm)) {
$summary += "skip $vm - no DR IP mapping"
continue
}
$newIp = $drIpMap[$vm]
# 1. Update DNS A record (delete + recreate is the simplest reliable path)
$existing = Get-DnsServerResourceRecord -ZoneName $forwardZone -Name $vm `
-RRType A -ComputerName $dnsServer -ErrorAction SilentlyContinue
if ($existing) {
Remove-DnsServerResourceRecord -ZoneName $forwardZone -Name $vm `
-RRType A -RecordData $existing.RecordData.IPv4Address `
-ComputerName $dnsServer -Force
}
Add-DnsServerResourceRecordA -ZoneName $forwardZone -Name $vm `
-IPv4Address $newIp -ComputerName $dnsServer -TimeToLive 00:05:00
# 2. Update AD computer description so on-call can see at a glance
Set-ADComputer -Identity $vm -Description "[DR-$($body.operation)] $(Get-Date -Format s)"
$summary += "ok $vm -> $newIp"
}
# 3. Notify Teams
$msg = @{
text = "Webhook DR prep for VPG **$($body.vpg)** ($($body.operation)):`n" +
($summary -join "`n")
} | ConvertTo-Json
Invoke-RestMethod -Uri $teamsWebhook -Method POST -ContentType 'application/json' -Body $msg | Out-Null
# 4. Print the summary so Zerto's pre/post script log captures it
$summary -join "`n"
```
A few choices worth calling out:
- **`$input | ConvertFrom-Json`** — Webhook Server pipes the request body into the script via stdin when "JSON body to stdin" is ticked. `$input` is PowerShell's automatic variable for pipeline input.
- **`$ErrorActionPreference = 'Stop'`** — turn cmdlet warnings into terminating errors so the script exits non-zero on real problems. Webhook Server then returns 502 (configurable via "Fail on non-zero exit") and Zerto sees the failure.
- **Two-way Teams notification but one-way return** — the script's stdout becomes the HTTP response. Zerto logs it. The Teams notification is a separate Invoke-RestMethod.
## 3. Configure the endpoint in the GUI
In Webhook Server's GUI, **File → New endpoint**:
| Section | Setting | Value |
|---|---|---|
| Identity | Slug | `dr-failover-prep` |
| Identity | Description | "Zerto pre-script: update AD/DNS during failover" |
| Auth | Mode | **Bearer** |
| Auth | Bearer secret | generate a 32-byte random string; copy it for the Zerto script |
| Allowed clients | (one per line) | `10.0.0.0/8` (your ZVM's network) |
| Executor | Type | **Windows PowerShell** |
| Executor | Script path | `C:\Scripts\dr-failover-prep.ps1` |
| Data passing | JSON body to stdin | ✓ |
| Data passing | Headers/query as env vars | ✗ |
| Run as | Identity | **Service** if the service is running as a gMSA with AD rights, otherwise **SpecificUser** with a delegated account |
| Response | Mode | **Sync** |
| Response | Timeout (sec) | `60` |
| Response | Fail on non-zero exit | ✓ |
Save. Right-click the row → **Copy URL** to grab the full URL, e.g. `http://webhooks.contoso.local:8080/hook/dr-failover-prep`.
> **Why Bearer auth and not None?** Even though the IP allowlist limits who can reach this endpoint, the Bearer token is a defense-in-depth layer. If someone managed to spoof or get on the trusted network, they still need the token. Generate it once, store it in a secrets manager (or in Zerto's encrypted script parameters), and never email it.
## 4. The Zerto pre/post script
Zerto pre/post scripts are PowerShell files placed on the ZVM. The path varies by Zerto version; in 9.x it's typically `C:\Program Files\Zerto\Zerto Virtual Replication\Scripts\`.
Create `dr-failover-prep.ps1` on the ZVM:
```powershell
# Zerto passes context as parameters/environment - exact names vary by version.
# Document yours; this is illustrative.
param(
[string]$VpgName = $env:ZertoVPGName
)
$webhookUrl = 'http://webhooks.contoso.local:8080/hook/dr-failover-prep'
$bearer = 'paste-the-bearer-secret-here' # store via Zerto secret param if available
# Build the body. In a real script, list the VMs by querying Zerto's API or by
# convention from the VPG name.
$body = @{
operation = 'failover'
vpg = $VpgName
vms = @('app01','app02','db01')
} | ConvertTo-Json
$response = Invoke-RestMethod -Method POST -Uri $webhookUrl -Body $body `
-ContentType 'application/json' -TimeoutSec 90 `
-Headers @{ Authorization = "Bearer $bearer" }
# Print whatever the webhook returned to Zerto's log.
$response.stdout
```
Wire this script into your VPG's **Pre-Recovery** or **Post-Recovery** hook in the Zerto UI.
## 5. Test before going live
In a maintenance window, hit the endpoint manually with a fake VPG name to confirm the wiring works:
```powershell
$body = @{ operation='test'; vpg='SmokeTest'; vms=@('app01') } | ConvertTo-Json
Invoke-RestMethod -Method POST `
-Uri http://webhooks.contoso.local:8080/hook/dr-failover-prep `
-Headers @{ Authorization = "Bearer paste-the-secret" } `
-ContentType application/json -Body $body
```
You should see the summary line(s) come back, AD descriptions update, DNS A records update, and a Teams notification. If anything's off:
- **No response, hang** → check the GUI's log panel. The auto-poll updates every 3 seconds. Look for the run line with the slug + exit code.
- **401 Unauthorized** → bearer mismatch
- **403 Forbidden** → IP allowlist blocking you
- **502 Bad Gateway** → script ran but exited non-zero. The response body has stderr.
After a real failover triggers it, audit by checking the daily log file at `C:\ProgramData\WebhookServer\logs\webhook-YYYYMMDD.log` for the `Run <id> dr-failover-prep ok exit=0` line.
## Variations
### Different actions for failover vs. failback
Pass an `operation` field in the body and branch on it in the PowerShell. The script above already does this — extend the `switch` to handle `failback` (revert DNS to production IPs, clear DR description, etc.).
### Per-VPG endpoints
If you want fine-grained access control per VPG, create one endpoint per VPG and give each its own bearer secret. The GUI's grid handles dozens of endpoints fine.
### Async + callback for long-running work
If your AD/DNS update genuinely takes minutes (e.g., updating thousands of records in a large environment), set the endpoint to **Async** mode. Zerto's pre-script gets `202 Accepted` immediately and continues. Configure the endpoint's **Callback** with a URL that records the result (e.g., another endpoint that logs to a file, or your monitoring system's API).
### Audit trail to a SIEM
Configure each endpoint's **Callback** with your SIEM's HTTP collector URL + an HMAC secret. Every run produces a JSON record with runId, exit code, duration, stdout, and stderr — perfect for compliance audit logs.
+78
View File
@@ -0,0 +1,78 @@
# Run As modes — when to use which
Each endpoint has a **Run As** setting (in the editor's "Run as" section) that controls *who* the script runs as. The default works for most cases, and switching modes is one dropdown change.
## The three modes
| Mode | Runs as | Use when… |
|---|---|---|
| **Service** *(default)* | Whoever the Windows Service runs under (LocalSystem by default) | Almost everything. Local file ops, calling local APIs, running cmd / PowerShell scripts that don't need a user identity. |
| **InteractiveUser** | The user logged in at the keyboard | The script needs to put a window on the screen (Calculator, a notification dialog, opening a browser tab) |
| **SpecificUser** | A named local or domain user / password you provide | The script runs in AD, a fileshare, or any system that wants the action attributed to a specific identity — and you don't want the service itself running as that user. |
## Service (default)
Nothing to configure. The hook runs as `LocalSystem` by default — full local rights, very limited network identity (the machine account on a domain).
You can change the service identity at install time via the `-ServiceAccount` parameter to `install-service.ps1` (gMSA, domain user, etc.). Anything you set there applies to **all** Service-mode endpoints. See [Service account & Active Directory](service-account-and-ad.md).
**Pros**: zero config per endpoint, no passwords to manage, fastest path
**Cons**: the script can't pop UI on the user's desktop (Session 0 isolation), and on a workgroup machine it has no domain identity at all
## InteractiveUser
Pick this when the hook should appear visually on the desktop of whoever is logged in. The clearest example is "fire a hook from my phone, get a Calculator window on my PC."
How it works internally: the service (running as SYSTEM) calls the Win32 API `WTSQueryUserToken` to grab the active console session's user token, then `CreateProcessAsUser` to land the new process inside that session.
What you don't have to configure: username, password, profile loading, session ID. All inferred at runtime.
What can go wrong:
- **No one logged in** at the keyboard → hook fails with `No active console session - is anyone logged in at the keyboard?`. The hook can't run; there's no desktop to land on.
- **Service runs as anything other than LocalSystem** → `WTSQueryUserToken` requires SYSTEM. If you switched the service to a gMSA / domain user, InteractiveUser stops working.
- **Locked desktop, no user logged in but session 1 reserved** → similar to "no one logged in." Once a user logs in interactively (even just to the lock screen with credentials cached), the session is "active enough" for this to work.
**Use case examples**: see [recipes/ui-on-desktop.md](recipes/ui-on-desktop.md).
## SpecificUser
Pick this when the hook needs to authenticate as a specific account — a service account with delegated AD rights, a local Administrator on a remote machine, etc. — but you don't want the *whole service* running as that account.
Configure:
- **Username**: `DOMAIN\user`, `.\local-user`, or a UPN like `user@contoso.com`. The leading `.\` is shorthand for the local machine.
- **Password**: stored DPAPI-encrypted at rest. Visible in plaintext in the GUI for an admin user, by design — anyone with admin pipe access already has SYSTEM-equivalent rights.
- **Load profile**: optional. Loads the user's HKCU and AppData before running. Slower (~1s extra). Only needed if the script reads user-scoped settings (uncommon).
How it works internally: the service calls `LogonUser` with the credentials (interactive logon type first, falls back to batch logon for service-only accounts), then `DuplicateTokenEx` + `CreateProcessAsUser`. The script lands in a fresh batch session with the user's network identity.
> **Why not `psi.UserName` / `psi.Password` like a normal .NET app?** Because `CreateProcessWithLogonW` (what those properties use under the hood) refuses to run when the caller is `LocalSystem`, which is exactly our scenario. The token-based path is the documented Windows mechanism for this.
What can go wrong:
- **Wrong password** → log shows `LogonUser (DOMAIN\user) failed - The user name or password is incorrect`. Re-enter in the editor.
- **Account is denied logon locally** → log shows `Logon failure: the user has not been granted the requested logon type`. Make sure the account has at least one of *Log on as a batch job* or *Log on locally* under `secpol.msc` → Local Policies → User Rights Assignment.
- **Domain controller unreachable** → for domain accounts, the service must be able to reach a DC. For local accounts (`.\name`), no domain dependency.
## Decision flowchart
```
Need UI on the user's desktop?
┌─────── yes ─────┴────── no ─────┐
│ │
InteractiveUser Need specific identity (AD / fileshare / etc.)?
┌──── yes ────┴──── no ────┐
│ │
Should ALL hooks run as Service
this identity?
┌────── yes ──────────┴───────── no ──────────┐
│ │
Run service itself SpecificUser per endpoint
as that account
(gMSA / domain user)
see service-account-and-ad.md
```
+149
View File
@@ -0,0 +1,149 @@
# Service account & Active Directory
The service runs as `LocalSystem` out of the box. That's right for local-only scripts and **read-only** AD queries (LocalSystem authenticates to the network as the machine account, which Authenticated Users includes by default). It is wrong for hooks that need to **modify** AD — passwords, group memberships, computer objects.
This page covers the four real-world choices and how to switch.
## The four options
| Account | Network identity | When to use |
|---|---|---|
| **`LocalSystem`** *(default)* | Computer account `DOMAIN\MACHINE$` on a domain-joined host; nothing on a workgroup host | Default. Local file ops, simple PowerShell, read-only AD queries. Most powerful local account — any hook running under it has full local rights. |
| **`LocalService`** | None | **Don't.** Cannot talk to a domain controller. Listed only to rule it out. |
| **`NetworkService`** | Same machine account as LocalSystem | Slightly less local privilege than LocalSystem, same network identity. Rarely the right pick. |
| **Domain user** (`DOMAIN\svc-webhookserver`) | That user | Use when hooks need write access to AD and you can't use a gMSA. You own password rotation. |
| **gMSA** (`DOMAIN\svc-webhookserver$`) | That gMSA | **Recommended for AD-write workloads.** AD generates and rotates the password automatically every 30 days. Requires domain functional level 2012+. |
## Switching the service account at install time
Pass `-ServiceAccount` to `install-service.ps1` (or to the deploy / dev launcher):
```powershell
# Domain user
& "C:\Program Files\WebhookServer\scripts\install-service.ps1" `
-BinaryPath "C:\Program Files\WebhookServer\WebhookServer.Service.exe" `
-ServiceAccount "CONTOSO\svc-webhookserver" -Password "..."
# gMSA - note trailing $ and no -Password
& "C:\Program Files\WebhookServer\scripts\install-service.ps1" `
-BinaryPath "C:\Program Files\WebhookServer\WebhookServer.Service.exe" `
-ServiceAccount 'CONTOSO\svc-webhookserver$'
```
Or do it manually with `sc.exe` if the service is already installed:
```powershell
sc.exe stop WebhookServer
sc.exe config WebhookServer obj= 'CONTOSO\svc-webhookserver$'
sc.exe start WebhookServer
```
## gMSA setup (recommended for AD writes)
A gMSA is a Group Managed Service Account. Active Directory generates and stores its password and rotates it every 30 days; the host machine account retrieves the password as needed. You never see or store it. This is the cleanest pattern for production.
### One-time domain setup
If your domain has never used gMSAs, create the KDS root key (only needed once per domain):
```powershell
# from a Domain Admin PowerShell, on any DC
Add-KdsRootKey -EffectiveImmediately
# in production wait 10 hours for replication; in a lab, override:
# Add-KdsRootKey -EffectiveTime ((Get-Date).AddHours(-10))
```
### Create the gMSA
```powershell
# from a DC, with AD PowerShell module loaded
New-ADServiceAccount -Name svc-webhookserver `
-DNSHostName webhook01.contoso.local `
-PrincipalsAllowedToRetrieveManagedPassword "DOMAIN\WebhookHosts"
```
`PrincipalsAllowedToRetrieveManagedPassword` is the security group containing the computer accounts allowed to use the gMSA. Add your webhook host(s) to that group:
```powershell
Add-ADGroupMember -Identity 'WebhookHosts' -Members 'WEBHOOK01$'
# the host needs to reboot OR have its kerberos ticket flushed for the new group membership to apply
```
### Install the gMSA on the host
On the webhook server machine itself:
```powershell
# from elevated PowerShell, AD PowerShell module installed (RSAT)
Install-ADServiceAccount svc-webhookserver
Test-ADServiceAccount svc-webhookserver # should return True
```
If `Test-ADServiceAccount` returns False, check:
- Host is in the `WebhookHosts` group (or whoever's in `PrincipalsAllowedToRetrieveManagedPassword`)
- Host has been rebooted since being added to the group
- KDS root key has had time to propagate (10 hours by default)
### Configure the service to use it
```powershell
# from elevated PowerShell on the webhook host
sc.exe stop WebhookServer
sc.exe config WebhookServer obj= 'CONTOSO\svc-webhookserver$'
sc.exe start WebhookServer
```
Note the trailing `$`. There is **no password parameter** for gMSAs. The trailing `$` is what tells the SCM "look up this account in AD as a managed service account, retrieve its password automatically."
### Grant AD permissions
Give the gMSA only what it needs. For a typical "reset user passwords" workload:
```powershell
# Delegate "Reset password and force change at next logon" on a specific OU
$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"
```
…or use the GUI Delegation of Control wizard in Active Directory Users and Computers.
## Domain user (fallback when gMSA isn't available)
```powershell
# 1. Create the user (one time)
New-ADUser -Name "svc-webhookserver" -SamAccountName "svc-webhookserver" `
-AccountPassword (Read-Host -AsSecureString "password") -Enabled $true `
-PasswordNeverExpires $true -CannotChangePassword $true
# 2. Grant "Log on as a service" right on the host:
# secpol.msc -> Local Policies -> User Rights Assignment -> Log on as a service
# Add CONTOSO\svc-webhookserver
# 3. Configure the service:
sc.exe config WebhookServer obj= "CONTOSO\svc-webhookserver" password= "..."
```
You own password rotation. When you change the password in AD, also update the service via `sc.exe config WebhookServer password= "newpw"` and restart it.
## What changes for hooks when you switch the service account
- **Service mode hooks** now run as the new account. PowerShell `whoami` from inside a hook will show the new identity.
- **InteractiveUser hooks stop working** if you switch off LocalSystem. Only SYSTEM can call `WTSQueryUserToken`. If you need both AD-write hooks and UI-on-desktop hooks, pick one of:
- Keep service as LocalSystem and use **SpecificUser** mode for AD-write hooks
- Switch service to a gMSA / domain user and drop UI hooks (or move them to a separate Webhook Server instance running as LocalSystem)
- **SpecificUser hooks** continue to work regardless. They use a separate `LogonUser` token per call.
## Verifying the switch worked
After changing the service account, restart the service and add a quick diagnostic endpoint:
```
slug: whoami
auth: none
executor: Windows PowerShell
inline command: whoami; whoami /groups
```
Hit it and verify the output matches the account you configured. The first line should be `domain\svc-webhookserver` (or `domain\machine$` for LocalSystem on a domain-joined host).
+136
View File
@@ -0,0 +1,136 @@
# Troubleshooting
This page indexes the most common ways things go wrong, where to look, and what to do.
## Where to look first
| Symptom | First check |
|---|---|
| GUI shows "Disconnected" | Service running? `Get-Service WebhookServer` |
| Hook returns 404 | Slug typo, or endpoint disabled |
| Hook returns 401 | Auth header / signature mismatch |
| Hook returns 403 | IP allowlist denies the caller |
| Hook returns 200 but nothing happens | Response is the script's stdout — check exit code, stderr |
| Hook returns 502 | Script ran and exited non-zero. Body has stderr. |
| Hook returns 500 | Launch error (script not found, invalid path) |
| Hook hangs | Timeout reached, or script is waiting on stdin |
| Calc / UI doesn't appear despite InteractiveUser | See [Run As modes](runas-modes.md) — common pitfalls |
## Where the logs are
`C:\ProgramData\WebhookServer\logs\webhook-YYYYMMDD.log` — daily rolling, 14-day retention by default.
Every webhook run logs:
- `[INF] Run <id> <slug> ok exit=0 dur=<ms>ms stdout=...` on success
- `[WRN] Run <id> <slug> non-zero exit=<n> dur=<ms>ms stdout=... stderr=...` on script failure
- `[WRN] Run <id> <slug> failed to launch: <reason>` on launch failure
- `[WRN] Run <id> <slug> timed out after <s>s; process killed` on timeout
The GUI's bottom panel auto-refreshes the same log file every 3 seconds. Tick the **Auto-scroll** checkbox to keep it pinned to the latest line.
## Common issues
### "Disconnected: Access to the path is denied" right after install
You launched the GUI without elevation. The admin pipe ACL is `SYSTEM` + `Administrators`-full-control; UAC token splitting denies the standard token.
**Fix in v0.1.1+**: nothing — the GUI's manifest is `requireAdministrator` and Start Menu / shortcut launches auto-elevate.
**Fix in v0.1.0**: right-click the Start Menu shortcut → **Run as administrator**, or upgrade.
### "Connection refused" hitting the hook URL
Three possibilities, in order of probability:
1. **Service stopped.** `Get-Service WebhookServer` and `Start-Service WebhookServer` if needed.
2. **Wrong port.** Default is 8080. Check **Server → Settings → HTTP port** in the GUI, or `netstat -an | findstr :8080`.
3. **Bound to a specific NIC and you're calling on another.** Check **Server → Settings → Listen on**. If "Listen on all interfaces" is unchecked and you only ticked LAN IPs, calls to `localhost` may fail. Tick `127.0.0.1` too.
### Hook works from `localhost` but not from another machine on the LAN
Windows Firewall. The installer doesn't add a firewall rule (intentional — you should choose your scope). Add one:
```powershell
# from elevated PowerShell on the webhook host
New-NetFirewallRule -DisplayName "Webhook Server HTTP 8080" -Direction Inbound `
-Action Allow -Protocol TCP -LocalPort 8080 -Profile Domain,Private
```
Use `-Profile Public` only if you really mean it. Better: front the server with a reverse proxy and don't expose 8080 directly.
### `[WRN] Run … failed to launch: launch error: An error occurred trying to start process 'X'. Access is denied.`
Likely **SpecificUser mode + `psi.UserName`** failure. Should be impossible in v0.1.1+ (we use `LogonUser` + `CreateProcessAsUser` directly). If you see this on v0.1.1, double-check the version: `Get-Item "C:\Program Files\WebhookServer\WebhookServer.Service.exe" | % VersionInfo`.
### `[WRN] Run … failed to launch: LogonUser (DOMAIN\user) failed`
The credentials don't authenticate. Common causes:
- Typo in the password (paste it back into the GUI to verify; the field is plaintext for an admin user)
- Account locked / disabled / expired
- The account is denied the right logon types — check `secpol.msc` → Local Policies → User Rights Assignment → "Deny logon as a batch job" / "Deny logon locally"
- For domain accounts: the host can't reach a DC
### `non-zero exit=-1073741502` (`0xC0000142` STATUS_DLL_INIT_FAILED)
The new process couldn't initialize. With **InteractiveUser** mode this means we tried to open `winsta0\default` and the user's session token doesn't have access (e.g., no one's logged in). With **SpecificUser** this should not occur in v0.1.1+ — we deliberately don't set lpDesktop for that mode.
### Hook returns 502 with empty stdout/stderr
The script's exit was non-zero but it didn't print anything. PowerShell's `$ErrorActionPreference = 'Stop'` is your friend — turn it on at the top of the script and any cmdlet failure becomes terminating with a clear message in stderr.
### "ServiceState: ListenerSettingsChanged" → service restart
After saving Server Settings with a port or HTTPS change, the service stops itself so the SCM restarts it on the new bindings. The GUI briefly shows "Disconnected" then reconnects. If it doesn't reconnect within ~10 seconds:
```powershell
Get-Service WebhookServer | Format-List Status, StartType
```
If the service is in `Stopped`, the SCM didn't restart it (failure-recovery only kicks in on *abnormal* termination, and a clean stop doesn't qualify). Manual:
```powershell
Start-Service WebhookServer
```
### GUI editor changes don't seem to take effect
After saving an endpoint, the service loads the new config in memory immediately — no restart needed. If a hook is mid-run when you save, that run finishes against the OLD config; the new config applies to subsequent runs.
If the GUI's grid still shows old values, hit any other endpoint or wait for the 3-second poll to refresh the display.
### Tray icon doesn't appear
Check whether the GUI is running: `Get-Process WebhookServer.Gui`. If not, the tray icon doesn't exist (it's part of the GUI process). To have a persistent tray independent of the main window, leave the GUI running and minimize it — it'll hide-to-tray rather than truly close.
To run the GUI minimized at login: create a Windows shortcut to `WebhookServer.Gui.exe`, set "Run" to "Minimized" in the shortcut properties, and put it in your user's Startup folder (`shell:startup`). The auto-elevate manifest still takes effect.
## Getting useful logs from a script
Inside your hook scripts, write to stderr for diagnostic info — Webhook Server logs stderr separately from stdout, and stderr is preserved even on success:
```powershell
[Console]::Error.WriteLine("processing item $i of $total")
```
Or use `Write-Error` which produces non-fatal errors:
```powershell
Write-Error "skipping bogus input" # stderr but doesn't terminate
```
The full stderr appears in the log line for the run, plus in the response body for sync calls.
## Asking for help
If you're stuck, file an issue at:
> https://github.com/recklessop/webhook-server/issues
Include:
- Webhook Server version (Help → About, or the file version of the `.exe`)
- Windows version (`winver`)
- The slug + relevant bits of the endpoint config (NOT the secrets)
- The log lines for the failing run (search for the runId)
- What you expected vs. what happened
+90
View File
@@ -0,0 +1,90 @@
# Uninstalling
## TL;DR
**Settings → Apps & features → Webhook Server → Uninstall.** Or right-click the **Uninstall Webhook Server** Start Menu shortcut.
Your endpoints, secrets, and logs in `C:\ProgramData\WebhookServer\` are preserved by default. To wipe those too, see [Below](#wiping-config-and-logs-too).
## What the uninstaller does
In order:
1. **Stops the service** (`net stop WebhookServer`).
2. **Removes the service** registration via `uninstall-service.ps1` (which calls `sc.exe delete WebhookServer`).
3. **Deletes** `C:\Program Files\WebhookServer\`.
4. **Removes** the Start Menu and (if created) Desktop shortcuts.
5. **Removes** the Programs and Features entry.
What it **does not** touch:
- `C:\ProgramData\WebhookServer\` (config, secrets, log files, auto-snapshots)
- Any cert in your local cert store you bound HTTPS to
- Domain accounts / gMSAs the service ran under
- Endpoints' deployed scripts, if you stored them outside the install dir
## Wiping config and logs too
After running the uninstaller, also remove the data root:
```powershell
# from elevated PowerShell
Remove-Item -Recurse -Force "$env:ProgramData\WebhookServer"
```
This deletes:
- `config.json` (with all your endpoints, encrypted secrets, settings)
- `backups\` (all auto-snapshots — you can't restore from these once gone)
- `logs\` (history of every webhook hit)
There's no recovery from this. If you might want to reinstall later with the same configuration, copy `config.json` to a safe location first. Note that **secrets in the saved config can only be decrypted on the same machine** (DPAPI LocalMachine scope) — you can move the file but the bearer/HMAC/RunAs passwords inside become unrecoverable on a different host.
## Silent uninstall
The Programs and Features uninstaller is `unins000.exe` in the install directory:
```powershell
# from elevated PowerShell
& "C:\Program Files\WebhookServer\unins000.exe" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
```
Same set of preserved/removed paths as the interactive flow.
## Removing only the service, keeping the binaries
If you want to keep the GUI installed but stop running the service (rare, but useful if you're testing):
```powershell
# from elevated PowerShell
sc.exe stop WebhookServer
sc.exe delete WebhookServer
```
The GUI will show **Disconnected** since there's no service to talk to. Re-create the service later by running `install-service.ps1`:
```powershell
& "C:\Program Files\WebhookServer\scripts\install-service.ps1" `
-BinaryPath "C:\Program Files\WebhookServer\WebhookServer.Service.exe"
```
## Edge cases
### "The service cannot be stopped because it has not been started."
Harmless. The uninstaller proceeds regardless.
### "Cannot delete: file in use"
A GUI window or other process is holding files in `C:\Program Files\WebhookServer\` open. Close everything and re-run the uninstaller. If that fails, reboot and re-run.
### Programs and Features entry remains after files are gone
If you deleted `C:\Program Files\WebhookServer\` manually before running the uninstaller, `unins000.exe` is gone too and Programs and Features can't run it. Remove the orphan entry by deleting its registry key:
```powershell
# from elevated PowerShell - dry run to confirm the key exists
Get-Item 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\{6E3B3C1A-9C20-4F50-B6A8-2B6D6D7E2F11}_is1' -ErrorAction SilentlyContinue
# if it shows up, delete it:
Remove-Item 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\{6E3B3C1A-9C20-4F50-B6A8-2B6D6D7E2F11}_is1' -Recurse
```
+76
View File
@@ -0,0 +1,76 @@
# Upgrading
## TL;DR
Download the new installer from [Releases](https://github.com/recklessop/webhook-server/releases/latest) and run it. That's it. Your config, endpoints, secrets, and logs are preserved.
## What the upgrade does
The Inno Setup installer detects an existing install and runs through these steps automatically:
1. **`net stop WebhookServer`** — synchronously stops the running service so its binaries are unlocked. Blocks until the SCM reports the service is actually stopped.
2. **`taskkill /f /im WebhookServer.Gui.exe`** — closes the GUI if you left it running. Same for any orphan `WebhookServer.Service.exe` from a `deploy.ps1` dev install.
3. **Copies** the new binaries into `C:\Program Files\WebhookServer\`. Files marked `ignoreversion` so newer files always overwrite older ones, even if version metadata happens to match.
4. **Re-registers** the service via `install-service.ps1`, which detects the existing `WebhookServer` service via `Get-Service` and takes the **update** branch (changes the binary path) rather than re-creating it. Your service account choice is preserved.
5. **Starts the service**. The GUI launches if you left the post-install checkbox ticked.
Total downtime for the service: 210 seconds depending on disk speed and how long the service takes to flush its log buffer.
## What's preserved
- `C:\ProgramData\WebhookServer\config.json` — the installer never touches this directory
- All endpoints, secrets, callback URLs, allowlists
- Bind addresses, display host, HTTPS binding settings
- Auto-snapshots in `C:\ProgramData\WebhookServer\backups\`
- Log files in `C:\ProgramData\WebhookServer\logs\`
- The Windows Service identity (LocalSystem, gMSA, domain user — whatever you configured)
## What gets replaced
- Everything in `C:\Program Files\WebhookServer\` — the .exe files, .dll files, the icon, `install-service.ps1`, `uninstall-service.ps1`, the bundled `README.md`, the `docs/` folder
## Silent upgrades (Group Policy / SCCM / Intune / Ansible)
Same as the silent install:
```powershell
WebhookServer-Setup-X.Y.Z.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
```
The pre-install `net stop` step still fires; downtime is unchanged.
## Rolling back to a previous version
The installer doesn't support side-by-side versions or downgrade detection. To roll back:
1. Uninstall the current version (Settings → Apps, or `Start Menu → Webhook Server → Uninstall`). This stops + removes the service. Your config in `C:\ProgramData\WebhookServer\` is preserved.
2. Run the older installer.
If a config field changed semantics between versions and you ran on the new version first, the **Config Checkpoints** menu (File → Config Checkpoints) lists snapshots taken before each save. The auto-snapshot from immediately before the upgrade is the closest you'll have to your pre-upgrade config.
## Edge cases
### "Setup cannot continue. Please close the following applications: WebhookServer.Gui.exe"
The taskkill step normally handles this, but if you're running an unusually slow process or if the GUI was elevated by a different user, you may see this. Close the GUI manually and click Retry.
### Service stays in a "Stopping" state forever
`net stop` waits up to 30 seconds for the service to stop. If a hook script hung (e.g. interactive prompt) and the service can't kill it cleanly, the SCM gives up and the install continues, but the service may end up in a bad state. Recovery:
```powershell
# from elevated PowerShell
Stop-Service WebhookServer -Force
# if that fails:
Get-WmiObject Win32_Service -Filter "Name='WebhookServer'" | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
```
…then re-run the installer.
### Upgrade from a `deploy.ps1` dev install to an installer-managed install
The first time you run the installer on a machine that previously used `deploy.ps1`, the installer thinks it's doing a fresh install (no `Programs and Features` registry entry). It still detects the existing service and updates it cleanly, so the only visible difference is that **a Programs and Features entry now exists** for "Webhook Server" with `Justin Paul` as publisher. Future upgrades take the proper upgrade path.
### `deploy.ps1` after an installer-managed install
`deploy.ps1` is the dev workflow. It publishes from source and copies binaries to the same install location. Running it on top of an installer-managed install will overwrite the binaries but won't deregister the installer. If you then uninstall via Programs and Features, the uninstaller may leave files behind that `deploy.ps1` introduced. Pick one workflow and stick with it.
+1
View File
@@ -56,6 +56,7 @@ Source: "{#RepoRoot}publish\gui\*"; DestDir: "{app}"; Flags: ignoreversion r
Source: "{#RepoRoot}scripts\install-service.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion Source: "{#RepoRoot}scripts\install-service.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion
Source: "{#RepoRoot}scripts\uninstall-service.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion Source: "{#RepoRoot}scripts\uninstall-service.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion
Source: "{#RepoRoot}README.md"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RepoRoot}README.md"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RepoRoot}docs\*"; DestDir: "{app}\docs"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#RepoRoot}resources\webhook-server.ico"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RepoRoot}resources\webhook-server.ico"; DestDir: "{app}"; Flags: ignoreversion
[Icons] [Icons]
+2
View File
@@ -57,6 +57,8 @@
<MenuItem Header="_Restart service" Command="{Binding RestartServiceCommand}"/> <MenuItem Header="_Restart service" Command="{Binding RestartServiceCommand}"/>
</MenuItem> </MenuItem>
<MenuItem Header="_Help"> <MenuItem Header="_Help">
<MenuItem Header="_Documentation…" Command="{Binding OpenDocumentationCommand}"/>
<Separator/>
<MenuItem Header="_About Webhook Server…" Command="{Binding ShowAboutCommand}"/> <MenuItem Header="_About Webhook Server…" Command="{Binding ShowAboutCommand}"/>
</MenuItem> </MenuItem>
</Menu> </Menu>
@@ -290,6 +290,23 @@ public sealed partial class MainViewModel : ObservableObject
dlg.ShowDialog(); dlg.ShowDialog();
} }
[RelayCommand]
private void OpenDocumentation()
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "https://github.com/recklessop/webhook-server/tree/main/docs",
UseShellExecute = true,
});
}
catch (Exception ex)
{
ShowError("Could not open documentation", ex);
}
}
[RelayCommand] [RelayCommand]
private void Exit() private void Exit()
{ {