Sync from docs/ at 85a7008
+77
@@ -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](Run-As-Modes). |
|
||||
| **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).
|
||||
+32
-1
@@ -1 +1,32 @@
|
||||
Welcome to the Wiki.
|
||||
# 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) — five-minute read on what a webhook is and how this server uses one
|
||||
2. [Installation](Installation) — download, install, first endpoint
|
||||
3. [Recipe: Zerto failover post-script → DNS + service checks](Recipe-Zerto-Failover) — the canonical reason this exists
|
||||
|
||||
## Topical
|
||||
|
||||
- [Upgrading](Upgrading)
|
||||
- [Uninstalling](Uninstalling)
|
||||
- [Run As modes — when to use which](Run-As-Modes)
|
||||
- [Service account & Active Directory](Service-Account-and-AD)
|
||||
- [Network & security](Network-and-Security)
|
||||
- [Troubleshooting](Troubleshooting)
|
||||
|
||||
## Recipes (cookbook style)
|
||||
|
||||
- [Zerto failover post-script → DNS + service checks](Recipe-Zerto-Failover) ↠canonical use case
|
||||
- [GitHub-style HMAC-signed webhook](Recipe-GitHub-HMAC)
|
||||
- [Pop UI on the user's desktop](Recipe-UI-on-Desktop)
|
||||
|
||||
The flagship Zerto recipe also ships with a **ready-to-use Zerto-side post-script** at [`scripts/examples/zerto-post-failover.ps1`](../scripts/examples/zerto-post-failover.ps1).
|
||||
|
||||
## 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)
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
# Installation
|
||||
|
||||
This page covers a fresh install. If you already have Webhook Server installed, see [Upgrading](Upgrading). To remove it, see [Uninstalling](Uninstalling).
|
||||
|
||||
## 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](Recipe-Zerto-Failover).
|
||||
|
||||
## 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) 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) for how to wipe them too.
|
||||
@@ -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 (1–3 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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,243 @@
|
||||
# Recipe: Zerto failover post-script → DNS update + service checks
|
||||
|
||||
This is the canonical reason Webhook Server exists.
|
||||
|
||||
When Zerto fails a VM over from production to DR, the VM boots fine — but **the things around it** often need attention: DNS records still point at the production IP, dependent services need to be checked, on-call needs a heads-up. Zerto pre/post scripts run on the **Zerto Virtual Manager**, not on a domain controller and not necessarily with admin rights to the things that need fixing. So you want a single webhook URL that the post-script hits, and a Windows host on the DR side that does the actual work with the right identity.
|
||||
|
||||
## What we're building
|
||||
|
||||
Zerto's post-recovery script (a one-shot PowerShell file pointing at curl) calls `http://webhook.dr.contoso.local:8080/hook/post-failover` with a JSON body identifying the VPG and operation. The Webhook Server, running on a DR-side Windows host as a gMSA with delegated AD/DNS rights, runs PowerShell that:
|
||||
|
||||
1. Updates DNS A records to point the failed-over hostnames at their DR IPs
|
||||
2. Waits for the failed-over VM to come up (ping + WinRM probe)
|
||||
3. Connects to the VM via PowerShell remoting and starts/checks critical services
|
||||
4. Sends a Teams notification with the result
|
||||
|
||||
The endpoint is **Async** so the Zerto script returns in milliseconds — no risk of timing out Zerto's failover sequence even if the actions take minutes. The script's full output ends up in the webhook log and (optionally) in an outbound callback.
|
||||
|
||||
## Why curl and not Invoke-WebRequest?
|
||||
|
||||
Zerto's PowerShell runner is intentionally minimal — many environments run an older Windows on the ZVM and don't have full PowerShell modules installed. `curl.exe` ships with Windows 10 1803+ and Server 2019+ and works without any modules. Plus, calling an HTTP endpoint with `curl.exe` doesn't depend on the version of `Invoke-WebRequest` shipped with the host's PowerShell.
|
||||
|
||||
## 1. The Zerto post-script (client side)
|
||||
|
||||
A ready-to-use script ships in this repo at [`scripts/examples/zerto-post-failover.ps1`](../../scripts/examples/zerto-post-failover.ps1). Copy it to the ZVM, edit `$WebhookUrl` and the bearer-token path at the top, and wire it into the VPG:
|
||||
|
||||
> **VPG settings → Recovery → Scripts → Post-Recovery Script**
|
||||
> Path: `C:\Scripts\zerto-post-failover.ps1`
|
||||
> Parameters: *(leave empty)*
|
||||
|
||||
The script is ~50 lines and only depends on `curl.exe` + a token file readable by the ZVM service account.
|
||||
|
||||
The flow:
|
||||
|
||||
```
|
||||
Zerto VPG failover starts
|
||||
|
|
||||
+-- VM is brought up at DR site
|
||||
|
|
||||
+-- Zerto post-script fires:
|
||||
| curl POST http://webhook.dr/hook/post-failover (async, returns 202 in ~50ms)
|
||||
|
|
||||
+-- Zerto sees success, finishes the failover and reports done
|
||||
|
|
||||
(meanwhile, on the webhook server)
|
||||
|
|
||||
running PowerShell for several minutes:
|
||||
- update DNS
|
||||
- wait for VM ready
|
||||
- check services on VM
|
||||
- notify Teams
|
||||
```
|
||||
|
||||
## 2. The server-side script (does the actual work)
|
||||
|
||||
Save this on the webhook host as `C:\Scripts\post-failover-handler.ps1`:
|
||||
|
||||
```powershell
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$body = $input | ConvertFrom-Json
|
||||
|
||||
# ---------- environment specifics; edit for your site ----------
|
||||
$dnsServer = 'dc01.contoso.local'
|
||||
$forwardZone = 'contoso.local'
|
||||
$teamsWebhook = 'https://contoso.webhook.office.com/...'
|
||||
$drIpMap = @{
|
||||
'app01' = '10.42.10.11'
|
||||
'app02' = '10.42.10.12'
|
||||
'db01' = '10.42.10.21'
|
||||
}
|
||||
$serviceMap = @{
|
||||
'app01' = @('W3SVC','MyAppSvc')
|
||||
'app02' = @('W3SVC','MyAppSvc')
|
||||
'db01' = @('MSSQLSERVER','SQLAgent')
|
||||
}
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
# Default the VM list to "all VMs we know about" if the post-script didn't
|
||||
# tell us, so the same handler works without having to embed the VM list in
|
||||
# every Zerto post-script.
|
||||
$vms = if ($body.vms) { $body.vms } else { $drIpMap.Keys }
|
||||
|
||||
$summary = @()
|
||||
|
||||
foreach ($vm in $vms) {
|
||||
if (-not $drIpMap.ContainsKey($vm)) {
|
||||
$summary += "skip $vm (no DR IP mapping in handler)"
|
||||
continue
|
||||
}
|
||||
$ip = $drIpMap[$vm]
|
||||
|
||||
# 1. DNS - delete + re-add the A record
|
||||
try {
|
||||
$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 $ip -ComputerName $dnsServer -TimeToLive 00:05:00
|
||||
$summary += "dns $vm -> $ip"
|
||||
} catch {
|
||||
$summary += "DNS! $vm $($_.Exception.Message)"
|
||||
continue
|
||||
}
|
||||
|
||||
# 2. Wait for the VM to be reachable (up to 5 minutes)
|
||||
$deadline = (Get-Date).AddMinutes(5)
|
||||
$reachable = $false
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-Connection -ComputerName $ip -Count 1 -Quiet -ErrorAction SilentlyContinue) {
|
||||
try {
|
||||
# Quick WinRM probe; succeeds when the VM has finished booting
|
||||
Invoke-Command -ComputerName $ip -ScriptBlock { $true } -ErrorAction Stop | Out-Null
|
||||
$reachable = $true
|
||||
break
|
||||
} catch { Start-Sleep -Seconds 10 }
|
||||
} else {
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
}
|
||||
if (-not $reachable) {
|
||||
$summary += "wait! $vm not reachable after 5 minutes"
|
||||
continue
|
||||
}
|
||||
|
||||
# 3. Check + start critical services on the VM
|
||||
if ($serviceMap.ContainsKey($vm)) {
|
||||
$svcReport = Invoke-Command -ComputerName $ip -ArgumentList @(,$serviceMap[$vm]) -ScriptBlock {
|
||||
param($services)
|
||||
$report = @()
|
||||
foreach ($s in $services) {
|
||||
$svc = Get-Service -Name $s -ErrorAction SilentlyContinue
|
||||
if (-not $svc) { $report += "$s : missing"; continue }
|
||||
if ($svc.Status -ne 'Running') {
|
||||
Start-Service $s
|
||||
Start-Sleep -Seconds 2
|
||||
$svc.Refresh()
|
||||
}
|
||||
$report += "$s : $($svc.Status)"
|
||||
}
|
||||
return $report
|
||||
}
|
||||
$summary += "svc $vm : $($svcReport -join ', ')"
|
||||
} else {
|
||||
$summary += "svc $vm (no services configured)"
|
||||
}
|
||||
}
|
||||
|
||||
# 4. Notify Teams
|
||||
$teamsBody = @{
|
||||
text = "Webhook post-failover for VPG **$($body.vpg)**:`n" + ($summary -join "`n")
|
||||
} | ConvertTo-Json
|
||||
try {
|
||||
Invoke-RestMethod -Uri $teamsWebhook -Method POST -ContentType 'application/json' -Body $teamsBody | Out-Null
|
||||
} catch {
|
||||
$summary += "teams! notification failed: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Return the summary so it shows up in the webhook log + outbound callback
|
||||
$summary -join "`n"
|
||||
```
|
||||
|
||||
Two things to call out:
|
||||
|
||||
- **PowerShell remoting to the VM** uses the gMSA's network identity (or whoever the service runs as). Make sure the gMSA / service account can `Invoke-Command` to the failed-over hosts — usually that means the account is a local admin on the target VMs, or you've configured constrained delegation.
|
||||
- **WinRM** must be enabled on the failed-over VMs for the remoting calls to work. `Enable-PSRemoting` is the simplest, but most prod environments configure WinRM via Group Policy.
|
||||
|
||||
## 3. Configure the endpoint in the GUI
|
||||
|
||||
**File → New endpoint:**
|
||||
|
||||
| Section | Setting | Value |
|
||||
|---|---|---|
|
||||
| Identity | Slug | `post-failover` |
|
||||
| Identity | Description | "Zerto post-recovery: DNS + service checks" |
|
||||
| Auth | Mode | **Bearer** |
|
||||
| Auth | Bearer secret | generate a 32-byte random string; copy it for the Zerto script's token file |
|
||||
| Allowed clients | (one per line) | `10.0.0.0/8` *(your ZVM's network)* |
|
||||
| Executor | Type | **Windows PowerShell** |
|
||||
| Executor | Script path | `C:\Scripts\post-failover-handler.ps1` |
|
||||
| Data passing | JSON body to stdin | ✓ |
|
||||
| Run as | Identity | **Service** if the service runs under a gMSA with the right rights, otherwise **SpecificUser** with a delegated account |
|
||||
| Response | Mode | **Async** ↠critical: this is what makes the Zerto script non-blocking |
|
||||
| Response | Timeout (sec) | `600` *(this is the cap on the long-running handler script, not the Zerto-facing response)* |
|
||||
| Response | Fail on non-zero exit | unticked *(async hooks have no caller to receive a 502)* |
|
||||
|
||||
Save. Right-click the row → **Copy URL** to grab `http://webhook.dr.contoso.local:8080/hook/post-failover` and paste it into `$WebhookUrl` at the top of the Zerto-side script.
|
||||
|
||||
> **Why Bearer instead of HMAC?** Both work. Bearer is simpler — drop the token in a file on the ZVM that's readable by the ZVM service account and you're done. HMAC requires the Zerto-side script to compute a signature, which is doable but adds a few lines of code. Pick what fits your environment.
|
||||
|
||||
## 4. Wire up the bearer token
|
||||
|
||||
Place the bearer token in a file the ZVM service account can read (and nobody else):
|
||||
|
||||
```powershell
|
||||
# on the ZVM, from elevated PowerShell
|
||||
$token = (New-Guid).ToString('N') # or paste the value from the GUI
|
||||
$tokenPath = 'C:\ProgramData\Zerto\webhook-token.txt'
|
||||
$token | Out-File -LiteralPath $tokenPath -Encoding utf8 -NoNewline
|
||||
icacls $tokenPath /inheritance:r /grant 'NT SERVICE\Zerto Online Services:R' 'BUILTIN\Administrators:F' /T
|
||||
```
|
||||
|
||||
Adjust the service principal name to whatever Zerto runs as on your version. The script reads from this path automatically; no change needed in the script itself.
|
||||
|
||||
## 5. Test before going live
|
||||
|
||||
In a maintenance window, fire the webhook by hand:
|
||||
|
||||
```powershell
|
||||
# from any machine that can reach the webhook server
|
||||
$body = @{
|
||||
operation = 'test'
|
||||
vpg = 'SmokeTest'
|
||||
timestamp = (Get-Date).ToUniversalTime().ToString('o')
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
curl.exe --silent --show-error --max-time 10 -X POST `
|
||||
-H "Authorization: Bearer paste-the-token" `
|
||||
-H "Content-Type: application/json" `
|
||||
-d $body `
|
||||
http://webhook.dr.contoso.local:8080/hook/post-failover
|
||||
```
|
||||
|
||||
You'll get back `{"runId":"…","accepted":true}` immediately. Open the Webhook Server GUI and watch the log panel — within 30 seconds or so you'll see lines for the run. Confirm DNS records updated, services on each VM ended in `Running`, and the Teams notification arrived.
|
||||
|
||||
## Variations
|
||||
|
||||
### Different actions for failover vs. failback
|
||||
|
||||
Pass an `operation` field in the body and branch on it. The Zerto-side script already sends `operation = 'failover'`. Add a separate post-failback script (or detect from `$env:ZertoOperationType`) that sends `operation = 'failback'` and have the handler revert DNS to production IPs.
|
||||
|
||||
### Per-VPG endpoints
|
||||
|
||||
If you want fine-grained access control or different actions per VPG, create one endpoint per VPG (`post-failover-app`, `post-failover-db`, …) and give each its own bearer token. The GUI handles dozens of endpoints fine.
|
||||
|
||||
### Audit trail to a SIEM
|
||||
|
||||
Each endpoint can have an outbound **Callback** URL. Configure it with your SIEM's HTTP collector + an HMAC secret, and every run produces a JSON record with runId, exit code, duration, stdout, and stderr — perfect for compliance.
|
||||
+78
@@ -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).
|
||||
|
||||
**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](Recipe-UI-on-Desktop).
|
||||
|
||||
## 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
|
||||
```
|
||||
@@ -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
@@ -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](Run-As-Modes) — 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
@@ -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
@@ -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: 2–10 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.
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
[Home](Home)
|
||||
|
||||
## Topical
|
||||
- [Concepts](Concepts)
|
||||
- [Installation](Installation)
|
||||
- [Upgrading](Upgrading)
|
||||
- [Uninstalling](Uninstalling)
|
||||
- [Run As Modes](Run-As-Modes)
|
||||
- [Service Account and AD](Service-Account-and-AD)
|
||||
- [Network and Security](Network-and-Security)
|
||||
- [Troubleshooting](Troubleshooting)
|
||||
|
||||
## Recipes
|
||||
- [Zerto Failover](Recipe-Zerto-Failover)
|
||||
- [GitHub HMAC](Recipe-GitHub-HMAC)
|
||||
- [UI on Desktop](Recipe-UI-on-Desktop)
|
||||
Reference in New Issue
Block a user