Developer Tools AWS

webhook on AWS User Guide

| Product: webhook on AWS

Overview

This guide covers deploying and operating webhook on AWS from the cloudimg AWS Marketplace listing. webhook (the open source adnanh/webhook) is a lightweight, configurable server written in Go that lets you create HTTP endpoints, called hooks, which run configured commands when they are called. It is the simple way to wire an external event to an action on your server: a Git push triggering a deployment, a CI job triggering a build step, or a monitoring alert triggering a remediation script.

The image installs the pinned, checksum-verified webhook 2.8.3 official release binary (a single Go binary) and runs it under systemd as a dedicated non-root webhook user, bound to the loopback interface on 127.0.0.1:9000. nginx sits in front of it on port 80 and reverse-proxies to it, so webhook itself is never exposed directly.

Secure by default. Because a webhook runs commands in response to HTTP requests, the shipped posture is locked down. The image ships exactly one demonstration hook whose command is a fixed, non-parameterised script that echoes a static message and receives no request-supplied data, so a caller can never inject a command. That hook is gated by a trigger rule requiring a per-instance secret that is generated on the instance's first boot and never baked into the image: a request without the correct X-Webhook-Token header is rejected with HTTP 403 and the command is not run.

What is included:

  • webhook 2.8.3 (single Go binary), run under systemd as webhook.service as a dedicated non-root user, bound to 127.0.0.1:9000 and hardened with systemd sandboxing (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp)

  • nginx on port 80 reverse-proxying to the loopback webhook endpoint (webhook is never exposed directly)

  • One ready-to-call demo hook, gated by a per-instance secret, whose command is fixed and non-parameterised

  • A per-instance random trigger secret generated on first boot into /etc/webhook/hooks.json, with a ready-to-run authorized curl written only to /root/webhook-credentials.txt (0600 root)

  • Ubuntu 24.04 LTS base with the latest security patches applied at build time and unattended security updates enabled

  • 24/7 cloudimg support

Connecting to your instance

webhook is a headless service: you administer it over SSH and call your hooks over HTTP. SSH in as the default login user for the operating system variant you launched.

OS variant SSH login user Example
Ubuntu 24.04 ubuntu ssh -i your-key.pem ubuntu@<instance-public-ip>

The instance security group must allow TCP 22 (admin SSH) and TCP 80 (the webhook endpoint via nginx) from the networks your administrators and callers connect from.

