Ders 05 / 16
Gizli Değer Yönetimi
Üretimdeki gizli değerin üç ölçüsü: döndürme sırasında iki anahtarın birlikte geçerli olduğu pencerede kaç isteğin hangi sürümle doğrulandığı, temizleme adımı atlandığında sızan değerin ne kadar geçerli kaldığı ve kodda kalan sabit değeri arayan taramanın yanlış geçme ile yanlış kalma sayıları.
İçindekiler
Önceki ders taşımayı sertleştirdi: dışarıdan gelen istek artık şifreli bir kanaldan giriyor ve tarayıcıya ne yapabileceğini söyleyen başlıklar sunucu tarafında konuyor. Kanalın iki ucu kapatıldığında geriye kanalın taşıdığı şey kalır. Ödünç sisteminin yedi servisi birbirini çağırırken kim olduklarını bir imza anahtarıyla kanıtlar; katalog kimliği, ödeme anahtarı ve bildirim belirteci de aynı sınıftandır. Bunlar gizli değerdir ve taşıma katmanı onları ancak ağ üzerinde korur — durdukları yerde değil.
Bu değerlerin test ortamına dağıtımı Gizli Değer ve Veri Yönetimi dersinde ölçülmüştü: hangi değerin ortama girdiği, ne kadar kapsam aldığı ve bir koşumun artefaktlarında kaç noktadan sızdığı orada sayıldı. Üretim tarafının sorusu başkadır. Üretimde değer zaten girmiştir, kapsamı zaten geniştir ve kimse onu koşum sonunda geri almaz. Burada ölçülen üç nicelik şudur: değer kaç yerde duruyor, döndürülürken kaç istek kırılıyor, ve sızdığında ne kadar süre işe yarıyor.
BG10 — yedi servis tek bir imza anahtarı halkasını paylaşır. Bileti veren de doğrulayan da aynı halkadan okur; halka sürümlüdür ve depo imzalayan sürüm ile doğrulayan sürüm kümesini ayrı tutar. BG11 — bir servis bileti 120 saniye geçerlidir ve servis bileti bitene kadar tazelemez. BG12 — yeni değer servislere sırayla, 45 saniye aralıklarla ulaşır; yedi servisin sonuncusu döndürme anından 270 saniye sonra okur.
Değerin Durduğu Yer
Depo, doğrulamanın hangi sürümleri kabul edeceğini zamana bağlı bir küme olarak verir. Döndürme anından önce yalnız eski sürüm, pencere boyunca ikisi, pencere kapandıktan sonra yalnız yeni sürüm.
// kasa.mjs — gizli deger deposu: surumlu anahtar halkasi, imzalama ve dogrulama kumesi import { createHmac, timingSafeEqual } from 'node:crypto'; export const BILET_OMRU = 120; // BG11: bir servis bileti 120 saniye gecerli export const DONDURME_ANI = 300; // yeni surumun uretildigi an export const DAGITIM_ARALIGI = 45; // BG12: servisler 45 saniye arayla yeni degeri okur export const UFUK = 900; export const SERVISLER = ['katalog', 'odunc', 'uyelik', 'bildirim', 'ceza', 'arama', 'raporlama']; // Depo iki kumeyi ayri tutar: imzalayan tek surum, dogrulayan bir veya iki surum. export function kasaAc(pencere) { const anahtar = { 1: 'v1-halka-4c9a1d', 2: 'v2-halka-77e5b0' }; const kapanis = pencere === Infinity ? Infinity : DONDURME_ANI + pencere; return { dogrulayan: (t) => (t < DONDURME_ANI ? [1] : t < kapanis ? [1, 2] : [2]), imzala: (surum, govde) => createHmac('sha256', anahtar[surum]).update(govde).digest('base64url'), dogrula(t, govde, imza) { for (const surum of this.dogrulayan(t)) { const beklenen = Buffer.from(this.imzala(surum, govde)); const gelen = Buffer.from(imza); if (beklenen.length === gelen.length && timingSafeEqual(beklenen, gelen)) return surum; } return 0; }, }; } // Degerin kac yerde durdugu: her servis hem surecinde hem zamanlanmis isinde okuyor. export const YERLESIM = { dagitik: { yer: SERVISLER.length * 2, okuma: 0 }, merkezi: { yer: 1, okuma: SERVISLER.length }, };
Son satırlar bu kursun üçüncü sorusunu karşılıyor: ayar kaç yerde tekrarlanıyor. Değer her servisin sürecinde ve her servisin zamanlanmış işinde ayrı ayrı duruyorsa on dört yerdedir; çalışma anında tek bir depodan okunuyorsa bir yerdedir ve yedi okuma noktası vardır. Fark, döndürmenin maliyetinde görünür.
Döndürme Penceresi
Aşağıdaki koşum gerçek bir doğrulama servisi ayağa kaldırır ve 900 saniyelik bir zaman çizgisini beş saniyelik adımlarla dolaşır. Zaman sanaldır: her istek taşıdığı saati başlıkta bildirir, böylece sayılar duvar saatine bağlı olmaz. Pencere genişliği taranır ve her genişlik iki kez koşar — biri servislerin biletlerini olağan sırayla tazelediği hâl, öteki her servisin son eski bileti yeni değeri okumasından bir saniye önce tazelediği en kötü hizalama.
// dondurme.mjs — ortusme penceresi tarama: hangi anahtarla dogrulandi, kac istek reddedildi import { createServer } from 'node:http'; import { kasaAc, BILET_OMRU, DONDURME_ANI, DAGITIM_ARALIGI, UFUK, SERVISLER, YERLESIM } from './kasa.mjs'; let kasa = kasaAc(0); const sunucu = createServer((istek, yanit) => { const t = Number(istek.headers['x-saat']); const govde = istek.headers['x-bilet']; const verildi = Number(govde.split('.')[1]); const surum = kasa.dogrula(t, govde, istek.headers['x-imza'] ?? ''); const [kod, sonuc] = surum === 0 ? [401, 'imza'] : t >= verildi + BILET_OMRU ? [401, 'sure'] : [200, surum]; yanit.writeHead(kod, { 'content-type': 'application/json' }); yanit.end(JSON.stringify({ sonuc })); }); sunucu.listen(8951, '127.0.0.1'); const TABAN = 'http://127.0.0.1:8951/odunc'; // Servis i, DONDURME_ANI + i*DAGITIM_ARALIGI aninda yeni degeri okur; bileti bitene kadar tutar. const okumaAni = (i) => DONDURME_ANI + i * DAGITIM_ARALIGI; const SON_OKUMA = okumaAni(SERVISLER.length - 1); // hizala: her servisin son v1 bileti, yeni degeri okumasindan bir saniye once tazelenir (en kotu hal). async function kosum(pencere, hizala = false) { kasa = kasaAc(pencere); const bilet = SERVISLER.map(() => null); const hizalandi = SERVISLER.map(() => false); const sayac = { 1: 0, 2: 0, imza: 0, sure: 0 }; let sonRet = null; for (let t = 0; t <= UFUK; t += 5) { const i = (t / 5) % SERVISLER.length; const kotu = hizala && !hizalandi[i] && bilet[i] !== null && t >= okumaAni(i); if (bilet[i] === null || t >= bilet[i].verildi + BILET_OMRU || kotu) { const verildi = kotu ? okumaAni(i) - 1 : t; const surum = verildi < okumaAni(i) ? 1 : 2; const govde = `${SERVISLER[i]}.${verildi}`; bilet[i] = { surum, govde, imza: kasa.imzala(surum, govde), verildi }; hizalandi[i] ||= kotu; } const y = await fetch(TABAN, { headers: { 'x-saat': String(t), 'x-bilet': bilet[i].govde, 'x-imza': bilet[i].imza } }); const { sonuc } = await y.json(); sayac[sonuc] += 1; if (y.status === 401) sonRet = t; } // Sizmis eski surumle ufuktaki deneme: deger hala kapiyi aciyor mu. const govde = `arama.${UFUK}`; const sizan = await fetch(TABAN, { headers: { 'x-saat': String(UFUK), 'x-bilet': govde, 'x-imza': kasa.imzala(1, govde) } }); return { ...sayac, sonRet, sizan: sizan.status }; } const s = (x, n) => String(x).padStart(n); console.log(`${'pencere'.padEnd(12)}${s('v1', 6)}${s('v2', 6)}${s('red', 6)}${s('son red', 9)}` + `${s('kotu red', 10)}${s('sizan v1', 10)}`); for (const pencere of [0, 100, 200, 300, 400, Infinity]) { const r = await kosum(pencere); const k = await kosum(pencere, true); const ad = pencere === Infinity ? 'temizlemesiz' : `${pencere} sn`; console.log(`${ad.padEnd(12)}${s(r[1], 6)}${s(r[2], 6)}${s(r.imza + r.sure, 6)}` + `${s(r.sonRet ?? '-', 9)}${s(k.imza + k.sure, 10)}${s(r.sizan, 10)}`); } sunucu.close(); const gerekli = SON_OKUMA - DONDURME_ANI + BILET_OMRU; console.log(`\nson okuma ${SON_OKUMA} sn, bilet omru ${BILET_OMRU} sn -> aritmetik alt sinir ${gerekli} sn`); for (const [ad, y] of Object.entries(YERLESIM)) { console.log(`${ad}: deger ${y.yer} yerde duruyor, calisma aninda ${y.okuma} okuma, ` + `yilda 4 dondurme ${y.yer * 4} elle adim eder`); } const gun = 24 * 60; for (const donem of [90, 30, 7]) { console.log(`dondurme donemi ${donem} gun -> sizan anahtarin ortalama gecerliligi ${donem / 2} gun, ` + `en kotu ${donem} gun; ayni anda sizan bilet ${BILET_OMRU / 60} dakika (${(donem * gun / (BILET_OMRU / 60)).toFixed(0)} kat kisa)`); }
pencere v1 v2 red son red kotu red sizan v1 0 sn 60 85 36 555 51 401 100 sn 80 85 16 555 31 401 200 sn 90 85 6 555 15 401 300 sn 96 85 0 - 3 401 400 sn 96 85 0 - 0 401 temizlemesiz 96 85 0 - 0 200 son okuma 570 sn, bilet omru 120 sn -> aritmetik alt sinir 390 sn dagitik: deger 14 yerde duruyor, calisma aninda 0 okuma, yilda 4 dondurme 56 elle adim eder merkezi: deger 1 yerde duruyor, calisma aninda 7 okuma, yilda 4 dondurme 4 elle adim eder dondurme donemi 90 gun -> sizan anahtarin ortalama gecerliligi 45 gun, en kotu 90 gun; ayni anda sizan bilet 2 dakika (64800 kat kisa) dondurme donemi 30 gun -> sizan anahtarin ortalama gecerliligi 15 gun, en kotu 30 gun; ayni anda sizan bilet 2 dakika (21600 kat kisa) dondurme donemi 7 gun -> sizan anahtarin ortalama gecerliligi 3.5 gun, en kotu 7 gun; ayni anda sizan bilet 2 dakika (5040 kat kisa)
Pencerenin Okunması
İlk satır penceresiz döndürmedir ve bu kursun kuralının en açık örneğidir. Yeni sürüm üretilir üretilmez doğrulama eskisini kabul etmeyi bırakır; hiçbir yerde yapılandırma hatası yoktur, depo çalışır, imza doğrudur, servisler ayaktadır. Yine de 181 isteğin 36’sı reddedilir. Reddedilen istekler döndürme anında değil, döndürmeden 255 saniye sonrasına kadar sürer: son ret 555’inci saniyededir. Sebep, elindeki bileti daha bitmeden atmayan servistir; en son okuyan servisin bileti 570’inci saniyeye kadar eski sürümle imzalanmıştır.
Pencere genişledikçe ret sayısı 36’dan 16’ya, 6’ya ve sıfıra iner. Sıfırın hangi genişlikte geldiğine dikkat etmek gerekir: bu koşumda 300 saniye yetmiştir, ama aynı genişlik en kötü hizalamada üç isteği reddeder. Koşumdan bağımsız nicelik tablonun altındaki satırdır — son okuma anı ile döndürme anı arasındaki fark, artı bilet ömrü: 270 + 120 = 390 saniye. Ölçülen değer (300) doğru ayarı vermez; yalnızca o koşumun hizalamasını verir. Pencereyi ölçüme göre seçen bir ekip, çizelge birkaç saniye kaydığında yeniden istek düşürür ve düşen istekler döndürmeden dört dakika sonra geldiği için sebeple bağdaştırılmaz.
Son satır ise ölçünün tersinden yalan söylediği yerdir. Temizlemesiz düzende doğrulama iki sürümü de süresiz kabul eder. Düşen istek sıfırdır, imza hatası sıfırdır, gösterge tablosu temizdir; döndürme “başarılı” görünür. Sızan eski değerle 900’üncü saniyede yapılan deneme yine de 200 alır — öteki beş satırda 401 alır. Yeni sürümün üretilmesi tek başına eskisini geçersiz kılmaz; geçersiz kılan şey kapatma adımıdır ve o adım atlandığında hiçbir ölçü kötüleşmez. Döndürmenin amacı budur ve döndürme tam olarak bu adımda sessizce başarısız olur.
Sızan Değerin Ömrü
Bir değerin sızdığı fark edildiğinde sorulan soru “ne kadar süre işe yaradı” değil, “ne kadar süre işe yarayacak” sorusudur ve yanıtı döndürme dönemine bağlıdır. Sızma anı dönem içinde herhangi bir yere düşebileceğinden bekleneni dönemin yarısı, en kötüsü dönemin tamamıdır: doksan günlük dönemde kırk beş gün, otuz günlükte on beş gün, yedi günlükte üç buçuk gün. Bu aritmetik, döndürmeyi sıklaştırmanın neyi satın aldığını gösterir — kapıyı kapatmayı değil, kapının açık kalacağı süreyi.
Aynı satırların ikinci yarısı asıl kaldıracı gösteriyor. Sızan şey anahtarın kendisi değil de anahtarla üretilmiş bir biletse ömrü 120 saniyedir; doksan günlük dönemin altmış dört bin katı kısa. Uzun ömürlü değeri az sayıda yerde tutup ondan kısa ömürlü bilet türetmek, döndürme sıklığından bağımsız olarak açık kalma süresini dört büyüklük sırası kısaltır. Yerleşim satırları bunun bedelini yazıyor: değer on dört yerde duruyorsa yılda dört döndürme elli altı elle adım eder ve her adım kaçırılabilir; tek yerde duruyorsa dört adım eder ve yedi servis onu çalışma anında okur.
Kodda Kalan Değer
Yukarıdaki bütün aritmetik, değerin yapılandırmada durduğunu varsayar. Kodda duran bir değer döndürüldüğünde geçersiz olmaz — kod değişene ve yayınlanana kadar eski değer imzalamaya devam eder, dolayısıyla son okuma anı yayın süresi kadar geriye kayar ve gerekli pencere onunla birlikte büyür. Bu yüzden kodda kalan sabit değeri aramak, döndürme düzeneğinin bir parçasıdır.
// tarama.mjs — kodda kalan sabit degerin taranmasi: yanlis gecme ve yanlis kalma // [dosya, satir, gercekten gizli mi] const kaynak = [ ['odunc/bilet.mjs', "const IMZA_ANAHTARI = 'v2-halka-77e5b0';", true], ['rapor/aktar.mjs', "const DEPO_PAROLASI = 'k3M-pW9-zQ4-vB7-nR2';", true], ['bildirim/gonder.mjs', "const ayar = { ust: 'T-4f8a1c9d2b6e0357a9c4f1b8' };", true], ['arama/dizin.mjs', "const yenile = 'Zq8Rv3Nw6Lp1Ke5Ty9Xu2Bd7Hm4';", true], ['uyelik/sinama.mjs', "const SINAMA_BELIRTECI = 'sb-9d31-77af-2c60';", true], ['katalog/kayit.mjs', "const KATALOG_KIMLIGI = 'ac-kutuphane-govde-2';", false], ['odunc/ayar.mjs', "const ANAHTAR_ADI = 'imza-anahtari-v2';", false], ['arayuz/ikon.mjs', "const IKON = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAI';", false], ['ceza/ayar.mjs', "const gizliDizin = './veri/gecici/ceza';", false], ['bildirim/ornek.mjs', "const BELIRTEC_YERI = 'DEGISTIR-BENI-KURULUMDA';", false], ['uyelik/kimlik.mjs', "const uyeKimligi = '9f2c1a4e-77b0-4d31-8c6a-2e5b90f14d73';", false], ]; const deger = (satir) => (satir.match(/'([^']+)'/) ?? [, ''])[1]; const ad = (satir) => (satir.match(/(?:const|let)\s+(\w+)|(\w+)\s*:/) ?? [, ''])[1] ?? ''; function entropi(d) { const say = {}; for (const c of d) say[c] = (say[c] ?? 0) + 1; return -Object.values(say).reduce((a, n) => a + (n / d.length) * Math.log2(n / d.length), 0); } const AD_KALIBI = /anahtar|gizli|parola|belirtec|imza|sifre/i; const IZINLI = [/^ac-/, /^DEGISTIR-BENI/]; const kurallar = { 'ad kalibi': (s) => AD_KALIBI.test(ad(s)) && deger(s).length >= 8, 'entropi': (s) => deger(s).length >= 20 && entropi(deger(s)) >= 3.6, 'birlesim': (s) => kurallar['ad kalibi'](s) || kurallar['entropi'](s), 'birlesim + izin listesi': (s) => kurallar.birlesim(s) && !IZINLI.some((r) => r.test(deger(s))) && !deger(s).includes('/'), }; const s = (x, n) => String(x).padStart(n); console.log(`${'kural'.padEnd(24)}${s('bulgu', 7)}${s('yakalanan', 11)}${s('yanlis gecme', 14)}${s('yanlis kalma', 14)}`); for (const [kad, kural] of Object.entries(kurallar)) { const bulgu = kaynak.filter(([, satir]) => kural(satir)); const dogru = bulgu.filter(([, , gizli]) => gizli).length; console.log(`${kad.padEnd(24)}${s(bulgu.length, 7)}${s(`${dogru}/5`, 11)}` + `${s(5 - dogru, 14)}${s(bulgu.length - dogru, 14)}`); } console.log('\nad kalibinin kacirdigi gercek degerler:'); for (const [dosya, satir, gizli] of kaynak) { if (gizli && !kurallar['ad kalibi'](satir)) { console.log(` ${dosya.padEnd(22)} ad=${ad(satir).padEnd(8)} ` + `uzunluk=${s(deger(satir).length, 2)} entropi=${entropi(deger(satir)).toFixed(2)}`); } } console.log('entropi kuralinin yanlis kaldigi zararsiz dizgeler:'); for (const [dosya, satir, gizli] of kaynak) { if (!gizli && kurallar.entropi(satir)) { console.log(` ${dosya.padEnd(22)} uzunluk=${s(deger(satir).length, 2)} ` + `entropi=${entropi(deger(satir)).toFixed(2)} izinli=${IZINLI.some((r) => r.test(deger(satir)))}`); } }
kural bulgu yakalanan yanlis gecme yanlis kalma ad kalibi 6 3/5 2 3 entropi 5 2/5 3 3 birlesim 10 5/5 0 5 birlesim + izin listesi 7 5/5 0 2 ad kalibinin kacirdigi gercek degerler: bildirim/gonder.mjs ad=ayar uzunluk=26 entropi=4.09 arama/dizin.mjs ad=yenile uzunluk=27 entropi=4.75 entropi kuralinin yanlis kaldigi zararsiz dizgeler: katalog/kayit.mjs uzunluk=20 entropi=3.78 izinli=true bildirim/ornek.mjs uzunluk=23 entropi=3.76 izinli=true uyelik/kimlik.mjs uzunluk=36 entropi=4.00 izinli=false
Ad kalıbı tek başına beş gerçek değerin üçünü bulur. Kaçırdığı ikisi listenin sonunda adıyla
duruyor: değişkenin adı ayar ve yenile. Adlandırma sözleşmesine dayanan bir tarama, sözleşmeye
uymayan satırı göremez ve gizli bir değeri koda sabitleyen kişi zaten sözleşmeyi izlememiştir.
Entropi kuralı bu ikisini yakalar ama kendi başına daha kötüdür: beşte iki. Kısa ve düzenli
biçimdeki değerler — on dokuz karakterlik parola, kısa çizgilerle bölünmüş bilet — eşiğin altında
kalır.
İkisinin birleşimi beş değerin beşini bulur ve bedeli beş yanlış kalmadır. Yanlış kalanların üçü kural yazılarak elenebilir: açık katalog kimliğinin öneki, kurulumda değiştirilecek yer tutucu ve eğik çizgi taşıyan dizin yolu. Kalan iki yanlış kalma elenemez, çünkü biri anahtarın adını tutan bir sabittir, öteki bir üye kimliğidir; ikisi de gizli değere birebir benzer. Buradaki takas kursun her yerinde aynıdır: yanlış geçmenin bedeli kodda kalan ve döndürülemeyen bir değerdir, yanlış kalmanın bedeli ise iki satırlık elle incelemedir. Eşiği ikinci maliyete göre ayarlamak, birinciyi ödemeyi göze almak demektir.
Özet
- Üretimdeki gizli değerin üç ölçüsü kaç yerde durduğu, döndürülürken kaç istek kırdığı ve sızdığında ne kadar süre işe yaradığıdır; test ortamındaki ölçü olan sızma yüzeyi burada zaten en geniş hâlindedir.
- Penceresiz döndürme 181 isteğin 36’sını reddetti ve retler döndürmeden 255 saniye sonrasına kadar sürdü; hiçbir bileşen hata vermedi.
- Gerekli pencere ölçümden değil aritmetikten okunur: son okuma anı eksi döndürme anı artı bilet ömrü, bu düzende 390 saniye. Bu koşumda 300 saniye de sıfır ret verdi, en kötü hizalamada üç.
- Temizleme adımı atlandığında düşen istek sıfır kalır, gösterge temiz görünür ve sızan eski anahtar 900’üncü saniyede hâlâ 200 alır; döndürme yalnız kapatma adımıyla tamamlanır.
- Anahtarı az yerde tutup ondan 120 saniyelik bilet türetmek, sızan değerin ömrünü doksan günlük dönemin altmış dört bin katı kısaltır; değerin on dört yerde durması yılda elli altı elle adım, tek yerde durması dört adım eder.
- Sabit değer taramasında ad kalıbı beşte üç, entropi beşte iki yakaladı; birleşimi beşte beş yakaladı ve beş yanlış kalma üretti — üçü kuralla elenebilir, ikisi elenemez.
Sonraki Adım
Bu dersin bütün ölçümleri kendi yazdığımız kodun içinde kaldı: anahtarı biz ürettik, biletin ömrünü biz seçtik, taramanın eşiğini biz koyduk. Oysa isteği karşılayan kodun büyük bölümü bizim değildir. Ödünç servisinin süreci ayağa kalktığında yüzlerce dosya yüklenir ve bunların çok azı ekibin yazdığı satırlardır; her biri bir sürüm numarasına bağlıdır ve o numara bir gün değişir. Sonraki ders bu bağımlılığı bir yapılandırma kararı olarak ele alır — yükseltmeyi ne sıklıkta yapmanın biriken riskle kırılan sürüm arasında nasıl bir takas kurduğunu, üç ayrı politikayı aynı bağımlılık ağacında koşturarak sayar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.