Skip to main content

Run a webhook collector on a public VM

Some sources push events to the collector over HTTP instead of being scraped or polled. For those webhook-based receivers, the collector must expose an endpoint that the sending service can reach over the public internet, secured with TLS.

This guide covers the infrastructure you prepare around the collector so a webhook endpoint is reachable and trusted:

  • a public, stable address — a domain name pointing at a static public IP, and
  • HTTPS with a certificate the sending service trusts.

The collector software itself is installed using the platform-specific guides — see Linux, Windows, macOS, or Kubernetes. This page focuses on the networking and certificate work that makes a push endpoint safe to expose.


How a webhook receiver works

push events (HTTPS, POST)
External service ───────────────────────────────────▶ collector.example.com
(sends webhooks) (your public VM)


webhook receiver
(listens on a TLS port)

To get there you will:

  1. Provision a public VM with a static public IP.
  2. Point a domain name at that IP (DNS A record).
  3. Open the required firewall ports.
  4. Obtain a TLS certificate for the domain.
  5. Install the collector and point the receiver at your certificate.
  6. Verify the endpoint is reachable and healthy.

Prerequisites

RequirementWhy it's needed
A public VM (any cloud or hosting provider)Hosts the collector and must be reachable from the internet.
A static / reserved public IPThe DNS record must point at an address that does not change on reboot.
A domain or subdomain you controlWebhook senders connect to a hostname; certificates are issued against it.
Ability to edit DNS records for that domainTo create the A record and, for ACME, validation records.
Inbound firewall access to the listen portSo the external service can reach the collector.
Root / sudo on the VMTo open ports, install certificates, and run the collector service.

A modern 64-bit Linux VM with 2 vCPU / 4 GB RAM is a reasonable starting point, scaled to expected event volume.


1. Point a domain at the VM (DNS)

Webhook providers connect to a hostname, not a raw IP, and TLS certificates are issued for hostnames. Create a DNS A record for a subdomain that points to the VM's static public IP.