Prerequisites

  • An AWS account subscribed to the webhook listing on AWS Marketplace, and an EC2 key pair in the target region

  • A client that can make HTTP requests (curl, a CI/CD runner, a monitoring tool, or another service's outgoing webhook)

Recommended instance type: m5.large (2 vCPU, 8 GB RAM) is ample for a webhook server. webhook is lightweight; hooks run as short-lived child processes, so size the instance for whatever your hook commands actually do.

Step 1: Launch from AWS Marketplace

Subscribe to webhook on AWS Marketplace (published by cloudimg), then launch the Ubuntu 24.04 AMI with your key pair and a security group that opens ports 22 and 80 as described above. First boot generates the per-instance trigger secret, writes the real hooks configuration, and starts the service a few seconds after the instance reaches the running state.

Step 2: Confirm the service is running

SSH in and confirm webhook and nginx are active and listening. webhook owns the loopback port 127.0.0.1:9000; nginx owns the public port 80 and reverse-proxies to it.

sudo systemctl start webhook nginx
sleep 2
systemctl is-active webhook nginx
/usr/local/bin/webhook -version
sudo ss -tlnp | grep -E ':9000|:80 '

The webhook and nginx services reporting active under systemd, webhook version 2.8.3, and the loopback 127.0.0.1:9000 backend behind the nginx port 80 reverse proxy

webhook is running as the non-root webhook user, bound to loopback only. Only nginx on port 80 faces the network.

Step 3: Retrieve your per-instance trigger secret

The image bakes no trigger secret. On first boot a unique random secret is generated for this instance and written, along with the endpoint URL and a ready-to-run authorized curl, to a root-only file:

sudo cat /root/webhook-credentials.txt

The file looks like this (the secret is unique to your instance):

webhook.host=<instance-public-ip>
webhook.url=http://<instance-public-ip>/hooks/demo
webhook.token=<64-hex-character per-instance secret>

Keep this file secret; it is readable only by root (0600).

Step 4: Trigger the demo hook (authorized)

The shipped demo hook is gated by the per-instance secret. Send it in the X-Webhook-Token header and the configured command runs and returns its output. Read the secret into a shell variable so it never appears on screen:

TOKEN=$(sudo grep '^webhook.token=' /root/webhook-credentials.txt | cut -d= -f2)
curl -sS -X POST -H "X-Webhook-Token: ${TOKEN}" http://127.0.0.1/hooks/demo

You get HTTP 200 and the demo command's own output:

cloudimg webhook demo hook executed successfully at 2026-07-23T14:35:21Z on host ip-172-31-82-198.

An authenticated request with the correct X-Webhook-Token triggering the demo hook, returning HTTP 200 and the demo command output

From a remote client, replace 127.0.0.1 with your instance's public IP (http://<instance-public-ip>/hooks/demo) and make sure the security group allows port 80 from your network.

Step 5: Confirm anonymous requests are rejected

A request without the correct X-Webhook-Token is rejected with HTTP 403 and the command is never executed:

curl -sS -o /dev/null -w 'HTTP %{http_code}\n' -X POST http://127.0.0.1/hooks/demo
HTTP 403

A request with no X-Webhook-Token refused with HTTP 403; the per-instance secret is stored only in a root-only 0600 file

Step 6: Understand the secure-by-default posture

Inspect the shipped hook configuration. There is exactly one hook, demo; its command is a fixed script that takes no request data, and it carries a trigger-rule requiring the per-instance secret (shown redacted here):

sudo sed -E 's/("value": ").*/\1<per-instance-secret>",/' /etc/webhook/hooks.json
[
  {
    "id": "demo",
    "execute-command": "/usr/local/bin/webhook-demo.sh",
    "command-working-directory": "/var/lib/webhook",
    "http-methods": ["POST"],
    "include-command-output-in-response": true,
    "trigger-rule-mismatch-http-response-code": 403,
    "trigger-rule": {
      "match": {
        "type": "value",
        "value": "<per-instance-secret>",
        "parameter": { "source": "header", "name": "X-Webhook-Token" }
      }
    }
  }
]

The shipped hooks.json: one demo hook with a fixed non-parameterised command, gated by a per-instance secret matched from the X-Webhook-Token header

Note there is no pass-arguments-to-command and no pass-file-to-command: no request-supplied data ever reaches a shell, so a caller cannot inject a command. Keep this posture when you define your own hooks.

Step 7: Define your own hook

Add your own hooks by editing /etc/webhook/hooks.json and restarting the service. Write the command your hook should run as its own script, then reference it from a hook that keeps the same secure posture: a fixed command, gated by a secret.

For example, to add a deploy hook that runs a deployment script, first create the script:

sudo tee /usr/local/bin/my-deploy.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
# your deployment steps here
echo "deploy completed"
EOF
sudo chmod 0755 /usr/local/bin/my-deploy.sh

Then add a hook that runs it in /etc/webhook/hooks.json, reusing the same per-instance secret in a new trigger rule, and restart webhook:

sudo systemctl restart webhook

Every hook you add should keep a trigger-rule (so it cannot be called anonymously) and a fixed, non-parameterised command (so no request data reaches a shell). See the webhook hook definition documentation for the full set of options, including matching HMAC signatures from GitHub, GitLab and other providers.

Step 8: Expose it safely on the internet

webhook is fronted by nginx on plain HTTP port 80. For internet exposure, put TLS in front of it: point your own domain at the instance and add an HTTPS server block to nginx with a certificate (for example from Let's Encrypt), or place the instance behind an Application Load Balancer that terminates TLS. Restrict the security group to the source networks that need to call your hooks, and rely on the per-hook trigger secret (or an HMAC signature rule) for authentication.

Troubleshooting

  • The demo trigger returns 403 even with the token: confirm you are sending the exact value from /root/webhook-credentials.txt in the X-Webhook-Token header, and that you are using POST.
  • Connection refused on port 80: check systemctl is-active nginx webhook and that the security group allows port 80 from your source.
  • webhook will not start: inspect sudo journalctl -u webhook.service -n 50; a malformed /etc/webhook/hooks.json is the usual cause after an edit. Validate your JSON before restarting.

Support

This image is published by cloudimg and includes 24/7 support. webhook is open source software distributed under the MIT license by Adnan Hajdarevic; cloudimg is not affiliated with or endorsed by the webhook project. For product deployment questions, contact cloudimg support at support@cloudimg.co.uk.