webhookd on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of webhookd on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. webhookd is a minimalist, powerful webhook server that turns ordinary shell scripts into HTTP endpoints. Drop an executable script into its scripts directory and it becomes a webhook you can trigger over HTTP, with the script output streamed back to the caller in real time as chunked data or Server Sent Events (SSE). It is a lightweight way to wire deploys, automation jobs, notifications and integrations to an incoming HTTP request without standing up a full application.
The image installs the pinned official webhookd 1.22.0 release binary, verified by sha256 at build time, and runs it under systemd as a dedicated non root webhookd user bound to the loopback interface. An nginx endpoint terminates TLS on port 443 with a per VM self signed certificate and reverse proxies to webhookd, with response buffering disabled so streaming and SSE output flows to the caller in real time. Unattended security upgrades remain enabled on the running VM so the OS keeps receiving patches.
Security by design, no unauthenticated hook execution. Running arbitrary shell scripts over HTTP is powerful, so this image makes authentication mandatory. webhookd enforces HTTP Basic authentication against an htpasswd file, and the image ships that file with zero user entries, which is fail closed: every request to a hook is refused with HTTP 401 until first boot creates a real credential. There is no way to run a hook without a valid username and password.
Security by design, no bootstrap credential. No webhook password is baked into the image. On first boot the VM generates a unique per VM password, stores it as a bcrypt hash in the htpasswd file, and writes the plaintext to a root only credentials file. Every VM you deploy gets its own password; nothing is shared and there is no default credential to leak.
What is included:
-
webhookd 1.22.0, the official release binary verified by sha256, run under systemd as the unprivileged
webhookduser (webhookd.service), bound to127.0.0.1:8080 -
An nginx TLS endpoint on port 443 with a per VM self signed certificate, reverse proxying to webhookd with streaming and SSE output preserved (response buffering disabled)
-
Mandatory HTTP Basic authentication with a unique password generated on first boot and stored (bcrypt) in
/etc/webhookd/htpasswd, plaintext in/root/webhookd-credentials.txt(root only), never baked into the image -
A
scripts/directory with one example hook (echo) that streams its output, ready for you to add your own scripts -
A shipped self test tool,
webhookd-roundtrip.sh, that proves an unauthenticated request is refused (HTTP 401) and an authenticated request runs the hook (HTTP 200) -
Ubuntu 24.04 LTS base with latest security patches applied at build time, and unattended security upgrades kept enabled
-
Azure Linux Agent for seamless cloud integration and SSH key injection
-
24/7 cloudimg support with a guaranteed 24 hour response SLA
Prerequisites
-
Active Azure subscription, SSH public key, VNet and subnet in the target region
-
Subscription to the webhookd listing on Azure Marketplace
-
Network Security Group rules allowing TCP 22 (admin) and TCP 443 (HTTPS API) from the networks that will call your webhooks
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is ample for a webhook server driving shell scripts. Workloads that run heavier hook scripts or many concurrent hooks can use Standard_D2s_v5 or larger, and can raise the worker count with WHD_HOOK_WORKERS.
Step 1: Deploy from the Azure Portal
Search webhookd in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow TCP 443 (HTTPS API) from the networks that will call your webhooks, and TCP 22 for administration. Keep 443 restricted to trusted networks; webhookd runs shell scripts, so treat the endpoint as privileged.
Step 2: Deploy from the Azure CLI
RG="webhookd-prod"; LOCATION="eastus"; VM_NAME="webhookd1"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/webhookd-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 API and admin ports on the VM's NSG (restrict 443 to your caller networks):
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 443 --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 address, generates a per VM nginx TLS certificate, generates a unique per VM Basic auth password, writes it to the htpasswd file, starts webhookd and nginx, and proves that an unauthenticated request is refused while an authenticated request runs the example hook. This completes within a minute. SSH in as azureuser and read the credentials:
sudo cat /root/webhookd-credentials.txt
Expected output (the password is unique to your VM):
webhookd.user=cloudimg
webhookd.pass=<per-VM-password>
webhookd.host=10.0.0.5
webhookd.url=https://10.0.0.5/
webhookd.example_hook=https://10.0.0.5/echo
webhookd.htpasswd=/etc/webhookd/htpasswd
webhookd.scripts_dir=/opt/webhookd/scripts
Step 4: Confirm the services are running
webhookd.service is active and owns the loopback port 8080, and nginx.service terminates TLS on port 443. webhookd is deliberately not exposed directly; all external traffic goes through the nginx TLS endpoint.
systemctl is-active webhookd.service
systemctl is-active nginx.service
sudo ss -tlnp | grep -E '127.0.0.1:8080 |:443 '
Expected output:
active
active
LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("webhookd",pid=2075,fd=3))
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=2101,fd=6))

