Developer Tools AWS

WebHook Tester on AWS User Guide

| Product: WebHook Tester

Overview

This guide covers the deployment and configuration of WebHook Tester on AWS using cloudimg AWS Marketplace AMIs. 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://<public-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 instance'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, because 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-tester user (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 instance 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

Connecting to your instance

WebHook Tester is offered as one master listing with an AMI variant per operating system. Connect over SSH as the default login user for the variant you launched:

OS variant SSH login user Notes
Ubuntu 24.04 LTS ubuntu nginx reverse proxy on port 80; service webhook-tester.service
ssh ubuntu@<public-ip>

Prerequisites

  • An AWS account subscribed to the WebHook Tester listing on AWS Marketplace

  • A VPC with a public subnet in your target region, and an EC2 key pair for SSH

  • A security group allowing inbound TCP 22 (admin) and TCP 80 (WebHook Tester via nginx) from the services that will post to your capture URLs

Recommended instance type: m5.large (2 vCPU, 8 GB RAM) is ample for debugging and integration work. Increase only if you expect sustained high volume capture traffic.

Step 1: Deploy from AWS Marketplace

Find the WebHook Tester listing in AWS Marketplace, choose the cloudimg publisher, and click Continue to Subscribe, then Continue to Configuration and Continue to Launch. Pick your instance type, VPC subnet and key pair. Configure the 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 AWS CLI

aws ec2 run-instances \
  --image-id <ami-id> \
  --instance-type m5.large \
  --key-name <your-key-pair> \
  --security-group-ids <your-sg-id> \
  --subnet-id <your-public-subnet-id> \
  --associate-public-ip-address \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=webhook-tester}]'

The AMI id for your chosen OS variant and region is shown on the Marketplace launch page. Open TCP 80 and TCP 22 in the referenced security group.

Step 3: First boot

On the first boot the image generates a unique web UI password for this instance, resolves the instance public IPv4 via EC2 IMDSv2, writes both to a root only credentials file, and arms the service. Nothing is shared between instances, two machines launched from the same AMI get different passwords.

Read your credentials over SSH:

sudo cat /root/webhook-tester-credentials.txt
# WebHook Tester - Per-VM Instance Info
# Generated: Tue Jul 21 21:30:00 UTC 2026
#
# There is NO default password. The value below was generated uniquely on THIS
# instance's first boot and was never baked into the image. Keep this file secret (root only).
#
webhooktester.host=<public-ip>
webhooktester.url=http://<public-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 port 8080.

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

The following self contained snippet reads the per instance password from the credentials file, creates a capture session through the authenticated management API, posts a request to it the way a third party service would (with no credentials at all), and reads the captured request back. Run it on the instance over SSH:

PASSWORD="$(sudo grep '^webhooktester.password=' /root/webhook-tester-credentials.txt | cut -d= -f2-)"
SID="$(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":""}' \
  | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1)"
echo "Capture session: $SID"
curl -sS -o /dev/null -w 'anonymous POST -> 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}'
curl -sS -u "admin:$PASSWORD" "http://127.0.0.1/api/session/$SID/requests"

The SID is your capture endpoint. Your capture URL is http://<public-ip>/<SID>, the address you give to the service that will send you webhooks. The anonymous POST returns HTTP 200, and the authenticated read 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/<SID>"
    }
]

Decoding request_payload_base64 returns the payload byte for byte as it was sent.

Step 7: Use the web UI

Browse to http://<public-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.

The WebHook Tester web interface after signing in, showing the unique capture URL for the current session with copy and open actions, ready made request snippets for curl, wget, HTTPie and PowerShell, code examples in several languages, and the configurable response status code and body

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.

Three webhooks captured live in the WebHook Tester interface, listed in the left column with their source address, POST method and arrival time, and the selected request showing its details, HTTP headers and JSON body

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.

The request detail view for a captured webhook with all headers expanded, showing the complete header set including Content-Type, User-Agent, the custom X-Github-Event header and the forwarded client address, next to the decoded JSON request body

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 instance'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/<session-uuid>.

Step 9: Security recommendations

  • Restrict port 80 and 443 where you can. Most webhook providers publish their egress IP ranges. Scope the 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 instance keeps receiving security patches.

Step 10: Support and Licensing

WebHook Tester is open source software distributed under the MIT License. cloudimg packages it as a preconfigured AWS image; the software itself remains under its upstream license 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.