Entegrasyon dokümanı
Hook'lar
YEKE bir operasyonu uygulamadan önce sizin sisteminize sorabilir, uyguladıktan sonra haber verebilir. Bu sayfa protokolün tamamını anlatır: hangi başlıklar, hangi gövde, hangi yanıt, imza nasıl doğrulanır, endpoint cevap vermezse ne olur.
İki hook tipi ve zincirdeki yerleri
Biri kapı, diğeri haberci. Karıştırılan tek şey bu ve sonucu pahalı.
Doğrulama (validate)
Operasyon uygulanmadan önce sorulur ve durdurabilir. Yanıtınız planın akıbetini belirler.
- Anı: plan kurulurken, onaya basıldığı an, ya da ikisi.
- Yanıtınız:
allow/deny/elevate. - Ulaşılamazsa:
onFailurebelirler — varsayılandeny.
Bildirim (notify)
Operasyon uygulandıktan sonra haber verilir ve hiçbir şeyi durdurmaz. Kullanıcı sonucu çoktan almıştır.
- Anı: apply bittikten sonra, kalıcı bir kuyruktan.
- Yanıtınız: herhangi bir
2xx. Gövdeye bakılmaz. - Ulaşılamazsa: tekrar denenir, sonra ölü kuyruğa düşer.
Bir yazma isteğinin izlediği yol — hook'lar kalın:
istek → guardrail → dry-run → HOOK (stage: plan)
│
├─ deny → plan DENIED, onay kartı hiç doğmaz
├─ elevate → onay kartı "kaynağın adını yaz" ister
└─ allow → onay kartı açılır
│
kullanıcı onaya basar
│
HOOK (stage: approve)
│
┌───────────────┴─ deny → 409, plan BEKLEMEDE kalır
│
apply
│
HOOK (stage: notify) ← kuyruktan, asenkronGuardrail ile hook aynı şey değildir. Cevabı planın şeklinden tek başına bilinebilen kural guardrail'e aittir (“bu namespace'te silme yasak”) ve ağ turu bile atmaz. Hook, cevabını yalnızca dışarının bilebileceği sorular içindir: açık change ticket var mı, bakım penceresi mi, nöbetçi kim.
İstek: başlıklar ve gövde
Her zaman POST, her zaman JSON. Yönlendirme izlenmez —
3xx dönerseniz çağrı hata sayılır.
| Başlık | Her zaman | Değer |
|---|---|---|
content-type | Evet | application/json |
user-agent | Evet | yeke-core — sabit ve sürümsüz. Erişim günlüğünüzde isteğin YEKE'den geldiğini görebilesiniz diye. |
x-yeke-hook | Evet | Hook'un kimliği — sizin verdiğiniz ad. |
x-yeke-delivery | Evet | Teslim UUID'si. Idempotency anahtarınız budur; tekrar denemelerde aynı kalır. |
authorization | Hayır | Yalnız tanımladıysanız. Değer ham geçer — Bearer …, Basic …, ne yazdıysanız o. |
x-yeke-signature-256 | Hayır | Yalnız imza sırrı tanımladıysanız. Gövdenin HMAC-SHA256'sı, hex. |
Doğrulama gövdesi
stage alanı plan ya da approve taşır; gövdenin geri kalanı
ikisinde de aynıdır.
{
"schemaVersion": 1,
"hookId": "change-board",
"deliveryId": "6f1c9d84-2b77-4a51-9e0e-7a2f1c0d8b33",
"stage": "plan",
"plan": {
"id": "op-01KZGTWSG5WSSR8PDZ35",
"clusterId": "c-3",
"source": "ui",
"createdAt": "2026-08-08T14:02:09.144Z",
"actor": { "userId": "u-2", "username": "deniz" },
"identity": { "user": "deniz", "groups": ["platform"] },
"origins": [
{ "method": "PATCH", "path": "/apis/apps/v1/namespaces/uretim/deployments/api" }
],
"steps": [
{
"target": {
"schemaId": "apps/v1/Deployment", "group": "apps", "version": "v1",
"kind": "Deployment", "resource": "deployments",
"namespaced": true, "namespace": "uretim", "name": "api",
"subresource": null, "uid": "9c1…", "resourceVersion": "122835"
},
"classification": { "severity": "mutating", "flags": ["owner-managed"] },
"changedPaths": ["/spec/replicas"],
"dryRun": { "status": "ok" }
}
],
"policy": {
"verdict": "allow",
"approvalLevel": "standard",
"matches": [
{ "policyId": "uretim.replica-tavani", "effect": "allow", "message": null }
]
}
}
}| Alan | Değerler | Ne demek |
|---|---|---|
stage | plan · approve · test | test, yönetim ekranındaki Sına düğmesidir ve gövdesinde plan yoktur; kararı hiçbir şeye dönüşmez. |
source | ui · nl · api | İsteği doğuran yüzey. nl = sohbet. |
actor | nesne · null | YEKE kullanıcısı. Otomasyondan gelen bir istekte null olabilir. |
identity | nesne | Kubernetes kimliği — apiserver'a gerçekten bu kimlikle gidilir. |
origins[].method | POST · PATCH · PUT · DELETE | Ham HTTP niyeti. Fiil buradan okunur. |
classification.severity | mutating · destructive | Yıkıcılık sınıfı; nesnenin şeklinden türetilir. |
classification.flags | dizi | cluster-scoped, collection, finalizers, owner-managed, orphan-delete, dry-run-unsupported, irreversible, stream |
changedPaths | JSON Pointer dizisi | Hangi yolların değişeceği. Değerler yok, yalnız yollar. |
dryRun.status | ok · failed · unsupported · skipped · not-applicable | apiserver'ın dry-run'ı ne dedi. |
policy.verdict | allow · deny · elevate · error | YEKE'nin kendi guardrail kararı — sizinkinden önce. |
policy.approvalLevel | standard · elevated | Onay kartının bugünkü çıtası. |
Gövdede nesne DEĞERİ yoktur ve olmayacaktır. Manifest yok, önceki hâl yok,
dry-run sonucu yok, istek gövdesi yok. Hook “ne olacağını” bilmek zorunda, “hangi değerin ne
olacağını” değil. Bir Secret güncellenirken bile changedPaths
["/data/password"] der, parolayı değil. Bunu bir ayarla açamazsınız: gövdeyi üreten tek fonksiyon zaten başka bir şey
üretmiyor.
Bildirim gövdesi
stage her zaman notify. Adım şekli doğrulamayla aynı; farkı
event ve result.
{
"schemaVersion": 1,
"hookId": "slack-kanali",
"deliveryId": "b3d0…",
"stage": "notify",
"event": { "type": "plan.applied", "ts": "2026-08-08T14:02:14.881Z" },
"operation": {
"id": "op-01KZGTWSG5WSSR8PDZ35",
"clusterId": "c-3",
"source": "ui",
"revertOf": null,
"actor": { "userId": "u-2", "username": "deniz" },
"identity": { "user": "deniz", "groups": ["platform"] },
"origins": [ { "method": "PATCH", "path": "/apis/apps/v1/…" } ],
"steps": [ { "target": { … }, "classification": { … }, "changedPaths": ["/spec/replicas"] } ],
"result": {
"state": "APPLIED",
"stepsApplied": 1,
"appliedAt": "2026-08-08T14:02:14.702Z",
"reason": null
}
}
}event.type yalnız üç değer alabilir ve bu sınır şemada donmuştur:
plan.applied— bütün adımlar uygulandı.plan.partially_applied— bir kısmı uygulandı;stepsAppliedkaçını söyler.plan.failed— uygulanamadı;result.reasonapiserver'ın kendi cümlesini taşır.
revertOf doluysa bu operasyon başka bir operasyonun geri alınmasıdır ve değeri
o operasyonun kimliğidir — korelasyonu kurabileceğiniz tek yer burasıdır.
Yanıt sözleşmesi
Doğrulamada gövde okunur, bildirimde okunmaz. Sözlük üç kelimeden ibarettir.
HTTP/1.1 200 OK
content-type: application/json
{ "verdict": "deny", "reason": "CHG-0042 kapalı — açık change kaydı yok" }| verdict | stage: plan | stage: approve |
|---|---|---|
allow | Plan ilerler, onay kartı açılır. | Kapı açılır, apply koşar. |
deny | Plan DENIED. Onay kartı hiç doğmaz. Terminal. | 409. Plan AWAITING_APPROVAL'da bekler, kayıt değişmez. Geçicidir — pencere açılınca aynı karta tekrar basılır. |
elevate | Onay çıtası yükselir: kart kaynağın adını yazmanızı ister. | Ret + tazeleme yönlendirmesi. Donmuş bir planın çıtası değiştirilemez; boru hattı baştan koşar ve elevate doğru yerde söylenir. |
reasonzorunlu değil ama işe yarar. Kullanıcının ekranda gördüğü cümle odur — YEKE onu çevirmez, kendi cümlesiyle değiştirmez. 2048 karakterde kırpılır.- Tanımadığımız alanlar sorun değil. Yanıtınıza
ticketIdkoyabilirsiniz; yok sayılır. Yanıt sizin şemanız — alan eklediğiniz gün entegrasyonunuzun sessizce her planı engellemesi kabul edilebilir bir davranış olmazdı. - Gövde tavanı 64 KiB. Aşarsa yanıt anlaşılmamış sayılır.
- Bildirimde gövdeye hiç bakılmaz.
204yeter;2xx= teslim edildi.
2xx ama karar yok ile ulaşılamadı ayrı arızalardır ve
ayrı görünürler. Birincisi (gövde JSON değil, verdict yok, tanınmayan değer) sizi
yanıtın biçimine yollar ve tekrar denenmez — deterministik bir uyumsuzluktur.
İkincisi (bağlantı, zaman aşımı, 5xx) servisin kendisine yollar ve bir kez
tekrar denenir.
İmza doğrulama
Bir imza sırrı tanımlarsanız her istek x-yeke-signature-256 taşır.
İmza, ham gövdenin HMAC-SHA256'sıdır, hex kodlu. Başlıklar imzaya dâhil değildir:
başlıkları da kapsayan kanonik bir dize, sizden bizim sıralama kuralımızı yeniden yazmanızı
isterdi ve o gerçeklemenin ayrıştığı gün imza sessizce tutmazdı. Tekrar oynatmaya karşı
koruma gövdenin içinde: deliveryId imzanın altındadır.
Gövdeyi ayrıştırmadan önce doğrulayın ve ham baytlar üzerinde çalışın — JSON'u ayrıştırıp yeniden serileştirmek anahtar sırasını ve boşlukları değiştirir, imza tutmaz.
// Node.js — express, ham gövde ile import { createHmac, timingSafeEqual } from "node:crypto"; app.post("/yeke/hook", express.raw({ type: "application/json" }), (req, res) => { const expected = createHmac("sha256", process.env.YEKE_HOOK_SECRET) .update(req.body) // Buffer — ayrıştırılmamış hâli .digest("hex"); const got = req.get("x-yeke-signature-256") ?? ""; // Uzunluk eşit değilse timingSafeEqual FIRLATIR; önce onu ele. if (got.length !== expected.length || !timingSafeEqual(Buffer.from(got), Buffer.from(expected))) { return res.status(401).json({ error: "bad signature" }); } const body = JSON.parse(req.body.toString("utf8")); res.json({ verdict: "allow" }); });
# Python — Flask import hmac, hashlib, os from flask import request, jsonify @app.post("/yeke/hook") def yeke_hook(): raw = request.get_data() # bytes, ayrıştırılmamış expected = hmac.new(os.environ["YEKE_HOOK_SECRET"].encode(), raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, request.headers.get("x-yeke-signature-256", "")): return jsonify(error="bad signature"), 401 body = request.get_json() return jsonify(verdict="allow")
İmza sırrı en az 8 karakter olmalıdır. Daha kısası reddedilir; sebep teşhis metinlerindeki maskedir — kısa bir dize her yerde eşleşir ve maskeyi işe yaramaz hâle getirirdi. Sır bir kez yazılır, bir daha okunamaz: ekran bir sırrın var olduğunu gösterebilir, ne olduğunu asla.
Zaman aşımı, tekrar, fail-closed
İki tipin dayanıklılık modeli farklıdır, çünkü birinde bir insan bekliyor.
| Doğrulama | Bildirim | |
|---|---|---|
| Zaman aşımı | timeoutMs — 500–10000 ms, varsayılan 5000. Her deneme için taze kurulur. | |
| Tekrar | En fazla 2 deneme (yani bir tekrar), yalnız geçici arızada. Sabit; ayarlanamaz. | maxAttempts — 1–10. 1 = hiç tekrarlama. |
| Bekleme | Kısa ve jitter'lı: onay bekleyen bir insanın penceresindeyiz. | Üstel: 60 sn tabanlı, ×2, %20 jitter, tavan 15 dk. |
| Neyin tekrarı | Ağ hatası, zaman aşımı ve 5xx. 4xx ve anlaşılmayan gövde tekrarlanmaz — deterministik yanlış yapılandırmadır; tekrarlamak yalnız gecikmeyi ve kuyruğu şişirir. | |
| Tükenince | onFailure: deny → plan durur · warn → plan ilerler, iz kalır. | Ölü kuyruğa düşer ve denetim izine hook.delivery_failed yazılır. |
Varsayılan fail-closed'dur ve bedeli açıktır: onFailure: deny
kurulu bir doğrulama hook'unun endpoint'i çökerse o cluster'da yazma durur. Dengeleyicileri şunlar —
10 sn'lik zaman aşımı tavanı, bilinçli bir taviz olarak warn modu, hook'u ekrandan
kapatan acil yol, ve hook hiç kurulmadığında riskin sıfır olması. Zaman aşımı ve ağ arızası da
operasyonu durdurur — yalnız sizin deny'ınız değil.
Idempotency
x-yeke-delivery tekrar denemelerde aynı kalır. Yan etkili bir endpoint
yazıyorsanız (ticket açmak, kanala mesaj düşmek) o değeri saklayın ve aynı teslim ikinci kez
geldiğinde işlemi tekrarlamayın. Doğrulama tarafında ek bir tuzak var: aynı plan
stage: plan ve stage: approve ile iki kez sorulur ve bunlar
ayrı teslimlerdir — aynı plana ait olduklarını plan.id söyler.
Hook'u tanımlama
Yönetim ekranından: Yönetim → Hook'lar → Yeni hook. Yeniden başlatma gerekmez.
| Alan | Tip | Not |
|---|---|---|
| Kimlik | ikisi de | Denetim izinde, teslim kuyruğunda ve x-yeke-hook başlığında bu adla anılır. Sonradan değiştirilemez. |
| Adres | ikisi de | http ya da https. Host'un izin listesinde olması şart (aşağıda). |
| Zaman aşımı | ikisi de | 500–10000 ms. |
| Kapsam | ikisi de | Tüm cluster'lar ya da seçili cluster'lar. Eşleşme cluster kimliğiyledir, adıyla değil — ad değişebilir, kimlik değişmez. |
| Aşamalar | doğrulama | plan, approve ya da ikisi. Yazılmazsa plan. Yalnız bakım penceresi bekleten bir hook sadece approve seçebilir. |
| Ulaşılamazsa | doğrulama | deny (varsayılan, fail-closed) ya da warn. |
| Olaylar | bildirim | Üç terminal sonuçtan seçim. Başka bir denetim olayına abone olunamaz. |
| Teslim denemesi | bildirim | 1–10. |
| Sırlar | ikisi de | authorization başlığı ve/veya imza sırrı. Tek yönlü saklanır. |
Adresin host'u YEKE_HOOK_ALLOWED_HOSTS içinde olmalıdır. Bu
liste boşken core, iç ağa (loopback, RFC1918, link-local, ULA) çözülen hiçbir hook adresine
çıkmaz — genel internetteki bir endpoint çalışmaya devam eder, kesilen tek şey hook'ların hiç işi olmayan
iç ağdır. Sebep SSRF: hook adresini core çağırır ve core cluster'larınıza erişebilen bir ağ
konumunda oturur; kutudan çıktığı hâliyle ürün, yönetici ekranından sürülebilen bir iç-ağ
tarayıcısı olmamalıdır. İç bir adrese bilerek hook kurmak meşru bir hamledir ve yolu açıktır —
listeye yazmak:
YEKE_HOOK_ALLOWED_HOSTS=itsm.ic.ornek.com,change-board
Liste verildiğinde liste kazanır ve iç adres kontrolü hiç koşmaz. Kapı iki yerde birden
işler — kaydı yazarken ve her çağrıda; ikincisi olmasa listeyi daraltmanın hiçbir etkisi
olmaz, daha önce kaydedilmiş hook'lar sessizce çıkmaya devam ederdi. Eşleşme host dizesi
üzerindedir ve port dâhil değildir: ornek.com listedeyse o host'un her portu açıktır.
Eksikse arıza yanıltıcıdır. Ağ erişilebilir olsa bile her çağrı
HOOK_HOST_NOT_ALLOWED ile düşer ve fail-closed kurulumda bu, “hook çalışıyor ama hep
hayır diyor” gibi görünür. Teşhisin ucuz yolu core log'unda o kodu aramaktır.
Çalışan referans endpoint
Bağımlılıksız, tek dosya. Kopyalayın, çalıştırın, adresini hook'a yazın.
// node yeke-hook.mjs — :8787/hook (doğrulama) ve :8787/notify (bildirim) import { createServer } from "node:http"; import { createHmac, timingSafeEqual } from "node:crypto"; const SECRET = process.env.YEKE_HOOK_SECRET; const seen = new Set(); // idempotency: deliveryId const read = async (req) => { const chunks = []; for await (const c of req) chunks.push(c); return Buffer.concat(chunks); }; const signatureOk = (raw, header) => { if (!SECRET) return true; // sır yoksa imza da yok const expected = createHmac("sha256", SECRET).update(raw).digest("hex"); const got = header ?? ""; return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected)); }; createServer(async (req, res) => { const raw = await read(req); const json = (code, payload) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(payload)); }; if (!signatureOk(raw, req.headers["x-yeke-signature-256"])) { return json(401, { error: "bad signature" }); } const body = JSON.parse(raw.toString("utf8") || "{}"); // ── doğrulama ──────────────────────────────────────────────── if (req.url === "/hook") { // Sınama çağrısında plan YOK; ulaşılabilirlik ölçülüyor. if (body.stage === "test") return json(200, { verdict: "allow" }); // Karar YALNIZCA dışarının bildiği bir şeye dayanmalı. const ns = body.plan?.steps?.[0]?.target?.namespace; const ticket = await acikChangeKaydi(ns); // ← sizin ITSM'iniz return ticket ? json(200, { verdict: "allow" }) : json(200, { verdict: "deny", reason: `${ns} için açık change kaydı yok` }); } // ── bildirim ───────────────────────────────────────────────── if (req.url === "/notify") { // Aynı teslim iki kez gelebilir; yan etki bir kez koşmalı. if (!seen.has(body.deliveryId)) { seen.add(body.deliveryId); await kanalaYaz(body.event.type, body.operation); // ← sizin Slack'iniz } res.writeHead(204).end(); // 2xx = teslim alındı return; } json(404, { error: "not found" }); }).listen(8787);
Kurduktan sonra yönetim ekranındaki Sına düğmesi endpoint'in ayakta olduğunu ve kimliğinizi
kabul ettiğini tek çağrıda ölçer. O çağrının kararı hiçbir şeye dönüşmez — deny
dönseniz bile hiçbir plan bloklanmaz.
Hook'un yapamadıkları
Bunların hepsi kodda zorlanıyor; entegrasyonunuzu bunlara göre tasarlayın.
allowinsan onayını atlamaz. Siz “evet” derken bile onay kartı açılır ve bir insanın basması beklenir. Hook kapı ekler, kapı kaldırmaz.- Hook planı değiştiremez. Yanıt sözlüğü üç kelimedir. Replika sayısını düzeltmek, bir adım eklemek, hedefi değiştirmek — hiçbiri mümkün değil, alanı da yok.
- Denetim izinin tamamına abone olunamaz. Yalnız üç terminal operasyon sonucu teslim
edilir.
auth.logingibi bir olay yazmayı denerseniz kayıt reddedilir ve ret, sınırı adıyla söyler. İzin tamamının dışarı aktarımı ayrı bir yetenektir ve garantileri (sürekli akış, boşluk tespiti, imzalı biçim) bundan farklıdır. - Nesne değeri hiç görmezsiniz. Yukarıdaki gövde disiplini bir ayar değil sınırdır.
- Yönlendirme izlenmez.
3xxbir hatadır: bir302, imzalı gövdeyi veauthorizationbaşlığını operatörün yazmadığı bir host'a taşırdı. - Yanıtınız
2xxolmalı.200+ karar taşıyan gövde, ya da bildirimde herhangi bir2xx.
Kurmadan önce denemek ister misiniz?
Kendi sisteminizi bağlamadan önce zincirin nasıl davrandığını görmek için bir simülatör var: dış sistemi oynayan sahte bir endpoint ve gelen her isteğin ham gövdesini gösteren bir panel. Yukarıdaki “gövdede nesne değeri yoktur” iddiasını gözle doğrulayabileceğiniz yer orası.