Deployment guide

webhookd

Overview

webhookd is a minimalist 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. The script output is streamed back to the caller in real time, as chunked data or Server-Sent Events (SSE).

This SecureAnyCloud image runs webhookd on Ubuntu 24.04 on Azure, deployed from the Azure Marketplace. It is a lightweight way to wire deploys, automation jobs, notifications and integrations to an incoming HTTP request without standing up a full application.

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

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 you get:

  • webhookd 1.22.0, the official release binary verified by sha256 at build time, run under systemd as the unprivileged webhookd user (webhookd.service), bound to 127.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, with the 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 self-test tool, webhookd-roundtrip.sh, that proves an unauthenticated request is refused (HTTP 401) and an authenticated request runs the hook (HTTP 200)
  • An Ubuntu 24.04 LTS base with the latest security patches applied at build time and unattended security upgrades kept enabled on the running VM
  • The Azure Linux Agent for cloud integration and SSH key injection
  • 24/7 SecureAnyCloud support with a guaranteed 24-hour response SLA

Prerequisites

  • An active Azure subscription, an SSH public key, and a VNet with a subnet in the target region
  • A subscription to the webhookd listing on Azure Marketplace
  • Network security group (NSG) rules that allow 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. For heavier hook scripts or many concurrent hooks, use Standard_D2s_v5 or larger. You can also raise the worker count with WHD_HOOK_WORKERS.

1

Deploy from the Azure Portal

In the Azure Portal, search the Marketplace for webhookd, select the SecureAnyCloud publisher, and click Create. Configure the NSG to allow TCP 443 (HTTPS API) from the networks that will call your webhooks, and TCP 22 for administration.

Keep port 443 restricted to trusted networks. webhookd runs shell scripts, so treat the endpoint as privileged.

2

Deploy from the Azure CLI

Accept the Marketplace terms, create the VM, then open ports 443 and 22 on its NSG.

RG="webhookd-prod"; LOCATION="eastus"; VM_NAME="webhookd1" IMAGE_URN="sac1647283583153:webhookd-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:webhookd-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 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
3

First boot

On first boot, the image resolves the VM address and generates a per-VM nginx TLS certificate. It also generates a unique per-VM Basic auth password and writes it to the htpasswd file. Then it 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=sac 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
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, and 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))
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. The commands below 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 sac 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)
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: sac 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
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 through the htpasswd file. The htpasswd file holds this VM's unique password as a bcrypt hash, never as 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, with the 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 sac:$2y$<bcrypt-hash-masked>
8

Add your own hooks

A hook is simply an executable script in /opt/webhookd/scripts. The URL path maps to the script name, and the .sh extension is optional in the URL. Create a script and 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. webhookd passes them 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 to the script on standard input.

9

Rotate the password or add users (optional)

The per-VM password lives, bcrypt-hashed, in /etc/webhookd/htpasswd. To rotate it or add another user, run htpasswd -B, then restart webhookd so that it reloads the file.

sudo htpasswd -B /etc/webhookd/htpasswd sac
# rotate the sac 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. Do not empty the htpasswd file back to zero users unless you intend to take the endpoint offline, because with no users every request is refused with HTTP 401.

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 private 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. They then no longer need curl -k.

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_FILE at a missing file, because that would disable authentication.

Review your hook scripts. Anything a hook script can do runs as the webhookd user. Validate query parameters and request input inside your scripts. Keep the scripts directory writable only by root and the webhookd user.

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. For production callers, replace the per-VM self-signed certificate with one for your domain (Step 10).

Keep the OS patched. Unattended security upgrades remain enabled on the running VM.

12

Support and Licensing

webhookd is free and open-source software by Nicolas Carlier and contributors, distributed under the MIT License. This SecureAnyCloud image bundles the unmodified official release binary, verified by sha256.

SecureAnyCloud provides the packaging, the secure-by-default hardening, the mandatory per-VM authentication, the nginx TLS endpoint, the self-test tool, and 24/7 support with a guaranteed 24-hour response SLA.

SecureAnyCloud 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 SecureAnyCloud. Deploy from the Azure 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 webhookd 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

Deploy on Azure