How to Send HMAC-Signed Outbound Webhooks to Slack, Teams, PagerDuty, and Jira
Configure outbound webhook destinations, verify the HMAC signatures on receivers, and route events with per-team destination maps — including SSRF protection, per-org rate limits, retry/backoff, and DLQ handling.
Two Notification Channels
Chainsaw has two outbound-notification mechanisms:
| Mechanism | Use for |
|---|---|
| SIEM exporters | Splunk / Sentinel / QRadar — see tutorial 58 |
| HMAC outbound webhooks | Chat platforms, ticketing, custom integrations — Slack, Teams, PagerDuty, Jira, your own receiver |
This tutorial covers the second. The same primitive powers the per-team destination map for ActionNotifyOwner (see tutorial 61), so getting it right pays off twice.
Prerequisites
- Admin role.
- An incoming-webhook URL from the destination platform (Slack incoming-webhook, Teams connector, PagerDuty Events API, Jira Automation webhook, or your own HTTPS endpoint).
Step 1: Add a Webhook Destination
Open Settings → Webhooks → New:
| Field | Value |
|---|---|
| Name | slack-secops |
| URL | The incoming-webhook URL |
| Method | POST |
| Content type | application/json |
| HMAC secret | Auto-generated 32-byte key — used to sign every request. Save this for the receiver. |
| Event filter | The events to send. Defaults to violation.blocked, violation.quarantined, bypass.attempted. Add or remove as needed. |
| Severity floor | medium, high, critical — only fire for events at or above this severity |
| Per-org rate limit | The maximum events per minute Chainsaw will send to this destination. Defaults to 30. |
Click Test — Chainsaw fires a synthetic event. The receiver should accept it; the dashboard reports success or the HTTP error received.
Step 2: Verify the HMAC Signature
The webhook payload arrives with two headers:
X-Chainsaw-Signature: sha256=abc123...
X-Chainsaw-Timestamp: 1761969600
The signature is HMAC-SHA256(secret, "<timestamp>.<body>"). The timestamp is signed alongside the body to prevent replay attacks (you should reject any request whose timestamp is more than a few minutes in the past).
A reference verifier in Python:
import hmac
import hashlib
import time
def verify(headers, body, secret):
sig = headers["X-Chainsaw-Signature"].removeprefix("sha256=")
ts = headers["X-Chainsaw-Timestamp"]
# Reject stale requests
if abs(time.time() - int(ts)) > 300:
raise ValueError("timestamp too old")
expected = hmac.new(
secret.encode(),
f"{ts}.{body}".encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
raise ValueError("invalid signature")
For the canonical Slack / Teams / PagerDuty receivers, the platforms verify the signature on their side automatically when configured — you only write a verifier for your own receivers.
Step 3: Per-Team Destination Map
For routing violations to the team that owns the affected manifest path, configure the destination map at Admin → Org settings → Ownership:
| Owner | Slack channel | PagerDuty | |
|---|---|---|---|
@frontend-team | #frontend-secops | — | frontend-leads@example.com |
@platform-team | #platform-secops | PD-Schedule-Platform | — |
@security-team | #security | PD-Schedule-Security | secops@example.com |
When ActionNotifyOwner fires (see tutorial 61), Chainsaw resolves the manifest path’s CODEOWNERS team and dispatches to the destinations on the team’s row.
The map is also queryable via API for use by your own automation:
curl -H "Authorization: Bearer $CHAINSAW_TOKEN" \
https://chain305.com/chainproxy/api/admin/destination-map \
| jq '.owners["@frontend-team"]'
Step 4: SSRF Protection
Webhooks can be a vector for server-side request forgery — a malicious tenant configures a destination that points at an internal endpoint, and the control plane “helpfully” issues a request that exposes internal data.
Chainsaw’s webhook dispatcher refuses to issue requests to:
- RFC 1918 private addresses (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16) - Loopback addresses (
127.0.0.0/8,::1) - Link-local addresses (
169.254.0.0/16) - IPv4 mapped to IPv6 in any of the above ranges
- Cloud metadata endpoints (
169.254.169.254)
If you need a webhook to land on an internal receiver, the receiver should be exposed via your reverse proxy with a public hostname — that route runs through your normal ACLs.
To override (e.g., for a testing setup where the receiver is on the same private network):
CHAINSAW_WEBHOOK_ALLOW_PRIVATE_NETS=1
Don’t use this in production.
Step 5: Per-Org Rate Limit
The rate limit defaults to 30 events/minute per destination. The limiter is sliding-window — once you hit the limit, new events queue, and at the next window boundary the oldest events drain first.
Set the limit higher for high-volume destinations or lower for chat platforms that throttle aggressively:
| Platform | Recommended limit |
|---|---|
| Slack incoming-webhook | 30 / min (Slack’s documented limit) |
| Teams connector | 60 / min |
| PagerDuty | 120 / min |
| Custom HTTPS receiver | as high as your receiver can handle |
Step 6: Retry & DLQ
A failed webhook (any 4xx other than 429, any 5xx, network timeout) retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | 30s |
| 3 | 2m |
| 4 | 8m |
| 5 | 32m |
After 5 failed attempts, the event lands in the dead-letter queue at Settings → Webhooks → Dead-letter queue. From there you can:
- Inspect the failure reason (HTTP code, body, network error).
- Edit the destination and replay.
- Discard if the event is no longer relevant.
The DLQ has a default retention of 30 days — events older than that are automatically discarded.
Step 7: Receiver Patterns
Slack incoming-webhook
The simplest setup. Slack’s incoming-webhook URL accepts a JSON payload directly. Chainsaw maps event fields to Slack blocks automatically — your channel sees a card with the package, version, policy, and action.
Microsoft Teams connector
Teams expects MessageCard or AdaptiveCard JSON. The same automatic mapping applies; the resulting card includes “Investigate” and “Create exception” deep links to the Chainsaw dashboard.
PagerDuty Events API
Use the Events V2 endpoint (https://events.pagerduty.com/v2/enqueue). Chainsaw maps severity → PagerDuty severity, event_id → dedup key, and policy + package → summary.
Jira Automation webhook
The webhook URL goes into a Jira automation rule. Chainsaw posts the event payload; the rule converts it into a ticket. Jira’s automation language reads event.package, event.policy, etc. directly from the JSON.
Custom HTTPS receiver
The most flexible. You receive the JSON envelope, verify the HMAC, do whatever you want. The reference verifier in Step 2 is the starting point.
Step 8: Common Operational Issues
| Symptom | Likely cause | Fix |
|---|---|---|
| All events landing in DLQ | Bad HMAC secret on receiver, or signature verification rejecting based on stale timestamp | Re-paste the secret. Confirm the receiver compares timestamps with a reasonable skew. |
| Some events missing | Severity floor filtering them out | Lower the severity floor. |
| Webhook fires but Slack channel sees nothing | Slack channel was renamed; webhook URL is stale | Mint a new incoming-webhook URL in Slack and update the destination. |
| Rate limit alarms | Genuine event spike, or noisy policy | Investigate the spike via the dashboard; tune the policy that’s flapping. |
| Internal receiver not reachable | SSRF protection refusing the destination | Expose the receiver via a public hostname (right answer) or set CHAINSAW_WEBHOOK_ALLOW_PRIVATE_NETS=1 (lab only). |
Verification
- Test button on the destination fires a synthetic event that the receiver accepts.
- A real violation produces a notification in the destination within seconds.
- The HMAC verifier on a custom receiver accepts genuine requests and rejects requests with tampered bodies.
- After temporarily breaking the receiver’s network, events queue and then drain when restored.
- Five consecutive failures land an event in the DLQ.
Related Topics
- How to Send Violations to Splunk / Sentinel / QRadar — the SIEM-specific cousin of this tutorial.
- How to Route Violations to Owners with ActionNotifyOwner — the per-team destination map in action.
- How to Use Audit Logs to Track Consumption — every webhook dispatch is recorded in the audit log.