WebHook Tester on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of WebHook Tester on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. WebHook Tester is an open source, self hosted endpoint for capturing, inspecting and debugging inbound HTTP requests. You create a session, which gives you a unique capture URL, point a third party service at it, and every request that arrives is recorded so you can read back the exact method, headers and body that were actually sent.
It answers the question every webhook integration eventually raises: what did that service really send me? Instead of guessing from documentation, you point the sender at a capture URL and read the truth off the wire.
The image installs the official WebHook Tester binary (pinned and checksum verified at build) and runs it as the non root webhook-tester user under systemd, bound to loopback behind an nginx reverse proxy on port 80, so a capture endpoint is listening within minutes of launch.
No datastore is bundled. WebHook Tester supports memory, filesystem and Redis storage. This image uses the filesystem driver, so captures survive a reboot and there is no database to run, secure or patch.
Security by design — the auth split. A webhook receiver has an unusual requirement: the capture URL must accept unauthenticated requests, because that is the entire point, while everything else must not be world readable. This image mirrors WebHook Tester's own routing rule in nginx:
-
Capture URLs are anonymous. A request whose first path segment is a session UUID is a capture. Any third party service can POST to
http://<your-vm-ip>/<session-uuid>with no credentials and the request is recorded. -
The web UI and the management API require a password. Creating sessions, listing them, and reading captured history are all behind HTTP Basic auth using a credential generated uniquely on this VM's first boot and never baked into the image. An unauthenticated request returns HTTP 401.
-
Nobody can mint endpoints on your instance. Automatic session creation is left off, so a session can only be created through the authenticated API or UI. An anonymous caller can post to an endpoint you already made, but cannot create new ones.
Treat a capture URL as a capability. So that the UI's live feed works, the live update WebSocket for a session accepts an unauthenticated handshake — browsers cannot send credentials on a WebSocket handshake, so requiring auth there would permanently break live updates. Whoever holds a session UUID can therefore both post to that endpoint and watch that one session's live stream. They still cannot create sessions, list them, read captured history over the REST API, or open the web UI. If different senders must not see each other's traffic, give each one its own session.
What is included:
-
WebHook Tester (official pinned binary), run under systemd as the unprivileged
webhook-testeruser (webhook-tester.service) -
An nginx reverse proxy on port 80 in front of the loopback bound service (
nginx.service) -
The filesystem storage driver, so captured requests persist across reboots with no bundled database
-
A per VM web UI password generated on first boot, written to a root only credentials file and never baked into the image
-
A shipped round trip self test that proves an anonymous POST is captured and readable back, and that unauthenticated reads are rejected
-
Retention guard rails: a 7 day session lifetime, 128 requests per session and a 1 MiB request body cap
-
Unattended security upgrades left enabled so the server keeps receiving patches
Prerequisites
-
Active Azure subscription, SSH public key, VNet + subnet in the target region
-
Subscription to the WebHook Tester listing on Azure Marketplace
-
Network Security Group rules allowing TCP 22 (admin) and TCP 80 (WebHook Tester via nginx) from the services that will post to your capture URLs
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is ample for debugging and integration work. Increase only if you expect sustained high volume capture traffic.
Step 1: Deploy from the Azure Portal
Search WebHook Tester in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow TCP 80 from the services that will send you webhooks, and TCP 22 for administration. For internet exposure, front port 80 with TLS (see Step 8).
Step 2: Deploy from the Azure CLI
RG="webhook-tester-prod"; LOCATION="eastus"; VM_NAME="webhooktester1"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/webhook-tester-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az vm create \
--resource-group "$RG" --name "$VM_NAME" \
--image "$GALLERY_IMAGE_ID" \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values "$SSH_KEY" \
--public-ip-sku Standard \
--os-disk-delete-option Delete --nic-delete-option Delete
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 80 --priority 1001
Step 3: First boot
On the first boot the image generates a unique web UI password for this VM, writes it to a root only credentials file, and arms the service. Nothing is shared between VMs — two machines launched from the same image get different passwords.
Read your credentials over SSH:
sudo cat /root/webhook-tester-credentials.txt
# WebHook Tester — Per-VM Instance Info
# Generated: Sun Jul 19 07:13:34 UTC 2026
#
# There is NO default password. The value below was generated uniquely on THIS
# VM's first boot and was never baked into the image. Keep this file secret (root only).
#
webhooktester.host=<your-vm-ip>
webhooktester.url=http://<your-vm-ip>/
webhooktester.username=admin
webhooktester.password=****
Step 4: Confirm the server is running
systemctl is-active webhook-tester nginx
/usr/local/bin/webhook-tester --version
ss -tln
active
active
webhook-tester version 2.3.0 (go1.26.2)
The listener set is deliberately small: nginx on port 80 and sshd on port 22 are the only externally bound services, and WebHook Tester itself is reachable only on loopback.
LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("webhook-tester",...))
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",...))
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",...))

