webhook
Overview
webhook is a lightweight, configurable tool that creates HTTP endpoints (hooks) which run configured commands when they are called. It is the simple way to connect an external event to an action on your server: a Git push triggering a deployment, a build job triggering a packaging step, or a monitoring alert triggering a remediation script.
This guide covers deploying and configuring webhook on Ubuntu 24.04 on Azure from the SecureAnyCloud Azure Marketplace image. The image installs the official webhook binary (pinned at build) and runs it as the non-root webhook user under systemd. It is bound to loopback behind an nginx reverse proxy on port 80, so an endpoint runner is listening within minutes of launch.
Because webhook runs commands in response to HTTP requests, an open or careless configuration is a remote code execution risk, so the image is locked down by default. It ships exactly one demonstration hook. Its command is a fixed, non-parameterised script that receives no request-supplied data, so a caller can never inject a command through it.
What you get:
webhook(official pinned binary), run under systemd as the unprivileged webhook user (webhook.service). It is hardened with systemd sandboxing (ProtectSystem=strict,NoNewPrivileges, a single writable path) and bound to loopback rather than exposed directly.- An nginx reverse proxy on port 80 in front of the loopback-bound endpoint runner (
nginx.service). - One demo hook at
/hooks/demothat runs a fixed, non-parameterised command. It is gated by a trigger rule that requires a secret matched from theX-Webhook-Tokenrequest header. - A per-VM trigger secret generated on first boot, written to a root-only credentials file and never baked into the image. A request without the correct secret is rejected with an HTTP 403, and the command is not run.
- A shipped round-trip self test that proves an authorized request runs the command and returns its output, and that an unauthorized request is rejected.
- Unattended security upgrades left enabled, so the server keeps receiving patches.
Prerequisites
- An active Azure subscription, an SSH public key, and a VNet and subnet in the target region.
- A subscription to the webhook listing on Azure Marketplace.
- Network Security Group rules allowing TCP 22 (admin) and TCP 80 (webhook via nginx) from the clients that will call your hooks.
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for light use. Busy endpoints should use Standard_D2s_v5 or larger.
Deploy from the Azure Portal
Search for webhook in Azure Marketplace, select the SecureAnyCloud publisher, and click Create. Configure the Network Security Group to allow TCP 80 (webhook via nginx) from the systems that will call your hooks, and TCP 22 for administration. For internet exposure, front port 80 with TLS (see Step 8).
Deploy from the Azure CLI
RG="webhook-prod"; LOCATION="eastus"; VM_NAME="webhook1" IMAGE_URN="sac1647283583153:webhook-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION"
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:webhook-ubuntu-24-04:default:latest
az vm create \
--resource-group "$RG" --name "$VM_NAME" \
--image "$IMAGE_URN" \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values "$SSH_KEY" \
--public-ip-sku Standard
# Open the webhook and admin ports on the VM's NSG:
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 80 --priority 1001
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22 --priority 1002First boot
On first boot, the image resolves the VM public IP, generates a per-VM trigger secret, writes the real hooks.json with that secret embedded in the demo hook, and starts webhook behind nginx. This completes within a minute. SSH in as azureuser and read the per-VM details, including the demo endpoint URL and the trigger secret:
sudo cat /root/webhook-credentials.txtThe file contains the endpoint (webhook.url), the host (webhook.host), the secret (webhook.token) and a ready-to-run authorized curl command. Keep it secret. It is readable only by root.
Confirm the server is running
Confirm that webhook.service and nginx.service are active. webhook is bound to loopback on 9000, and nginx serves the reverse proxy on 80.
systemctl is-active webhook.service nginx.service /usr/local/bin/webhook -version ss -tln | grep -E '127.0.0.1:9000|:80 'Trigger the demo hook with the round trip self test
The image ships a self test. It reads the per-VM secret, sends an authorized request to the demo hook through nginx, and confirms the command ran and its output was returned. It also confirms that an unauthorized request is rejected with an HTTP 403. Run it to prove the endpoint runner is healthy end to end:
sudo bash /usr/local/lib/sac/webhook-roundtrip.shTrigger the demo hook yourself
Send an authorized request with the per-VM secret in the X-Webhook-Token header, and webhook runs the demo command and returns its output. Send the same request without the secret, and it is rejected with an HTTP 403 and the command does not run.
sudo bash -c 'TOKEN=$(grep ^webhook.token= /root/webhook-credentials.txt | cut -d= -f2-); \
echo "--- authorized request (with the secret): ---"; \
curl -sS -X POST -H "X-Webhook-Token: $TOKEN" http://127.0.0.1/hooks/demo; echo; \
echo "--- unauthorized request (no secret) returns HTTP: ---"; \
curl -sS -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1/hooks/demo'From another machine, call the demo endpoint over the network using the URL and secret from the credentials file:
curl -X POST -H 'X-Webhook-Token: <WEBHOOK_TOKEN>' <WEBHOOK_URL>Define your own hook
Hooks are defined in /etc/webhook/hooks.json (webhook also supports YAML). Each hook has an id (which becomes /hooks/<id>), an execute-command, and optional rules.
Add a hook that runs your own script, and keep the two habits the demo hook models: do not pass request data to the command, and gate it with a secret. Edit the file, then restart the service:
[ { "id": "deploy", "execute-command": "/usr/local/bin/my-deploy.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": "<WEBHOOK_TOKEN>", "parameter": { "source": "header", "name": "X-Webhook-Token" } } } } ]sudo systemctl restart webhookYour new hook is now live at http://<WEBHOOK_HOST>/hooks/deploy and callable only with the secret. Write my-deploy.sh so that it takes no arguments from the request, exactly as the shipped /usr/local/bin/webhook-demo.sh does.
Front the server with TLS for internet exposure
Port 80 speaks plain HTTP. For exposure to the public internet, terminate TLS in front of webhook. The reverse proxy is already nginx, so add a TLS server block with your certificate and key (for example from your own domain or a certificate authority) and redirect port 80 to 443.
The nginx site lives at /etc/nginx/sites-available/sac-webhook. Add a listen 443 ssl; server that proxies to http://127.0.0.1:9000 just as the port 80 server does, then reload nginx:
sudo nginx -t && sudo systemctl reload nginxAfter that, call your hooks with https:// instead of http://.
Security recommendations
Restrict the NSG. Allow TCP 80 only from the systems that call your hooks, and TCP 22 for administration only.
Front public exposure with TLS. Terminate TLS in nginx (Step 8) whenever the endpoint is reachable from the internet.
Never pass request data into a command. Do not configure pass-arguments-to-command or pass-file-to-command with untrusted request fields. Keep every hook command fixed and non-parameterised, as the shipped demo command is.
Gate every hook with a secret or signature. Require a per-hook secret in a header (as the demo hook does), or verify an HMAC signature such as a GitHub X-Hub-Signature-256. Never ship a hook with no trigger rule.
Keep the credentials secret. The demo hook secret lives only in /root/webhook-credentials.txt (root only) and is unique to this VM. hooks.json is readable only by root and the webhook group.
Keep the OS and webhook patched. Unattended security upgrades remain enabled on the running VM.
Support and Licensing
webhook is developed by Adnan Hajdarevic (adnanh) and distributed under the MIT license. This SecureAnyCloud image bundles the unmodified official webhook binary. SecureAnyCloud provides the packaging, the secure-by-default first boot (per-VM trigger secret, a fixed non-parameterised demo command, a non-root hardened service behind nginx), the systemd integration, and 24/7 support with a guaranteed 24-hour response SLA.
SecureAnyCloud is not affiliated with or endorsed by the webhook project. webhook is a mark of its respective owner and is used here only to identify the software.
Deploy on Azure
Find webhook on Ubuntu 24.04 LTS in Azure Marketplace, published by SecureAnyCloud. Deploy from the Portal or the Azure CLI as shown above.
Need Help?
Email sales@secureanycloud.com for deployment help, configuration questions or licensing enquiries.
Deploy on Azure
Launch webhook HTTP Endpoint Command Runner on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com
Overview
webhook (the open source adnanh/webhook) is a lightweight, configurable server written in Go that creates 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.
This guide covers deploying and operating webhook on AWS from the SecureAnyCloud AWS Marketplace listing. 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.
Because webhook runs commands in response to HTTP requests, the image is locked down by default. It ships exactly one demonstration hook. Its 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.
What you get:
- 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 with a fixed, non-parameterised command. It is gated by a trigger rule requiring a per-instance secret.
- A per-instance random trigger secret, generated on first boot into
/etc/webhook/hooks.jsonand never baked into the image. A ready-to-run authorizedcurlis written only to/root/webhook-credentials.txt(0600root). A request without the correctX-Webhook-Tokenheader is rejected with HTTP 403, and the command is not run. - An Ubuntu 24.04 LTS base with the latest security patches applied at build time and unattended security updates enabled.
- 24/7 SecureAnyCloud support.
Connecting to your instance
webhook is a headless service. You administer it over SSH and call your hooks over HTTP.
On Ubuntu 24.04, SSH in as the ubuntu user, for example with 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, and hooks run as short-lived child processes, so size the instance for whatever your hook commands actually do.
Launch from AWS Marketplace
Subscribe to webhook on AWS Marketplace (published by SecureAnyCloud), then launch the Ubuntu 24.04 AMI with your key pair and a security group that opens ports 22 and 80 as described above. A few seconds after the instance reaches the running state, first boot generates the per-instance trigger secret, writes the real hooks configuration and starts the service.
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 runs as the non-root webhook user and is bound to loopback only. Only nginx on port 80 faces the network.
Retrieve your per-instance trigger secret
The image bakes no trigger secret. On first boot, a unique random secret is generated for this instance. It is written to a root-only file, along with the endpoint URL and a ready-to-run authorized curl:
sudo cat /root/webhook-credentials.txtThe 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).
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/demoThe response is HTTP 200 with the demo command's own output:
sac 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). Make sure the security group allows port 80 from your network.
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/demoHTTP 403Understand 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 hook has 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.
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.shThen add a hook that runs the script in /etc/webhook/hooks.json, reusing the same per-instance secret in a new trigger rule, and restart webhook:
sudo systemctl restart webhookEvery hook you add should keep a trigger-rule, so it cannot be called anonymously. It should also keep 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.
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 that you send the exact value from
/root/webhook-credentials.txtin theX-Webhook-Tokenheader, and that you usePOST. - Connection refused on port 80. Check
systemctl is-active nginx webhook, and check 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, so validate your JSON before restarting.
Support
This image is published by SecureAnyCloud and includes 24/7 support. webhook is open source software distributed under the MIT license by Adnan Hajdarevic. SecureAnyCloud is not affiliated with or endorsed by the webhook project. For deployment questions, contact SecureAnyCloud support at sales@secureanycloud.com.
Deploy on AWS
Launch webhook on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com