Developer Tools Azure

webhook on Ubuntu 24.04 on Azure User Guide

| Product: webhook HTTP Endpoint Command Runner on Ubuntu 24.04 LTS on Azure

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 webhook user (webhook.service)

  • An nginx reverse proxy on port 80 in front of the loopback bound endpoint runner (nginx.service)

  • One demo hook at /hooks/demo that 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 '

The webhook and nginx services report active under systemd showing webhook version 2.8.3, with webhook bound to loopback on port 9000 and nginx serving the reverse proxy on port 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

The cloudimg round trip self test prints ROUNDTRIP OK, confirming an authorized POST triggered the demo hook, returned the command output and demonstrably ran the command, while an unauthorized POST was rejected with HTTP 403 and did not execute

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>

An authorized POST carrying the per VM X-Webhook-Token returns the demo command output, while an unauthorized POST with no token is rejected with HTTP 403, demonstrating that the demo hook is secret gated and cannot be triggered anonymously

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-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.

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.

The per VM credentials file shows the endpoint and host with the trigger secret masked, alongside the demo hook in hooks.json confirming a fixed non parameterised command gated by a trigger rule, and webhook running as the non root webhook user bound to loopback, proving no usable trigger secret is baked into the image

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.