Webhook publik di self-hosted stack menambah kompleksitas yang sering tidak terlihat saat masih berada di lingkungan internal. Endpoint harus bisa diakses internet, tetapi tetap membatasi apa yang dipercaya dari request masuk. Di sisi lain, sistem juga harus tahan terhadap duplicate delivery, retry agresif dari pihak ketiga, timeout jaringan, dan gangguan operasional seperti DNS salah, reverse proxy keliru, firewall memblokir, NAT tidak stabil, atau queue internal tersendat.
Artikel ini membahas desain webhook publik yang praktis untuk stack self-hosted: bagaimana menentukan trust boundary, memverifikasi keaslian request dengan HMAC signature dan timestamp, menambahkan replay protection dan idempotency, merancang retry policy, serta memasang observability yang cukup untuk debugging saat internet dan infrastruktur sedang tidak kooperatif.
Masalah utama: endpoint publik bukan berarti request bisa dipercaya
Begitu sebuah endpoint webhook dipublikasikan, ada dua fakta yang harus diterima:
- Semua orang dapat mencoba mengaksesnya, bukan hanya vendor atau partner yang Anda integrasikan.
- Komponen jaringan di depan aplikasi dapat mengubah konteks request, misalnya terminasi TLS di reverse proxy, rewriting header, rate limiting, atau timeout sebelum request sampai ke aplikasi.
Karena itu, desain webhook tidak boleh bergantung pada asumsi lemah seperti:
- "Kalau datang dari internet publik perusahaan X pasti aman"
- "Kalau IP cocok dengan dokumentasi vendor berarti valid"
- "Kalau HTTPS aktif berarti payload asli"
Trust boundary untuk webhook publik biasanya ada di aplikasi penerima setelah request melewati lapisan jaringan minimum yang Anda kontrol: DNS, firewall, load balancer, reverse proxy, lalu aplikasi. TLS melindungi transport, tetapi bukan bukti bahwa payload dikirim oleh pengirim yang sah. Bukti itu harus datang dari mekanisme aplikasi seperti HMAC signature, timestamp, dan identifier event.
Trust boundary pada stack self-hosted
Lapisan yang umum ditemui
Dalam deployment self-hosted, request webhook sering melewati jalur seperti ini:
Internet -> DNS -> Firewall/Router -> NAT -> Reverse Proxy / Load Balancer -> App -> Queue -> Worker -> DatabaseSetiap lapisan punya peran dan failure mode sendiri:
- DNS: domain salah, record belum propagasi, TTL terlalu tinggi, atau vendor masih mengirim ke IP lama.
- Firewall: port 443 terbuka untuk browser biasa, tetapi diblok untuk sumber tertentu atau ada inspeksi yang memutus koneksi.
- NAT: mapping tidak konsisten, hairpin NAT bermasalah, atau forwarding salah ke host internal.
- Reverse proxy: body dibatasi terlalu kecil, timeout terlalu pendek, header penting tidak diteruskan, atau raw body berubah.
- Aplikasi: verifikasi signature salah karena payload diubah parser, jam server meleset, atau database lambat.
- Queue/worker: enqueue berhasil tapi worker berhenti, backlog menumpuk, retry internal bertabrakan dengan retry dari pihak ketiga.
Apa yang boleh dipercaya?
Untuk request webhook dari internet, anggap hal berikut tidak cukup sebagai bukti keaslian:
- IP source saja
- User-Agent
- Header
X-Forwarded-Fortanpa validasi proxy tepercaya - Field pengirim di body JSON
Yang lebih layak dipercaya:
- Signature kriptografis yang dihitung atas raw payload dengan secret bersama
- Timestamp untuk membatasi jendela validitas request
- Event ID / delivery ID untuk replay protection dan deduplikasi
- Mutual TLS bila kedua pihak mengontrol sertifikat dan operasionalnya, meskipun ini lebih kompleks
IP allowlist masih berguna sebagai defense in depth, tetapi jangan dijadikan mekanisme autentikasi utama. Banyak integrator mengubah IP tanpa pemberitahuan sempurna, memakai CDN, atau mengirim dari beberapa region.
Desain autentikasi webhook: HMAC, timestamp, dan replay protection
Mengapa HMAC cocok untuk webhook
HMAC bekerja baik untuk webhook karena sederhana dan tidak memerlukan sesi. Pengirim dan penerima berbagi secret. Pengirim menghitung signature dari data tertentu, lalu penerima menghitung ulang signature dengan secret yang sama. Jika hasilnya sama, integritas payload terjaga dan pengirim kemungkinan sah.
Praktik penting:
- Hitung signature dari raw request body, bukan objek JSON hasil parsing ulang.
- Gabungkan timestamp ke data yang ditandatangani agar signature tidak valid tanpa batas waktu.
- Bandingkan signature dengan constant-time comparison untuk mengurangi risiko timing attack.
- Dukung secret rotation dengan menyimpan secret aktif dan secret sebelumnya selama masa transisi.
Contoh kontrak header webhook
Berikut contoh kontrak yang cukup umum dan realistis:
POST /webhooks/vendor-a HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
X-Webhook-Id: evt_01JABCXYZ789
X-Webhook-Timestamp: 1736157600
X-Webhook-Signature: v1=5f2d3b4c...
X-Webhook-Source: vendor-a
Idempotency-Key: evt_01JABCXYZ789Contoh payload:
{
"event_id": "evt_01JABCXYZ789",
"event_type": "invoice.paid",
"occurred_at": "2026-01-06T10:00:00Z",
"data": {
"invoice_id": "inv_12345",
"customer_id": "cus_999",
"amount": 250000,
"currency": "IDR"
}
}String yang ditandatangani sebaiknya eksplisit, misalnya:
signed_payload = X-Webhook-Timestamp + "." + raw_request_bodyLalu signature dihitung dengan HMAC-SHA256:
signature = hex(HMAC_SHA256(secret, signed_payload))Contoh verifikasi di server
function verifyWebhook(headers, rawBody, secrets, nowEpoch) {
const ts = headers["x-webhook-timestamp"];
const sigHeader = headers["x-webhook-signature"];
const eventId = headers["x-webhook-id"];
if (!ts || !sigHeader || !eventId) {
return { ok: false, reason: "missing_required_headers" };
}
const tsInt = parseInt(ts, 10);
if (Number.isNaN(tsInt)) {
return { ok: false, reason: "invalid_timestamp" };
}
const maxSkewSeconds = 300;
if (Math.abs(nowEpoch - tsInt) > maxSkewSeconds) {
return { ok: false, reason: "timestamp_out_of_window" };
}
const expectedPayload = ts + "." + rawBody;
const provided = extractV1Signature(sigHeader);
if (!provided) {
return { ok: false, reason: "invalid_signature_format" };
}
let matched = false;
for (const secret of secrets) {
const expected = hmacSha256Hex(secret, expectedPayload);
if (constantTimeEqual(expected, provided)) {
matched = true;
break;
}
}
if (!matched) {
return { ok: false, reason: "signature_mismatch" };
}
return { ok: true, eventId, timestamp: tsInt };
}Poin penting di contoh di atas:
- rawBody harus benar-benar body asli sebelum dinormalisasi framework.
- secrets berupa daftar secret aktif untuk mendukung rotasi.
- timestamp window mencegah request lama diputar ulang tanpa batas.
Replay protection
Signature yang valid belum cukup. Jika seorang penyerang atau proxy merekam request valid lalu mengirimkannya lagi dalam jendela waktu yang masih diterima, sistem Anda bisa memproses event berulang. Karena itu, simpan kombinasi berikut untuk periode tertentu:
event_idatauX-Webhook-Idtimestampsignature digestatau hash payload bila perlu
Strategi yang umum:
- Tolak request dengan
event_idyang sudah pernah diproses. - Atau, terima request duplikat tetapi respons sukses tanpa memproses ulang.
- Simpan catatan replay di Redis atau database dengan TTL sesuai kebutuhan audit dan retry pihak ketiga.
Pendekatan kedua biasanya lebih ramah integrasi: vendor yang me-retry karena tidak menerima respons dapat tetap memperoleh 2xx, sementara sistem Anda tetap idempoten.
Idempotency, deduplikasi event, dan model pemrosesan yang aman
Masalah duplicate delivery adalah normal
Dalam dunia webhook, duplicate delivery bukan edge case. Vendor dapat mengirim ulang karena:
- Timeout membaca respons
- Koneksi terputus setelah request terkirim
- Server Anda membalas
5xx - Load balancer atau proxy mengakhiri koneksi lebih awal
- Sistem mereka sendiri melakukan retry spekulatif
Karena itu, handler webhook harus didesain sebagai at-least-once consumer.
Bedakan deduplikasi transport dan deduplikasi bisnis
Ada dua level deduplikasi:
- Transport-level deduplication: mencegah event delivery yang sama diproses berulang, biasanya dengan
event_id. - Business-level idempotency: mencegah efek samping domain terjadi dua kali, misalnya invoice ditandai lunas dua kali atau saldo dikreditkan berulang.
Keduanya penting. Menyimpan event_id saja tidak selalu cukup jika vendor bisa mengirim event berbeda yang secara bisnis merepresentasikan objek yang sama.
Pola implementasi yang disarankan
- Terima request.
- Verifikasi signature, timestamp, dan header wajib.
- Cek apakah
event_idsudah pernah diterima. - Simpan event mentah ke tabel inbox atau append-only log.
- Kembalikan
202 Acceptedatau200 OKdengan cepat. - Proses event secara asynchronous melalui queue/worker.
- Di worker, terapkan idempotensi bisnis dengan transaksi database atau unique constraint.
Contoh skema tabel inbox:
webhook_inbox (
source text not null,
event_id text not null,
received_at timestamp not null,
signature_valid boolean not null,
payload_sha256 text not null,
payload_json jsonb not null,
processing_status text not null,
primary key (source, event_id)
)Keuntungan pola ini:
- Request publik tidak perlu menunggu logika bisnis selesai.
- Ada jejak audit untuk debugging.
- Retry dari pihak ketiga lebih mudah ditangani.
- Gangguan worker tidak langsung membuat vendor menganggap endpoint gagal, selama event berhasil diterima dan disimpan.
Unique constraint lebih kuat daripada cek manual
Anti-pattern yang sering muncul adalah:
if (!exists(event_id)) {
insert(event_id)
process()
}Di bawah concurrency, dua request dapat sama-sama lolos sebelum insert. Lebih aman gunakan unique constraint di database atau operasi atomik di Redis, lalu tangani konflik sebagai event duplikat yang sah.
Retry policy, timeout, dan respons yang benar
Kapan membalas 2xx, 4xx, atau 5xx
Status code memengaruhi perilaku retry pihak ketiga. Aturan praktis:
- 2xx: request diterima. Gunakan bila signature valid dan event berhasil dicatat, meskipun belum selesai diproses.
- 4xx: request buruk atau tidak sah, misalnya signature salah, timestamp invalid, header wajib hilang. Umumnya pengirim tidak perlu retry tanpa perubahan.
- 5xx: kegagalan sementara di sisi Anda, misalnya database down atau queue tidak tersedia. Pengirim biasanya akan retry.
Hati-hati: jika Anda membalas 2xx sebelum event tercatat secara durable, Anda bisa kehilangan event tanpa kesempatan retry. Sebaliknya, jika Anda menunggu seluruh pemrosesan bisnis selesai baru membalas, Anda akan memperbesar timeout dan duplicate delivery.
Timeout yang realistis
Untuk webhook masuk, targetkan respons cepat. Praktik umum:
- Batasi kerja sinkron di request path seminimal mungkin.
- Jangan memanggil API lain secara sinkron kecuali benar-benar perlu.
- Jangan melakukan query berat atau transaksi panjang sebelum mengembalikan respons.
Timeout harus konsisten lintas lapisan:
- Reverse proxy read timeout
- App server request timeout
- Database timeout
- Queue publish timeout
Jika proxy timeout 5 detik tetapi aplikasi butuh 15 detik untuk menulis ke storage yang lambat, vendor akan melihat kegagalan dan mengirim ulang, padahal aplikasi mungkin tetap melanjutkan pemrosesan. Ini sumber duplikasi klasik.
Retry internal vs retry eksternal
Jangan mencampur retry dari vendor dengan retry internal Anda tanpa kontrol. Contoh jebakan:
- Vendor retry 10 kali.
- Aplikasi Anda juga retry enqueue 5 kali per request.
- Worker retry job 20 kali.
Efeknya bisa berupa ledakan beban, pemrosesan ganda, dan backlog yang sulit dipulihkan.
Pisahkan tanggung jawab:
- Request ingress: validasi, simpan durable, respons cepat.
- Worker: retry logika bisnis yang memang transient, dengan backoff dan batas percobaan yang jelas.
- Dead-letter handling: jika event gagal terus, pindahkan ke status gagal untuk investigasi manual atau replay terkontrol.
Failure mode nyata pada infrastruktur self-hosted
DNS gagal atau salah arah
Masalah umum:
- Record A/AAAA salah setelah migrasi IP
- TTL panjang membuat vendor tetap mengirim ke alamat lama
- Sertifikat TLS tidak cocok karena domain berubah
Gejalanya:
- Tidak ada request masuk sama sekali
- Vendor melaporkan connection refused atau name resolution failure
- Akses dari browser lokal berhasil karena cache DNS berbeda dengan sisi vendor
Debugging cepat:
- Periksa resolusi DNS publik dari beberapa lokasi.
- Bandingkan IPv4 dan IPv6 jika keduanya aktif.
- Pastikan sertifikat dan SNI cocok dengan hostname webhook.
Reverse proxy mengubah request
Proxy sering menjadi sumber bug verifikasi signature:
- Body di-buffer atau diubah
- Content-Encoding di-decompress sehingga raw body berbeda
- Header custom dihapus atau diganti huruf besar-kecilnya secara tak terduga
- Batas body terlalu kecil menghasilkan
413
Jika HMAC mendadak selalu gagal setelah melewati proxy baru, hal pertama yang diperiksa adalah apakah aplikasi masih menerima raw body yang identik dengan yang dikirim pengirim.
Firewall dan NAT memunculkan kegagalan intermiten
Pada self-hosted, masalah tidak selalu berupa down total. Bisa muncul sebagai:
- Sebagian region berhasil, sebagian timeout
- Koneksi TCP terbuka tetapi TLS handshake gagal
- Port forwarding hanya benar untuk satu host internal
- NAT idle timeout memutus koneksi panjang
Webhook biasanya request pendek, tetapi kegagalan intermiten seperti ini memicu retry dan duplicate delivery. Karena itu, observability harus bisa menjawab apakah request:
- tidak pernah mencapai edge,
- mencapai proxy tapi tidak ke aplikasi, atau
- sudah sampai aplikasi tetapi gagal dipersist.
Queue internal bermasalah setelah ingress sukses
Ini sangat umum dan berbahaya. Aplikasi membalas 202, tetapi:
- queue broker penuh,
- publish timeout,
- worker mati,
- consumer lag sangat tinggi.
Jika event belum disimpan durable sebelum enqueue, Anda bisa kehilangan data. Solusi yang lebih aman:
- Simpan dulu event ke database/inbox log.
- Gunakan outbox/inbox pattern atau polling worker dari tabel durable.
- Anggap queue sebagai akselerator pemrosesan, bukan satu-satunya tempat keberadaan event.
Observability yang wajib dipasang
Log terstruktur per delivery
Setiap request webhook sebaiknya punya correlation data minimum:
request_idsourceevent_idsignature_validtimestamp_skew_secondshttp_statusingress_latency_msenqueue_result
Jangan log secret, payload sensitif penuh, atau signature mentah jika tidak perlu. Untuk payload, lebih aman log hash, ukuran body, dan field identitas utama.
Metrik yang berguna
- Jumlah request per source dan per status code
- Rate signature mismatch
- Rate duplicate event
- Latency verifikasi dan persist
- Queue lag dan usia event tertua
- Jumlah event di dead-letter atau gagal diproses
Tanpa metrik duplicate event, Anda sulit membedakan antara serangan replay, bug vendor, dan timeout jaringan yang menyebabkan retry.
Tracing dan audit trail
Jika memungkinkan, buat alur yang dapat ditelusuri dari edge sampai worker:
request_id -> event_id -> inbox_row_id -> queue_job_id -> business_transaction_idIni sangat membantu saat pihak ketiga bertanya, “Kami menerima 200, apakah event sudah diproses?” Anda dapat menjawab dengan data, bukan asumsi.
Contoh alur implementasi yang aman
- Endpoint publik menerima request HTTPS.
- Reverse proxy meneruskan raw body dan header webhook tanpa modifikasi yang mengubah signature.
- Aplikasi membaca raw body.
- Aplikasi memverifikasi timestamp, signature, dan source secret.
- Aplikasi melakukan operasi atomik insert-if-not-exists ke tabel inbox berdasarkan
(source, event_id). - Jika duplikat, aplikasi tetap mengembalikan
200atau202. - Jika berhasil disimpan, aplikasi mengembalikan
202dengan cepat. - Worker membaca inbox, memproses logika bisnis secara idempoten, lalu menandai status sukses/gagal.
- Jika gagal sementara, worker retry dengan backoff.
- Jika gagal permanen, event dipindahkan ke antrian investigasi atau dead-letter.
Contoh respons minimal:
HTTP/1.1 202 Accepted
Content-Type: application/json
{"status":"accepted","event_id":"evt_01JABCXYZ789"}Checklist implementasi webhook publik di self-hosted stack
- Gunakan HTTPS end-to-end atau minimal sampai reverse proxy tepercaya.
- Definisikan trust boundary dengan jelas: mana edge tepercaya, mana yang tidak.
- Verifikasi request dengan HMAC signature berbasis raw body.
- Tambahkan timestamp dan batasi skew waktu.
- Gunakan constant-time comparison untuk signature.
- Dukung secret rotation.
- Simpan dan cek event_id untuk replay protection dan deduplikasi.
- Terapkan idempotensi bisnis, bukan hanya deduplikasi delivery.
- Persist event secara durable sebelum membalas sukses.
- Respon cepat; pindahkan kerja berat ke queue/worker.
- Samakan timeout di proxy, app, database, dan queue publish.
- Pasang log terstruktur, metrik duplicate/retry, dan tracing.
- Uji failure mode: DNS salah, proxy timeout, queue down, DB lambat, worker mati.
- Dokumentasikan kontrak header, payload, status code, dan retry expectation.
Anti-pattern umum saat integrasi pihak ketiga
1. Mengandalkan IP allowlist sebagai autentikasi utama
Masalah: IP bisa berubah, daftar tidak lengkap, dan sulit dikelola di multi-region. Tetap berguna, tetapi hanya sebagai lapisan tambahan.
2. Menghitung HMAC dari JSON yang sudah diparse
Masalah: perbedaan whitespace, urutan field, encoding, atau normalisasi membuat signature mismatch. HMAC harus menggunakan raw body.
3. Memproses logika bisnis penuh sebelum membalas
Masalah: mudah timeout, memicu retry vendor, dan memperbesar duplikasi.
4. Mengembalikan 200 sebelum event dipersist
Masalah: jika aplikasi crash setelah respons dikirim, event hilang dan vendor tidak akan retry.
5. Tidak membedakan kegagalan permanen dan sementara
Masalah: semua error dibalas 500 atau semua dibalas 400. Akibatnya perilaku retry pengirim menjadi salah.
6. Tidak menyimpan payload mentah atau hash payload
Masalah: sulit audit dan sulit membuktikan apakah perbedaan berasal dari vendor, proxy, atau aplikasi.
7. Tidak sinkronisasi waktu server
Masalah: verifikasi timestamp gagal walau signature benar. Untuk webhook berbasis timestamp, clock drift kecil pun bisa merusak integrasi.
Penutup
Webhook publik di self-hosted stack adalah kombinasi masalah aplikasi, jaringan, dan operasi. Endpoint yang bisa diakses dari internet belum berarti aman atau andal. Desain yang baik selalu berangkat dari trust boundary yang ketat: jangan percaya IP, header, atau body tanpa verifikasi kriptografis; anggap duplicate delivery sebagai kondisi normal; persist event sebelum mengakui sukses; dan siapkan observability untuk membedakan apakah masalah ada di DNS, proxy, firewall, NAT, aplikasi, atau queue.
Jika Anda hanya mengambil satu prinsip dari artikel ini, ambil yang ini: validasi di edge aplikasi, akui penerimaan hanya setelah durable, lalu proses secara idempoten di belakang. Pola itu tidak menghilangkan semua failure mode, tetapi secara signifikan menurunkan risiko event palsu, replay, kehilangan data, dan duplikasi efek bisnis.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!