Step 5: Call the example hook
The example hook lives at /opt/webhookd/scripts/echo.sh and is reachable at https://<host>/echo. A request without credentials is refused with HTTP 401. A request with the per VM credentials runs the hook and returns its output with HTTP 200. The certificate is self signed, so use curl -k (or trust the per VM certificate). Read the username and password from the credentials file:
WU="$(sudo grep -E '^webhookd\.user=' /root/webhookd-credentials.txt | cut -d= -f2-)"
WP="$(sudo grep -E '^webhookd\.pass=' /root/webhookd-credentials.txt | cut -d= -f2-)"
curl -k -s -o /dev/null -w 'unauthenticated request: HTTP %{http_code}\n' https://127.0.0.1/echo
curl -k -s -u "$WU:$WP" https://127.0.0.1/echo
sudo /usr/local/sbin/webhookd-roundtrip.sh
Expected output:
unauthenticated request: HTTP 401
cloudimg webhookd example hook
hook_name=echo method=GET authenticated_user=
step 1/3 working...
step 2/3 working...
step 3/3 working...
done: hook completed successfully
round-trip OK: hook 'echo' — no credentials refused (HTTP 401), wrong credentials refused (HTTP 401), correct per-VM credentials ran the hook and returned its output (HTTP 200); enforced on both the loopback backend (:8080) and the nginx TLS edge (:443)

Step 6: Stream hook output in real time
webhookd streams a hook's output to the caller as the script runs, rather than buffering until it finishes. Send Accept: text/event-stream to receive Server Sent Events, or use the default chunked mode. Because nginx runs with response buffering disabled, the lines arrive in real time as the example hook emits them.
WU="$(sudo grep -E '^webhookd\.user=' /root/webhookd-credentials.txt | cut -d= -f2-)"
WP="$(sudo grep -E '^webhookd\.pass=' /root/webhookd-credentials.txt | cut -d= -f2-)"
curl -k -N -u "$WU:$WP" -H 'Accept: text/event-stream' https://127.0.0.1/echo
Expected output (each data: line arrives as the script prints it):
data: cloudimg webhookd example hook
data: hook_name=echo method=GET authenticated_user=
data: step 1/3 working...
data: step 2/3 working...
data: step 3/3 working...
data: done: hook completed successfully

Step 7: Review the configuration and the secure defaults
webhookd is configured from /etc/webhookd/webhookd.env. It binds the loopback interface only, points at the scripts directory, and enforces Basic auth via the htpasswd file. The htpasswd file holds this VM's unique password as a bcrypt hash (never plaintext, and never baked into the image).
/usr/local/bin/webhookd -version | head -1
grep -vE '^[[:space:]]*#|^[[:space:]]*$' /etc/webhookd/webhookd.env
sudo sed -E 's/(:\$2[aby]\$).*/\1<bcrypt-hash-masked>/' /etc/webhookd/htpasswd | grep -vE '^[[:space:]]*#'
Expected output (bcrypt hash masked):
Version: v1.22.0
WHD_LISTEN_ADDR=127.0.0.1:8080
WHD_HOOK_SCRIPTS=/opt/webhookd/scripts
WHD_PASSWD_FILE=/etc/webhookd/htpasswd
WHD_HOOK_TIMEOUT=30
WHD_HOOK_DEFAULT_MODE=chunked
WHD_HOOK_LOG_DIR=/var/log/webhookd
WHD_LOG_MODULES=http,hook
WHD_LOG_LEVEL=info
cloudimg:$2y$<bcrypt-hash-masked>