Step 5: Run the round trip self test
The image ships a self test that proves the product actually captures data, rather than merely that a port is open. It creates a session through the authenticated API, posts a known payload to the capture URL with no credentials, reads it back through the authenticated API and asserts the exact payload and header survived, then checks that unauthenticated reads are refused.
sudo bash /usr/local/lib/cloudimg/webhook-tester-roundtrip.sh
ROUNDTRIP OK: created a capture session through the authenticated API; an ANONYMOUS
POST to the capture URL was accepted (200) and the exact payload plus its custom
header were read back through the authenticated API; the same read without
credentials was rejected 401, a wrong password was rejected 401, and the web UI root
was rejected 401. The live-update WebSocket upgraded (101) while a plain
unauthenticated GET to the same path was rejected 401. Data capture and the auth
split are both proven end to end through nginx.

Step 6: Capture a webhook yourself
First create a capture session through the authenticated management API. Substitute your own password from Step 3.
PASSWORD="<your-password-from-step-3>"
curl -sS -u "admin:$PASSWORD" -X POST http://127.0.0.1/api/session \
-H 'Content-Type: application/json' \
-d '{"status_code":200,"headers":[],"delay":0,"response_body_base64":""}'
{
"created_at_unix_milli": 1784445686996,
"response": {
"delay": 0,
"headers": [],
"response_body_base64": "",
"status_code": 200
},
"uuid": "b6946d26-430a-4c9c-b98f-d141858c2def"
}
The uuid is your capture endpoint. Your capture URL is http://<your-vm-ip>/<uuid> — this is the address you give to the service that will send you webhooks.
Now post to it the way a third party would, with no credentials at all:
SID="<the-uuid-from-above>"
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' -X POST "http://127.0.0.1/$SID" \
-H 'Content-Type: application/json' \
-H 'X-Demo-Source: order-service' \
-d '{"event":"order.created","order_id":"A-1024","total":49.99}'
HTTP 200
Read the captured request back through the authenticated API:
curl -sS -u "admin:$PASSWORD" "http://127.0.0.1/api/session/$SID/requests"
The response records the exact method, the client address, every header the caller sent (including your custom X-Demo-Source), and the body, base64 encoded:
[
{
"captured_at_unix_milli": 1784445687047,
"client_address": "127.0.0.1",
"headers": [
{ "name": "Content-Type", "value": "application/json" },
{ "name": "User-Agent", "value": "curl/8.5.0" },
{ "name": "X-Demo-Source", "value": "order-service" }
],
"method": "POST",
"request_payload_base64": "eyJldmVudCI6Im9yZGVyLmNyZWF0ZWQiLCJvcmRlcl9pZCI6IkEtMTAyNCIsInRvdGFsIjo0OS45OX0=",
"url": "http://127.0.0.1/b6946d26-430a-4c9c-b98f-d141858c2def"
}
]
Decoding request_payload_base64 returns the payload byte for byte as it was sent:
echo 'eyJldmVudCI6Im9yZGVyLmNyZWF0ZWQiLCJvcmRlcl9pZCI6IkEtMTAyNCIsInRvdGFsIjo0OS45OX0=' | base64 -d
{"event":"order.created","order_id":"A-1024","total":49.99}

Step 7: Use the web UI
Browse to http://<your-vm-ip>/ and sign in with admin and the password from Step 3. The UI shows your current capture URL, ready made snippets for curl, wget, HTTPie, PowerShell and several languages, and the response the endpoint will return to callers.

Requests appear in the left hand column live, as they arrive, with no page reload. The image ships the WebSocket proxying that makes this work.

Selecting any request shows the full detail: path, method, originating address, timestamp, size, the complete header set and the body rendered as text or binary.

Expanding Show all headers reveals every header exactly as the sender transmitted it, which is usually where webhook debugging actually gets resolved — signature headers, content types and custom event fields.

Step 8: Front the server with TLS for internet exposure
Capture URLs are anonymous by design, so anything sent to them travels in the clear over plain HTTP. If your capture URLs are reachable from the internet, terminate TLS in front of nginx so webhook payloads and your UI password are encrypted in transit.
Point a DNS A record at the VM's public IP, then use certbot's nginx plugin, which edits the shipped server block in place and sets up renewal:
sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d webhooks.example.com
Certbot will offer to redirect HTTP to HTTPS. Accept it, then re-issue your capture URLs as https://webhooks.example.com/<uuid>.
Step 9: Security recommendations
-
Restrict port 80/443 where you can. Most webhook providers publish their egress IP ranges. Scope the Network Security Group to those ranges rather than leaving the capture port open to the whole internet.
-
Give each sender its own session. Anyone holding a session UUID can post to that endpoint and watch its live stream. Separate sessions keep separate senders from seeing each other's traffic.
-
Terminate TLS before exposing capture URLs to the internet (Step 8).
-
Keep the credentials file root only. It is written
0600 root:root; leave it that way, and rotate the password if it is ever shared. -
Treat captured data as sensitive. Webhooks routinely carry customer records, tokens and signatures. Captured requests expire after 7 days, and you can clear them at any time with Delete all requests in the UI.
-
Leave unattended upgrades enabled so the VM keeps receiving security patches.

Step 10: Support and Licensing
WebHook Tester is open source software distributed under the MIT Licence. cloudimg packages it as a preconfigured Azure image; the software itself remains under its upstream licence and is available from the upstream project.
For image specific issues, contact cloudimg support at support@cloudimg.co.uk. For questions about WebHook Tester itself, consult the upstream project documentation.