Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<Version>0.1.1</Version>
<Version>0.1.2</Version>
<Authors>Justin Paul</Authors>
<Company>Justin Paul</Company>
<Product>Webhook Server</Product>
Expand Down
148 changes: 64 additions & 84 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,111 +1,91 @@
# 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

- **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 IP allowlist.** Restrict by IP or CIDR (IPv4 + IPv6). Empty list = open. Checked before auth.
- **Per-endpoint auth** — HMAC signature (GitHub / Stripe / Slack style), bearer token, or none.
- **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 input.** Any combination of: JSON body to stdin, query/headers as env vars, `{{template}}` arg expansion.
- **Sync or async per endpoint.** Sync returns exit code + stdout/stderr; 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).
- **Service-first.** Always-on Windows Service. The WPF GUI is a thin config/monitor client over a named pipe.
- **HTTPS optional.** Bind a `.pfx` or cert-store thumbprint from the GUI; HTTP works out of the box.
- **Secrets at rest.** Tokens and HMAC secrets are encrypted via DPAPI (LocalMachine scope) in `config.json`.
- **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 to the caller; async returns 202 immediately.
- **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.
- **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.
- **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

```
+------------------+ named pipe +------------------------------+
| WPF GUI app | <----------> | Windows Service |
| (config/monitor)| | - Kestrel: webhook listener |
+------------------+ | - Named-pipe admin server |
| - Executor pool |
| - Serilog file logging |
+------------------------------+
^
C:\ProgramData\WebhookServer\
- config.json (DPAPI-encrypted secrets)
- logs\*.log
```

## Project layout (planned)

```
WebhookServer.sln
src/
WebhookServer.Core/ class lib: models, auth, execution, storage, IPC
WebhookServer.Service/ .NET 8 Worker Service (hosts Kestrel + admin pipe)
WebhookServer.Gui/ WPF (.NET 8) MVVM config/monitor client
scripts/
install-service.ps1
uninstall-service.ps1
```

## Requirements

- Windows 10 / 11 or Windows Server 2019+
- .NET 8 SDK to build, .NET 8 Runtime (or self-contained publish) to run
- Administrator rights to install the service and to run the GUI (the admin named pipe is ACL'd to SYSTEM + Administrators)

## Building (on Windows)

```powershell
dotnet restore
dotnet build -c Release
dotnet publish src/WebhookServer.Service -c Release -r win-x64 --self-contained
dotnet publish src/WebhookServer.Gui -c Release -r win-x64 --self-contained
+------------------+ 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)
- backups\ (auto-snapshots)
- logs\ (daily rolling)
```

## 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
```
## Documentation

`scripts/install-service.ps1` will wrap this once implemented and will accept a `-ServiceAccount` parameter.
Everything you need to operate the server:

## Service account & Active Directory
- [Concepts](docs/concepts.md) — what a webhook is and how this server uses one
- [Installation](docs/installation.md) — interactive and silent install
- [Upgrading](docs/upgrading.md) — single click; what's preserved
- [Uninstalling](docs/uninstalling.md) — clean removal
- [Run As modes](docs/runas-modes.md) — Service / InteractiveUser / SpecificUser
- [Service account & Active Directory](docs/service-account-and-ad.md) — gMSA + delegated rights
- [Network & security](docs/network-and-security.md) — bind addresses, allowlists, HTTPS, secrets
- [Troubleshooting](docs/troubleshooting.md) — common errors and where to look

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:
Recipes:

- **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=`.
- [Zerto failover post-script → DNS + service checks](docs/recipes/zerto-pre-post-scripts.md) ← **canonical use case**
- [GitHub-style HMAC-signed webhook](docs/recipes/github-style-hmac.md)
- [Pop UI on the user's desktop](docs/recipes/ui-on-desktop.md)

- **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
```
A ready-to-drop-in Zerto-side script is included at [`scripts/examples/zerto-post-failover.ps1`](scripts/examples/zerto-post-failover.ps1).

Don't use `LocalService` — it has no network identity and cannot talk to a domain controller.
## Requirements

> 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.
- Windows 10 / 11 / Server 2019+
- x64
- .NET 8 SDK to build (the released installer includes everything else)

## Configuration
## Building from source

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.
```powershell
git clone https://github.com/recklessop/webhook-server.git
cd webhook-server

## Out of scope for v1
# Dev install (publishes + copies to C:\Program Files\WebhookServer + registers service)
powershell -ExecutionPolicy Bypass -File scripts\deploy.ps1

- 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.
# Or build the installer locally (requires Inno Setup 6: winget install JRSoftware.InnoSetup)
powershell -ExecutionPolicy Bypass -File scripts\build-installer.ps1
```

## License

Not yet chosen.
TBD.
32 changes: 32 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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 failover post-script → DNS + service checks](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 failover post-script → DNS + service checks](recipes/zerto-pre-post-scripts.md) ← canonical use case
- [GitHub-style HMAC-signed webhook](recipes/github-style-hmac.md)
- [Pop UI on the user's desktop](recipes/ui-on-desktop.md)

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)
77 changes: 77 additions & 0 deletions docs/concepts.md
Original file line number Diff line number Diff line change
@@ -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).
Loading
Loading