webhook on AWS User Guide
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.serviceas a dedicated non-root user, bound to127.0.0.1:9000and 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 authorizedcurlwritten only to/root/webhook-credentials.txt(0600root) -
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 '

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.

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

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" }
}
}
}
]

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.txtin theX-Webhook-Tokenheader, and that you are usingPOST. - Connection refused on port 80: check
systemctl is-active nginx webhookand 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.jsonis 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.