Lattice tells your systems when a lease changed or someone mutated IPAM. It does this with a signed HTTPS POST after the outbox row is processed — the same queue that writes DNS. dhcpd never waits. A dead webhook is a retry, not a rolled-back lease.
Hub: integrations. Secret refs: same env: / file: rules as TSIG. Observability (logs vs webhooks vs Overview alerts): observability.
HMAC-SHA256 is RFC 2104. Lattice signs the raw POST body; how your receiver verifies that header is in that product’s docs (SIEM, CM, ticketing), not here. Slack-style incoming webhooks that 302 are a common miss — see §3.
Just show me the steps — create + verify at the bottom.
1. What problem this solves
DHCP and IPAM are the system of record. CMDB, SIEM, ticketing, and home-grown scripts still need to know:
- This MAC just got
10.20.20.55(lease.granted) - That name is gone (
lease.released/lease.expired) - An operator reserved
.10(ipam.audit)
Polling /api/v1/leases every ten seconds is noisy, late, and needs a token that can read everything. A webhook is push: Lattice already decided the event happened; your receiver only has to accept JSON.
Overview alert destinations (SMTP, Alertmanager, PagerDuty, a generic webhook URL under Settings) are a different path. Those fire when a dashboard sentence opens or clears (disk, VIP, quiet dhcpd). This page is the outbox webhook: one POST per lease/IPAM event, with a topic and an HMAC.
2. Why a shared secret (HMAC)
Anyone who can POST to your URL could invent a fake grant. Lattice signs the raw body with HMAC-SHA256 using a secret only you and the worker share.
X-Lattice-Topic: lease.granted
X-Lattice-Delivery: <outbox id>
X-Lattice-Signature: sha256=<hex hmac-sha256 of raw body>
Content-Type: application/json
Verify over the exact bytes you received, not a re-serialized JSON object. If the signature does not match, reject (401/403). Do not apply the event.
The API stores secret_ref (env:LATTICE_WEBHOOK_SECRET or file:/etc/lattice/webhook.hmac), never the secret. The Operator role cannot GET webhook rows (the ref would leak enough to hunt the file). Mode 0600 on the worker host. HA: every worker.
3. Why HTTPS, no redirects, no metadata
A webhook URL is an outbound call from the worker. If an admin could set http://169.254.169.254/, Lattice would become an SSRF client into the cloud metadata service. So:
httpsis required. Plainhttpis allowed only for localhost.- Metadata and link-local hosts (including AWS IPv6 IMDS) are refused at create and at dial.
- Delivery does not follow HTTP redirects. A 302 to a metadata IP must not succeed.
That is why “just use a Slack incoming webhook that 302s” can fail. Put a receiver you control in front, or use a URL that 200s on POST.
4. Topics
topics is a comma list, or * (default: everything).
| Topic | When | Typical consumer |
|---|---|---|
lease.granted | Ack committed | CM, NAC, “new device” ticket |
lease.renewed | Renew committed | Usually noise; skip unless you graph churn |
lease.released | Release / force-release | DNS-adjacent CM, “device left” |
lease.expired | Worker expiry | Same as released, but the client went quiet |
lease.declined | Client declined the Offer | Conflict / duplicate-IP hunt |
ipam.audit | Prefix/subnet/pool/reservation/token/… mutation | SIEM, compliance copy of Audit |
Lease bodies include lease_id, ip, subnet_id, hostname, fqdn, state, and related fields.
ipam.audit body: { "id", "at", "actor", "action", "object_type", "object_id", "before", "after" }. IPAM writes are not rolled back if this POST fails. Keep a long copy here if Audit retention (default 365 days) is shorter than your compliance window.
DNS adapter success or failure is not a webhook topic. That queue is Work → DNS.
5. Create one
Console More → Webhooks (admin). API:
curl -s -X POST http://127.0.0.1:8080/api/v1/webhooks \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{
"name": "cm",
"url": "https://hooks.example.com/lattice",
"secret_ref": "env:LATTICE_WEBHOOK_SECRET",
"topics": "lease.granted,lease.released,lease.expired"
}'
Receiver sketch (verify, then ack with 2xx):
signature = HMAC-SHA256(secret, raw_body) # hex
header = "sha256=" + signature
# compare to X-Lattice-Signature (constant-time)
# 2xx = done. 4xx/5xx = Lattice retries the outbox row.
Timeouts are short (~10s). Do the real work asynchronously after you return 2xx, or you will trip retries and see duplicates. Delivery is at-least-once. Key on X-Lattice-Delivery (outbox id) if you must be idempotent.
6. Failure mode
Same as DNS adapters:
- The lease stays.
- The outbox row retries with backoff, then
failed. - Replay is operator+ on Work → DNS (the outbox is shared; webhook-only rows still sit there).
A webhook that hangs or 500s does not take DHCP down. Fix the receiver, then Replay.
7. Rotation
- Put the new secret on every worker (
envorfile). - Update the webhook
secret_refif the name/path changed. - Update the receiver to accept both HMACs for a few minutes, then drop the old one.
Do not put the HMAC secret in git, in a ticket, or in the IPAM UI.
Just the steps
Put the HMAC secret on every worker (0600). HTTPS URL (plain http only for localhost). Console More → Webhooks, or:
curl -s -X POST http://127.0.0.1:8080/api/v1/webhooks \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{
"name": "cm",
"url": "https://hooks.example.com/lattice",
"secret_ref": "env:LATTICE_WEBHOOK_SECRET",
"topics": "lease.granted,lease.released,lease.expired"
}'
Receiver: HMAC-SHA256 of the raw body, compare to X-Lattice-Signature (sha256= + hex). Return 2xx quickly. Idempotent on X-Lattice-Delivery. This is not Settings → Overview alert webhook.