Case · Backend · Rust

hook-relay: webhook delivery

A webhook delivery service in Rust: it accepts an event, signs it with HMAC-SHA256, sends it to the receiver and does not lose it. On failure it retries with exponential backoff and jitter, after the attempt limit it parks the delivery in a dead-letter state where it can be requeued by hand. The receiver address is checked for SSRF after DNS: every resolved address must be public. Idempotent by Idempotency-Key, state in SQLite.

  • Rust
  • axum
  • tokio
  • SQLite
  • HMAC-SHA256
  • SSRF
  • proptest
  • musl
  • distroless
  • Docker
  • GitHub Actions

01 · How it looks

Screenshots

Screenshot of the project "hook-relay: webhook delivery" at 1280 pixels wide
Desktop, 1280 px
Screenshot of the project "hook-relay: webhook delivery" on a phone screen, 390 pixels wide
Phone, 390 px

02 · About

What it is and why

The task: deliver webhooks so that an event is not lost, the receiver can verify authenticity, and the service itself cannot be used to reach an internal network.

The solution: Rust (axum + tokio + SQLite). The body is signed with HMAC-SHA256 over the string "t.body" using the receiver's secret; on failure delivery retries with exponential backoff and jitter and after the limit goes to dead-letter. The receiver address is resolved through DNS and every resulting address must be public.

Limitations

At-least-once delivery: the receiver must be idempotent (X-Relay-Delivery-Id).

Receiver secrets are stored in the database in plain text and never returned in API lists.

The receiver response body is not read or stored.

The project runs on a small VPS with demo data: no redundancy or backups.

What is inside

  • SSRF: loopback, private, link-local, CGNAT, documentation, ULA, IPv4-mapped, NAT64 and 6to4 with an embedded private address are blocked; checked at registration and on every delivery
  • Retries: network errors, 5xx, 408 and 429; other 4xx are a final refusal; manual requeue from dead-letter
  • Idempotency by Idempotency-Key: a repeat returns the earlier delivery and does not send the event again
  • After a crash restart in-flight deliveries return to the queue; graceful shutdown waits up to 15 s
  • 20 tests; proptest: the signature fails after changing any byte, any private IPv4 and its IPv4-mapped form are blocked
  • Result: a 14.4 MB image, 3.5 MiB idle and 5.5 MiB after 200 events with real delivery to a receiver

03 · How to run

Run it locally

git clone <repository on request> hook-relay
cd hook-relay
DATA_DIR=./data cargo run --release
# http://localhost:8113

04 · Code

The repository is private

The source is in a private repository. Message me on Telegram and I will open access.

Request the code