webhook on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of webhook on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. webhook is a lightweight, configurable tool that lets you create 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.
The image installs the official webhook binary (pinned at build) and runs it as the non root webhook user under systemd, bound to loopback behind an nginx reverse proxy on port 80, so an endpoint runner is listening within minutes of launch.
Security by design — a command runner is locked down by default. Because webhook runs commands in response to HTTP requests, an open or careless configuration is a remote code execution risk. This image ships exactly one demonstration hook whose command is a fixed, non parameterised script that receives no request supplied data, so a caller can never inject a command through it.
Security by design — the demo hook needs a per VM secret. The demo hook is gated by a trigger rule that requires a secret matched from the X-Webhook-Token request header. That secret is generated uniquely on this machine's first boot and never baked into the image. A request without the correct secret is rejected with an HTTP 403 and the command is not run.
Security by design — least privilege. webhook runs as a dedicated non root user, hardened with systemd sandboxing (ProtectSystem=strict, NoNewPrivileges, a single writable path), and is bound to loopback behind nginx rather than exposed directly.
What is included:
-
webhook (official pinned binary), run under systemd as the unprivileged
webhookuser (webhook.service) -
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, gated by a per VM secret -
A per VM trigger secret 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 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
-
Active Azure subscription, SSH public key, VNet + subnet in the target region
-
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.
Step 1: Deploy from the Azure Portal
Search webhook in Marketplace, select the cloudimg 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).
Step 2: Deploy from the Azure CLI
RG="webhook-prod"; LOCATION="eastus"; VM_NAME="webhook1"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/webhook-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
# 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 1002
Step 3: First 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.txt
The 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.
Step 4: Confirm the server is running
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 '

Step 5: Trigger the demo hook with the round trip self test
The image ships a self test that reads the per VM secret, sends an authorized request to the demo hook through nginx, confirms the command ran and its output was returned, and 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/cloudimg/webhook-roundtrip.sh

Step 6: Trigger 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>

Step 7: 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 webhook
Your new hook is now live at http://<WEBHOOK_HOST>/hooks/deploy, 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.
Step 8: 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/cloudimg-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 nginx
Then call your hooks with https:// instead of http://.
Step 9: 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-commandorpass-file-to-commandwith 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.jsonis readable only by root and thewebhookgroup. -
Keep the OS and webhook patched. Unattended security upgrades remain enabled on the running VM.
Step 10: Support and Licensing
webhook is developed by Adnan Hajdarevic (adnanh) and distributed under the MIT license. This cloudimg image bundles the unmodified official webhook binary. cloudimg 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.
cloudimg 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 on the Azure Marketplace, published by cloudimg. Deploy from the Portal or the Azure CLI as shown above.
Need Help?
Email support@cloudimg.co.uk for deployment help, configuration questions, or licensing enquiries.