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:
webhookd1.22.0, the official release binary verified by sha256 at build time, run under systemd as the unprivilegedwebhookduser (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, 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.
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.
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 1002First 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.txtExpected 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/scriptsConfirm 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))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.shExpected 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)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/echoExpected 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 successfullyReview 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>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.shCall 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.
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 webhookdKeep /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.
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 nginxPoint your callers at your domain name over HTTPS. They then no longer need curl -k.
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.
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
Overview
webhookd is a minimalist, open-source HTTP server that turns ordinary shell scripts into webhook endpoints. You drop an executable script into a directory, and it becomes callable over HTTP. The script's standard output is streamed back to the caller in real time, either as chunked transfer encoding or as Server-Sent Events.
This guide covers deploying and operating webhookd on AWS from the SecureAnyCloud AWS Marketplace listing. Query parameters and HTTP headers from the request are passed to the script as environment variables, so a hook can react to whatever triggered it.
Use webhookd to trigger a deployment, run a maintenance job, kick off a backup, restart a service, or drive any automation from an HTTP call. The call can come from a CI/CD pipeline, a chatbot, a monitoring alert, or another service's outgoing webhook.
The image installs the pinned, checksum-verified webhookd 1.22.0 official release binary (a single Go binary). It runs under systemd as a dedicated non-root webhookd user, bound to the loopback interface on 127.0.0.1:8080. nginx terminates HTTPS on port 443 in front of it and reverse-proxies to it with response buffering disabled, so streaming and Server-Sent Events output reaches the client the instant the script produces it.
Secure by default. HTTP Basic authentication is mandatory and enforced by webhookd itself. A request with no credentials, or with the wrong password, is refused with HTTP 401, and no hook is ever run.
The shipped AMI bakes no credential. On the very first boot, the image generates a unique per-instance username and password, hashes them with bcrypt into an htpasswd file, and writes them to a root-owned 0600 file. It also generates a per-instance self-signed TLS certificate for nginx on the same first boot, so no default certificate ships inside the image.
What you get:
- webhookd 1.22.0 (single Go binary), run under systemd as
webhookd.serviceas a dedicated non-root user, bound to127.0.0.1:8080 - nginx terminating TLS on :443 with a per-instance self-signed certificate regenerated on first boot, reverse-proxying to webhookd with streaming and Server-Sent Events output forwarded unbuffered
- An unauthenticated
/healthzload balancer probe and a301redirect to HTTPS on port 80 - Mandatory HTTP Basic authentication: a unique per-instance credential generated on first boot into
/etc/webhookd/htpasswd(bcrypt), with the plaintext written only to/root/webhookd-credentials.txt(0600root) - One ready-to-call example hook,
echo, that streams a few lines so you can see the streaming behaviour immediately - 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
webhookd is a headless service: you administer it over SSH and call your hooks over HTTPS. On Ubuntu 24.04, the SSH login user is ubuntu. For example: ssh -i your-key.pem ubuntu@<public-ip>
The instance security group must allow TCP 22 (admin), TCP 443 (the webhook API over HTTPS) and TCP 80 (the HTTP-to-HTTPS redirect and the health probe). Allow them from the networks your callers and administrators connect from.
Prerequisites
- An AWS account subscribed to the webhookd listing on AWS Marketplace, and an EC2 key pair in the target region
- A client that can make authenticated HTTPS requests, such as curl, a CI/CD runner, a chatbot, or another service's webhook sender
Recommended instance type: m5.large (2 vCPU, 8 GB RAM) is ample for a webhook server. webhookd is lightweight, and hooks run as short-lived child processes, so size the instance for whatever your hook scripts actually do.
Launch from AWS Marketplace
Subscribe to webhookd 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, 80 and 443 as described above.
First boot generates the per-instance credential and TLS certificate and starts the service. This takes a few seconds after the instance reaches the running state.
Confirm the service is running
SSH in and confirm that webhookd and nginx are active and listening. webhookd owns the loopback port 127.0.0.1:8080, and nginx owns the public ports 80 and 443.
sudo systemctl start webhookd nginx sleep 2 systemctl is-active webhookd nginxRetrieve your per-instance credentials
On first boot, the unique username, password and endpoint for this instance are written to a root-owned file. Read them over SSH.
$
sudo cat /root/webhookd-credentials.txt webhookd.user=sac webhookd.pass=<your-per-instance-password> webhookd.host=<public-ip> webhookd.url=https://<public-ip>/ webhookd.example_hook=https://<public-ip>/echoThe password is unique to this instance and never leaves it. Keep the file readable only by root.
Authentication is mandatory
webhookd refuses any request that does not carry valid HTTP Basic credentials, so no unauthenticated caller can ever run a hook. A request with no credentials is refused with HTTP 401.
curl -sk -o /dev/null -w 'anonymous request without credentials: HTTP %{http_code}\n' https://localhost/echoThe image ships a fail-closed htpasswd file with zero user entries. If first boot has not yet completed, every hook request is refused rather than left open.
Call the example hook
The image includes a self-signed certificate, so pass -k to curl, or install a CA-signed certificate for your domain in production. Call the bundled echo hook with your per-instance credentials, and its output streams back line by line.
$ curl -k -u sac:<your-per-instance-password> https://<public-ip>/echo?name=sac sac webhookd example hook hook_name=echo method=GET authenticated_user= hello, sac step 1/3 working... step 2/3 working... step 3/3 working... done: hook completed successfullyFor real-time Server-Sent Events, ask for them with an Accept header.
$ curl -k -u sac:<your-per-instance-password> -H 'Accept: text/event-stream' https://<public-ip>/echoYou can prove the full authentication round trip on the instance itself, without printing any password, with the bundled proof script. It confirms that anonymous and wrong credentials are refused with 401, while the correct per-instance credential returns 200 and runs the hook. It checks both the loopback backend and the nginx TLS edge.
sudo /usr/local/sbin/webhookd-roundtrip.shAdd your own hook
To publish a new webhook, drop an executable script into the hooks directory and call it by name. A script at /opt/webhookd/scripts/deploy.sh becomes the endpoint https://<public-ip>/deploy.
$
sudo tee /opt/webhookd/scripts/deploy.sh >/dev/null <<'SCRIPT' #!/usr/bin/env bash echo "deploying ${ref:-main}..."
# your deployment commands here echo "done" SCRIPT $
sudo chmod +x /opt/webhookd/scripts/deploy.sh $
sudo chown webhookd:webhookd /opt/webhookd/scripts/deploy.sh $ curl -k -u sac:<your-per-instance-password> "https://<public-ip>/deploy?ref=release-1"Request query parameters (for example ?ref=release-1) and HTTP headers are passed to your script as environment variables, so a hook can act on the payload that triggered it.
Operating webhookd
Rotate the password at any time, then restart the service to pick it up.
$
sudo htpasswd -B /etc/webhookd/htpasswd sac $
sudo systemctl restart webhookdConfiguration lives in /etc/webhookd/webhookd.env. It holds the listen address, hook directory, htpasswd path, hook timeout and default streaming mode.
To replace the self-signed certificate with a CA-signed certificate for your domain in production, place your certificate and key in /etc/nginx/certs/ as cert.pem and key.pem. Then run sudo systemctl reload nginx.
The service runs under systemd, so it restarts automatically and logs to the journal. Check its status with this command.
systemctl status webhookd --no-pager --lines 0Security notes
- No baked-in credential. The per-instance username, password (bcrypt in
/etc/webhookd/htpasswd) and self-signed TLS certificate are all generated on first boot and are unique to this instance. - Authentication on every hook. HTTP Basic authentication is enforced by webhookd itself, and nginx terminates TLS in front of it. There is no unauthenticated path to running a hook.
- Restrict the security group. Allow ports 443 and 80 only from the networks that need to call your hooks, and port 22 only from your administrators.
- Review your hooks. Your hook scripts run as the non-root
webhookduser. Treat every hook as remotely triggerable code, and keep the scripts under your own review.
Support
webhookd is free and open-source software distributed under the MIT license. This image repackages the unmodified upstream release with SecureAnyCloud support services. SecureAnyCloud is not affiliated with or endorsed by the webhookd project or Nicolas Carlier.
For help with this image, contact SecureAnyCloud support. For questions about webhookd itself, see the upstream project at github.com/ncarlier/webhookd.
Deploy on AWS
Launch webhookd on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com