Notifications
Configure how and when you receive alerts when tripwires are triggered.
Notification channels
Trip alerts are per user: each person chooses where their alerts go under Settings → Notifications. Alerts are sent the moment a tripwire is triggered.
A detailed alert to your account address (or a separate notification address).
Slack
One-click Connect Slack (hosted), or paste an incoming-webhook URL.
Microsoft Teams
Paste the URL of a Teams Workflows “Post to a channel when a webhook request is received” flow. Alerts arrive as an Adaptive Card.
Custom HTTPS webhook
Signed JSON to your own endpoint — SOAR, ticketing, chat-ops, a Lambda. Hosted and self-hosted. See Custom webhooks.
Self-hosted deployments can additionally forward every detection to a SIEM/SOC pipeline — see SIEM / SOC forwarding. Admins can route alerts by tripwire tag to specific channels with a severity, via PUT /admin/notifications/routing.
Test before you rely on it
Every channel card has a Save & send test button. It saves your settings, sends a sample alert marked as a test to that channel — even before you switch it on — and shows the result inline: ✓ Delivered to https://soar.example.com/… in 182 ms, or the receiver’s own error, e.g. ✗ webhook returned HTTP 401: invalid token.
Below the cards, Recent deliveries lists every alert sent on your behalf with its status (delivered, pending retry, failed), the number of attempts and the last error, so you never need an admin to find out why an alert didn’t arrive.
Email notifications
Alert emails include the tripwire name, detection time, source IP, the credential that was tried, and a link to the tripwire in the console. Self-hosted email needs an SMTP relay (SMTP_HOST; see Configuration).
Slack
Self-hosted: the one-click Connect Slack button is hosted-only — the OAuth flow runs against our Slack app, whose redirect URL can’t cover every customer domain. Self-host installs use an incoming webhook instead: create one at Slack apps (Incoming Webhooks → Add New Webhook to Workspace) and paste the URL under Settings → Notifications. Same alerts, same delivery.
Custom webhooks
A custom webhook POSTs every detection, as signed JSON, to an HTTPS endpoint you own. It works the same on the hosted service and on self-hosted installs. Header values and signing secrets are encrypted at rest — with a dedicated AWS KMS key on the hosted service, and with NOTIFY_SECRET_KEY on self-host (without it, self-host deliveries are unsigned and header-less). On self-host an admin can turn the feature off under System Setup → Platform modules — doing so stops deliveries immediately, including queued retries.
Set up
- Settings → Notifications → Custom HTTPS endpoint: enter the URL (must be
https://). - Optionally add request headers (e.g.
Authorization: Bearer …). Values are encrypted at rest and never shown again. - Click Save & send test. A signing secret (
whsec_…) is created on first save — Reveal it and configure your receiver to verify signatures. - Switch the endpoint on.
The request
| Header | Value |
|---|---|
| Content-Type | application/json |
| User-Agent | Tripwire-Webhooks/1.0 |
| X-Tripwire-Event | tripwire.triggered |
| X-Tripwire-Delivery | UUID of this delivery — identical on every retry. Use it as your idempotency key. |
| X-Tripwire-Timestamp | Unix seconds when this attempt was signed. |
| X-Tripwire-Signature | t=<unix>,v1=<hex HMAC-SHA256> — see Verifying signatures. |
| your headers | Any you configured. They cannot override the X-Tripwire-* headers. |
Payload (schema v1)
Every key is always present (empty string or {} when unknown) so you can rely on the shape; only severity is omitted when no routing rule assigned one. New fields may be added without notice; schema_version changes only on a breaking change.
{
"event": "tripwire.triggered",
"schema_version": "1",
"delivery_id": "6f1c2a8e-4b3d-4e0a-9b7e-2d5f8c1a9e33",
"test": false,
"tripwire_id": "3b8e1f0c-7a2d-4c5e-8f9a-1b2c3d4e5f60",
"tripwire_name": "prod-payroll-db",
"tripwire_type": "postgresql",
"tags": { "env": "prod", "owner": "data-platform" },
"protocol": "postgresql",
"source_ip": "203.0.113.7",
"username": "tw_k3j9x2",
"timestamp": "2026-10-07T09:14:03Z",
"severity": "critical",
"url": "https://tripwire.example.com/tripwire.html?id=3b8e1f0c-…",
"trip_id": "trp_5f0c2b9a1e7d4c3b8a6f0e21",
"namespace": "prod/db",
"org_id": "org_cfddaba1-…",
"user_agent": "",
"detection": {
"protocol": "postgresql",
"source_ip": "203.0.113.7",
"username": "tw_k3j9x2",
"database": "payroll",
"qname": "",
"node_id": "sink-eu-west-2a",
"edns_subnet": "",
"timestamp": "2026-10-07T09:14:03Z"
}
}
| Field | Meaning |
|---|---|
| event | Always tripwire.triggered. |
| delivery_id | Same as X-Tripwire-Delivery; stable across retries. |
| test | true only for Send test deliveries (tripwire id is all zeros, source IP is in TEST-NET-3). Real alerts are always false. |
| tripwire_id / tripwire_name | The tripwire that was touched. |
| tripwire_type | The technology, e.g. postgresql, aws, docx, web_token. |
| tags | The tripwire’s tags — route on env, owner, instance-id… |
| protocol / source_ip / username / timestamp | The original flat fields (kept for compatibility); duplicated in detection. |
| severity | info | warning | critical when a tag-routing rule matched. |
| url | Deep link to the tripwire in the console. |
| trip_id | The detection’s stable id — the same trip_id GET /trips returns, so you can correlate a webhook with the API. |
| namespace / org_id | Where the tripwire lives (empty for the org root / a personal tripwire). |
| user_agent | The HTTP client’s User-Agent for web/document/QR beacons; empty for protocol honeypots and DNS. |
| detection | Full evidence: protocol, source IP, username tried, database, DNS query name, sink node, EDNS client subnet, timestamp (RFC 3339, UTC). |
Verifying signatures
Compute HMAC-SHA256(secret, "<t>." + raw_body), hex-encode it, and compare it in constant time with each v1 value. Reject the request if t is more than 5 minutes from your clock — that blocks replays. Always verify against the raw request bytes, before any JSON parsing. More than one v1 may be present in future (secret-rotation overlap), so accept a match on any of them.
Node.js
const crypto = require('crypto');
// rawBody must be the exact bytes received (e.g. express.raw({ type: 'application/json' })).
function verifyTripwire(secret, header, rawBody, toleranceSec = 300) {
let t = null;
const sigs = [];
for (const part of (header || '').split(',')) {
const [k, v] = part.trim().split('=');
if (k === 't') t = Number(v);
if (k === 'v1') sigs.push(v);
}
if (!t || sigs.length === 0) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay window
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
return sigs.some(s => {
const got = Buffer.from(s, 'hex');
return got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
}
Python
import hashlib, hmac, time
def verify_tripwire(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
t, sigs = None, []
for part in (header or "").split(","):
k, _, v = part.strip().partition("=")
if k == "t":
t = int(v)
elif k == "v1":
sigs.append(v)
if t is None or not sigs or abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in sigs)
Go
func verifyTripwire(secret, header string, body []byte, tolerance time.Duration) bool {
var ts int64
var sigs []string
for _, part := range strings.Split(header, ",") {
k, v, _ := strings.Cut(strings.TrimSpace(part), "=")
switch k {
case "t":
ts, _ = strconv.ParseInt(v, 10, 64)
case "v1":
sigs = append(sigs, v)
}
}
if ts == 0 || len(sigs) == 0 || time.Since(time.Unix(ts, 0)).Abs() > tolerance {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.", ts)
mac.Write(body)
want := mac.Sum(nil)
for _, s := range sigs {
if got, err := hex.DecodeString(s); err == nil && hmac.Equal(got, want) {
return true
}
}
return false
}
# Reproduce a signature by hand (bash + openssl):
TS=$(date +%s); BODY='{"hello":"world"}'
printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$WHSEC" -hex
# → compare with the v1= value of X-Tripwire-Signature: t=$TS,v1=…
Rotate the secret from the same card if it leaks; new deliveries are signed with the new secret immediately.
Delivery, retries and idempotency
- Success is any
2xxresponse within 10 seconds. Respond fast and do the work asynchronously. - Redirects are not followed — a
3xxcounts as a failure and the delivery log names theLocation. Configure the final URL. - Retries: a failed delivery is retried after 30 s, 2 min, 8 min, 32 min and 1 h (6 attempts in total), then marked failed. Each retry is freshly signed and carries the same
X-Tripwire-Delivery. - At-least-once: if your endpoint processed a request but the response was lost, you will see it again — de-duplicate on
delivery_id. - The receiver’s status code and the first 200 bytes of its response body are shown in Recent deliveries and in the Send test result.
Network policy
Webhook URLs are user-supplied, so the server refuses to connect to loopback, link-local (including the 169.254.169.254 cloud metadata service) and multicast addresses — checked on the resolved IP at connect time, so DNS tricks cannot bypass it. Private ranges (RFC 1918, 100.64.0.0/10, fc00::/7) are refused by default; an operator alerting an internal SOAR sets WEBHOOK_ALLOW_PRIVATE_NETWORKS=true on the control server. Outbound proxies (HTTPS_PROXY) are honoured. Admin-configured SIEM forwarding is not subject to this policy.
Hosted service: webhook targets must be reachable on the public internet — private, loopback and cloud-metadata addresses are always refused. Deliveries come from AWS eu-west-2 with the Tripwire-Webhooks/1.0 user agent; verify the signature rather than allow-listing IPs.