Webhooks

Signed outbox HTTP after DNS. Never rolls back the lease.

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:

  • https is required. Plain http is 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).

TopicWhenTypical consumer
lease.grantedAck committedCM, NAC, “new device” ticket
lease.renewedRenew committedUsually noise; skip unless you graph churn
lease.releasedRelease / force-releaseDNS-adjacent CM, “device left”
lease.expiredWorker expirySame as released, but the client went quiet
lease.declinedClient declined the OfferConflict / duplicate-IP hunt
ipam.auditPrefix/subnet/pool/reservation/token/… mutationSIEM, 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

  1. Put the new secret on every worker (env or file).
  2. Update the webhook secret_ref if the name/path changed.
  3. 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.