Step 8: Add your own hooks
A hook is simply an executable script in /opt/webhookd/scripts. The URL path maps to the script name (the .sh extension is optional in the URL). Create a script, make it executable, and it is immediately callable with the same Basic auth credentials:
sudo tee /opt/webhookd/scripts/deploy.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
echo "deploying ${app:-demo} to ${env:-staging}"
echo "triggered by ${x_forwarded_for:-unknown}"
EOF
sudo chmod +x /opt/webhookd/scripts/deploy.sh
sudo chown webhookd:webhookd /opt/webhookd/scripts/deploy.sh
Call it with query parameters, which webhookd passes to the script as environment variables:
WU="$(sudo grep -E '^webhookd\.user=' /root/webhookd-credentials.txt | cut -d= -f2-)"
WP="$(sudo grep -E '^webhookd\.pass=' /root/webhookd-credentials.txt | cut -d= -f2-)"
curl -k -u "$WU:$WP" 'https://127.0.0.1/deploy?app=web&env=production'
webhookd also converts HTTP headers to snake case variables and adds built ins such as hook_name, hook_method, x_forwarded_for and x_webauth_user (the authenticated username). POST bodies are available on standard input to the script.
Step 9: Rotate the password or add users (optional)
The per VM password lives (bcrypt hashed) in /etc/webhookd/htpasswd. To rotate it, or to add another user, use htpasswd -B and restart webhookd so it reloads the file:
sudo htpasswd -B /etc/webhookd/htpasswd cloudimg # rotate the cloudimg password
sudo htpasswd -B /etc/webhookd/htpasswd teammate # add a second user
sudo systemctl restart webhookd
Keep /root/webhookd-credentials.txt in step with any change. Never empty the htpasswd file back to zero users unless you intend to take the endpoint offline, since with no users every request is refused with HTTP 401.
Step 10: Use a trusted TLS certificate (optional)
The image ships a per VM self signed certificate at /etc/nginx/certs/cert.pem (with the private key at key.pem). For production, replace it with a certificate for your own domain, for example one issued by Let's Encrypt or your internal CA, then reload nginx:
sudo cp your-fullchain.pem /etc/nginx/certs/cert.pem
sudo cp your-privkey.pem /etc/nginx/certs/key.pem
sudo systemctl reload nginx
Point your callers at your domain name over HTTPS, and they no longer need curl -k.
Step 11: Security recommendations
-
Restrict the NSG. Allow TCP 443 only from the networks that call your webhooks, and TCP 22 for administration only. webhookd runs shell scripts, so treat the endpoint as privileged.
-
Keep authentication on. The shipped configuration requires a valid username and password on every hook request; never point
WHD_PASSWD_FILEat a missing file, which would disable authentication. -
Review your hook scripts. Anything a hook script can do runs as the
webhookduser. Validate query parameters and request input inside your scripts, and keep the scripts directory writable only by root and thewebhookduser. -
Set a sensible timeout.
WHD_HOOK_TIMEOUT(default 30 seconds in this image) caps how long a hook may run; raise it only for hooks that genuinely need longer. -
Use a trusted certificate. Replace the per VM self signed certificate with one for your domain (Step 10) for production callers.
-
Keep the OS patched. Unattended security upgrades remain enabled on the running VM.
Step 12: Support and Licensing
webhookd is free and open source software by Nicolas Carlier and contributors, distributed under the MIT License. This cloudimg image bundles the unmodified official release binary, verified by sha256. cloudimg provides the packaging, the secure by default hardening, the mandatory per VM authentication, the nginx TLS endpoint, the shipped self test tool, and 24/7 support with a guaranteed 24 hour response SLA.
cloudimg is not affiliated with or endorsed by the webhookd project or Nicolas Carlier. webhookd is used here only to identify the software.
Deploy on Azure
Find webhookd 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.