FieldExample value
TypeA
Name / Hostcollector (becomes collector.example.com)
Value / Target203.0.113.10 (your VM's static public IP)
TTL300 (5 minutes) while setting up

Verify the record resolves before continuing:

dig +short collector.example.com
# should print your VM's public IP
tip

Use a low TTL during setup so DNS changes take effect quickly.


2. Open firewall ports

Open inbound access to the port the webhook endpoint will listen on. Use a dedicated HTTPS port.

PortPurposeRequired
Your chosen HTTPS port (e.g. 443)Inbound webhook traffic over TLSYes
80 (HTTP)Let's Encrypt HTTP-01 validation / renewal onlyOnly if using ACME HTTP-01

Open the ports in both places where applicable:

  • the cloud security group / network ACL (provider firewall), and
  • the host firewall on the VM, for example:
# ufw example
sudo ufw allow 443/tcp
sudo ufw allow 80/tcp # only if using Let's Encrypt HTTP-01

:::tip Restrict the source range If the sending service publishes a fixed set of egress IP ranges, allow only those ranges to reach your webhook port. :::


3. Obtain a TLS certificate

The receiver serves HTTPS using a certificate file, a private key file, and optionally a CA / chain file (used only when you verify client certificates, i.e. mTLS). Choose one of the approaches below.

You will end up with:

  • cert_file — the server certificate (usually the full chain: leaf + intermediates),
  • key_file — the matching private key,
  • ca_file(optional) a CA bundle for client-certificate (mTLS) verification.

Option A — Let's Encrypt (free, automated, public CA)

Best when the domain is publicly resolvable. Certificates auto-renew.

# Install certbot (package name varies by distro)
sudo apt-get update && sudo apt-get install -y certbot

# HTTP-01 validation (requires inbound port 80 open to this VM)
sudo certbot certonly --standalone -d collector.example.com

Issued files (default paths):

Certificate (chain): /etc/letsencrypt/live/collector.example.com/fullchain.pem
Private key: /etc/letsencrypt/live/collector.example.com/privkey.pem

Certbot installs a renewal timer automatically. Confirm with:

sudo certbot renew --dry-run

:::tip No inbound port 80? Use DNS-01 validation (certbot ... --preferred-challenges dns), which proves domain ownership via a DNS TXT record and works behind closed inbound ports. :::

Option B — Bring your own / commercial CA certificate

Best when your organization already has certificates from a commercial or internal CA.

  1. Generate a private key and a Certificate Signing Request (CSR):

    openssl req -new -newkey rsa:2048 -nodes \
    -keyout collector.example.com.key \
    -out collector.example.com.csr \
    -subj "/CN=collector.example.com"
  2. Submit the CSR to your CA and complete their validation. They return a signed certificate plus intermediate/chain certificates.

  3. Build the full chain the server presents (leaf first, then intermediates):

    cat collector.example.com.crt intermediate.crt > fullchain.pem

You now have fullchain.pem (certificate chain) and collector.example.com.key (private key).

:::caution Protect the private key Restrict permissions (chmod 600), never commit it to source control, and never share it with the webhook sender — the sender only needs to trust the CA, not hold your key. :::

:::note Internal / private PKI If you are issuing certificates from your own private CA with OpenSSL, the TLS and certificates guide walks through creating a CA, server, and client certificates. The same PEM artifacts apply here. :::

Common certificate handling

  • Place certificate and key files in a stable directory the collector can read, for example /etc/collector/tls/, owned by the user the collector runs as:

    sudo mkdir -p /etc/collector/tls
    sudo chmod 700 /etc/collector/tls
    sudo chmod 600 /etc/collector/tls/*.pem
  • The certificate's Common Name (CN) or Subject Alternative Name (SAN) must match the domain in your DNS record (collector.example.com).


4. Configure the webhook receiver

Install the collector with the guide for your platform, then configure the webhook receiver to use:

  • Listen endpoint — bind to all interfaces on your chosen HTTPS port, e.g. 0.0.0.0:443.
  • TLS settings — point at the files from step 3:
    • certificate / chain file (fullchain.pem),
    • private key file (privkey.pem or your .key),
    • CA file (only if you require client-certificate / mTLS authentication).
  • Path — the route the sender POSTs to (for example /events). Configure the sending service to use https://collector.example.com/<path>.
  • Authentication — enable the authentication the receiver supports (shared token / HMAC signature, or client certificates) so the endpoint never accepts anonymous writes.

:::caution Binding privileged ports If the collector runs as a non-root user it cannot bind to ports below 1024 (like 443) by default. Either grant the capability (sudo setcap 'cap_net_bind_service=+ep' <collector-binary>), front it with a reverse proxy, or use a high port (e.g. 8443) and open that port in the firewall instead. :::


5. Verify the endpoint

From a machine outside the VM, confirm TLS and reachability:

# Check the certificate is served and valid for the domain
openssl s_client -connect collector.example.com:443 -servername collector.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -dates

# Hit the health endpoint (path depends on receiver config)
curl -i https://collector.example.com/health_check

Send a test event to the configured path (include the auth headers your receiver expects):

curl -i -X POST https://collector.example.com/events \
-H "Content-Type: application/json" \
-d '{"test":"event"}'

A clean TLS handshake (no certificate warnings) plus a 2xx response confirms the endpoint is ready. Then point the external service at https://collector.example.com/<path>.


6. Renew and rotate certificates

  • Let's Encrypt: renewal is automatic via the certbot timer (certificates last ~90 days). After renewal the collector must reload the new files — restart the collector service, or use a renewal hook, so it picks up the rotated certificate.
  • Commercial / internal CA: track the expiry date and repeat step 3 (Option B) before it lapses. Replace the files in place and restart the collector.

Check the live expiry any time:

echo | openssl s_client -connect collector.example.com:443 -servername collector.example.com 2>/dev/null \
| openssl x509 -noout -enddate

Troubleshooting

SymptomLikely causeWhat to check
Connection times outFirewall/security group not open, or wrong portVerify the cloud firewall and host firewall allow the listen port; confirm the collector is listening (ss -tlnp).
certificate name mismatchCert CN/SAN ≠ domainReissue the certificate for the exact hostname in your DNS record.
unable to verify the first certificateIncomplete chainServe the full chain (leaf + intermediates), not just the leaf certificate.
DNS doesn't resolveA record missing or not propagateddig +short collector.example.com; wait for TTL; confirm it points to the static IP.
Sender reports 401/403Authentication misconfiguredConfirm the sender includes the expected token / signature or client certificate.
Collector can't bind port 443Non-root process binding a privileged portUse setcap, a high port, or a reverse proxy (see step 4).
Let's Encrypt validation failsPort 80 blocked or DNS not pointing hereOpen port 80 for HTTP-01, or switch to DNS-01 validation.

Security checklist

  • Private key stored with 600 permissions, never committed or shared.
  • Endpoint requires authentication (token / HMAC or client certificate) — no anonymous writes.
  • Firewall restricts the webhook port to the sender's IP ranges where possible.
  • TLS minimum version set to a modern baseline (TLS 1.2+).
  • Certificate auto-renewal (or an expiry reminder) is in place.
  • Only the required ports are exposed publicly.