zero-knowledge
Mimari Körlük Olarak Zero-Knowledge (2/5)
Çoğu secret yöneticisi getSecret(name) fonksiyonunu dışa açar ve düz metin AI'ın transcript'ine düşer. lodos şeması bu alışverişi yapısal olarak imkânsız kılar, AI bir secret'ın değerini göremez ve build bunu zorunlu kılar.
AI asistanınız az önce Stripe canlı anahtarınızı görmek istedi. Çoğu secret yöneticisi buna izin verirdi, API getSecret(name)'i dışa açar, agent onu çağırır, düz metin transcript'e düşer ve oradan agent'ın konuştuğu her pipeline'a geçer. İlginç soru şu: hangi tür şema bu alışverişi yapısal olarak imkânsız kılar?
lodos için, savunması kolay ama mühendisliği zor bir pozisyon seçtim: AI, vault'taki hiçbir secret'ın değerini göremez. "Görmeyecek" değil, göremez. Çağıran AI'a düz metin bir secret döndüren hiçbir kod yolu yok. Prisma şemasında AI'ın okuyabileceği bir kolon yok; IPC katmanında şifresi çözülmüş bir alan döndüren bir metot yok; MCP tool registry'sinde kontratı bana değeri ver olan bir tool yok. Gelecekteki ben böyle bir şey eklemeye kalkarsa, build script'i bunu merge'den önce yakalar.
Bu yazı şema turu, policy körlüğü yerine mimari körlük tasarlamanın gerçekte nasıl bir şey olduğu ve karşılığında nelerden vazgeçtiğiniz. Vault, en gurur duyduğum katman; kısmen çünkü mühendisliği, yapmadığı şey konusunda dürüst.
İki-store modeli
Naif tasarım tek bir kolondur: encryptedJson section payload'unu tutar, okuma sırasında şifresini çözersiniz, alanları şifresi çözülmüş blob'dan sunarsınız. Bu çalışır ve tek bir bug sınıfı vardır, her okuma yolu er ya da geç şifre çözer. account_id'yi (public) isteyen lookup, api_key'in (sensitive) de şifresini çözer. Audit log boşa giden şifre-çözmeyi yakalayabilir; LLM transcript'ine sızıntıyı muhtemelen yakalayamaz.
Bu yüzden vault section'larının yan yana iki store'u var:
model SecretSection {
id String @id @default(cuid())
name String // "stripe", "aws", "github"
encryptedJson Bytes // libsodium secretbox(sensitive-fields, sectionKey)
nonSensitive Json // { account_id, region, username, ... } - plain
fieldSchema Json // [{key, type, sensitive, required}, ...]
keyWrapped Bytes // sectionKey, wrapped by masterDEK
// ...
}

Her alan sensitive: true mu yoksa sensitive: false mi olduğunu bilir. Yönlendirme yalnızca bir kez, yazma anında gerçekleşir. Sensitive alanlar libsodium'dan geçer; sensitive olmayan alanlar düz metin bir JSON kolonuna düşer. Sensitive olmayan bir alanı okumak bir SQLite okumasıdır, şifre çözme yok, DEK erişimi yok, audit yazımı yok. Sensitive bir alanı okumak ise master password gerektirir.
Kaybettiğiniz şey: hangi alanın ne olduğu konusunda kendinize yalan söyleme yeteneği. Okuma anında karar veremezsiniz. Şema bu kararı baştan verdirtir.
Kazandığınız şey: silinen bir bug sınıfı. Chat MCP tool'u secrets_field_metadata, sensitive olmayan alanlar için gerçek uzunluğu, sensitive alanlar için 0 döndürür, uzunluğu gizlediğimiz için değil, metadata yolu encryptedJson'a hiç dokunmadığı için. Disiplin, yorum seviyesinde değil, type seviyesinde geçerli.
Şifreleme pipeline'ı
Sensitive taraf Argon2id ve libsodium üzerinde çalışır. Egzotik bir şey yok:
// On unlock
const masterDEK = await argon2id(masterPassword, {
memLimit: 64 * 1024 * 1024, // 64MB
opsLimit: 3,
salt: vault.argon2Salt,
});
// HMAC check against vault.masterKeyCheck - fail-fast on wrong password
// On read of a sensitive field
const sectionKey = unwrap(section.keyWrapped, masterDEK);
const plaintext = sodium.crypto_secretbox_open(
section.encryptedJson, section.nonce, sectionKey,
);
const value = JSON.parse(plaintext)[field];
sectionKey.fill(0); // zero-fill before GC

Körlük iddiası için üç özellik önemli. Birincisi, master DEK yalnızca main-process belleğinde yaşar, asla renderer'da değil, asla chat-engine subprocess'inde değil, asla bir IPC payload'unda değil. İkincisi, tek bir section'ın ele geçirilmesi vault'un ele geçirilmesi anlamına gelmez: her section'ın master DEK tarafından wrap edilmiş kendi rastgele sectionKey'i vardır. Üçüncüsü, unlock durumunun beş dakikalık bir idle timeout'u ve otuz dakikalık mutlak bir tavanı vardır; bundan sonra her section, tekrar-şifre-girme adımı kadar uzakta, okunamaz hale gelir.
Burası, güvenlik odaklı okuyucuların "evet ama" diyeceği ve memory dump'ları, swap dosyaları, GPU residency hakkında soru sormaya başlayacağı kısım. Makul sorular, ama çoğunlukla burada kapsam dışı. İlgili nokta şu: AI bu pipeline'a erişemez. Agent SDK, vault durumuna erişimi olmayan bir subprocess'te çalışır; main process ile IPC üzerinden konuşur ve IPC yüzeyinde düz metin döndüren bir metot yoktur.
Pattern A, B, D: asla C
Bu sistemde bir secret'ın kullanılabileceği tam olarak dört yol vardır:
- A: Vault'tan asla çıkmaz. Bir secret, bir workflow içinde adıyla referans alınır, main-process tarafında şifresi çözülür, bir subprocess'in env'ine inject edilir, AI'a asla döndürülmez. AI, YAML içinde
{{ secrets.stripe.live_key }}'i ve yanıtta bir HTTP 200'ü görür. - B: Dar egress filtresi.
postgres://user:pass@host/dbgibi bir değer main-process tarafında şifresi çözülür, bir filtre yalnızcahost'u çıkarır ve buallowedEgressdeğeri haline gelir. Secret bir değer olarak IPC sınırını asla geçmez; ondan türetilmiş bir projeksiyon geçer. - C: Şifresini çöz ve AI'a döndür. ❌ Mevcut değil. Kayıtlı hiçbir MCP tool'u yok. Hiçbir IPC metodu yok. Hiçbir kod yolu yok. Codebase'i
_value_get$ile eşleşen herhangi bir tool adı için grep'lerseniz, sonuç yapı gereği boştur. Build script bunu zorunlu kılar. - D: Inject et ve çalıştır. Secret env'inde olan bir subprocess spawn edilir. Secret ile herhangi bir AI yüzeyi arasında altı katman: yalnızca-env (argv değil), stdout/stderr üzerinde encoding-redactor, shell-string-build yok, çıktı üzerinde content-firewall, opak path'li HMAC audit log kaydı, çağrı başına hard-coded egress allowlist.

Bunları pattern olarak yazmanın anlamı şu: çoğu ürünün gönderdiği C'dir. Onların secret yöneticisi tool'u düz metni agent loop'una döndürür ve buna entegrasyon derler. A'dan D'ye kadar olan pattern'ler de entegrasyondur, sadece secret'ı transcript'e koymazlar.
Bu takas dürüst. Bazı workflow'lar Pattern C ile daha kolay. Hiçbiri Pattern C ile zorunlu değil. Bu yüzden A, B, D'yi gönderiyoruz ve C'yi dışarıda tutmak için build-time bir grep kullanıyoruz.
Opak audit zinciri
Audit log, tasarlarken beni en çok şaşırtan kısımdı.
Bir vault audit satırı şunu söyler: "section X, field Y, actor Z tarafından T zamanında inject edildi, signature S, prev-signature P." Doğal şema fieldPath'i düz metin olarak saklar. Bu bir sızıntıdır: audit tablosuna okuma erişimi olan herkes, AI agent'ın bir şey inject ettiğini değil, stripe.live_key'i inject ettiğini görür. Compliance incelemesi yapan regüle bir alıcı için, o audit tablosunun kendisi secret materyal haline gelir.
Bu yüzden şema path'i saklamaz. Hash'i saklar:
fieldPathHash: sha256(sectionName + '.' + fieldKey).slice(0, 16)
On altı karakterlik opak bir token. Hata yanıtları bile bunu kullanır: injection başarısız olduğunda hata { pathHash: "a3f2…", sectionExists: true }'tür, asla { field: "stripe.live_key", error: "not found" } değil. Main process'te, kullanıcının kendi bağlamında çalışan audit reader, ters lookup'ı yapar. AI düz metin path'i asla görmez ve sızdırılan bir audit tablosu hangi secret'ların saklandığı hakkında hiçbir şey ortaya koymaz.
Zincir HMAC ile bağlıdır, her satır, master DEK'ten türetilen bir sub-key ile (payload + prev-signature)'ı imzalar. verifyAuditChain() başlangıçta çalışır; kurcalanmış veya silinmiş bir satır zinciri kırar ve UI'da bir forensic alert olarak yüzeye çıkar. Hard-delete yasaktır; archive, satırın yerinde bırakıldığı bir soft-delete'tir.
Audit tablosunun ekran görüntüsünü alıp bir SOC2 denetçisine gösterebilirsiniz. Denetçi bir actor kimliği, bir section, opak bir field hash'i, bir timestamp ve bir HMAC zinciri görür. Gerçek secret adları sızmaz. Compliance alıcılarının parasını ödediği şey de tam olarak bu.
Kullanıcı mimariyi bir bug'dan ayırt edemediğinde
İlk gerçek dogfood sırasında bir kullanıcı secret_inject_and_run üzerinden echo $API_KEY çalıştırdı. Çağrı MCP_LETHAL_TRIFECTA_GATE ile başarısız oldu. Kullanıcı teşhis için bunu AI asistanına götürdü; AI, komutun "statik metin" olduğu için reddedildiğini açıklamak için yaklaşık 3 bin token harcadı ve bir curl çağrısına geçmeyi önerdi. Yanlış. Gate koşulu allowedEgress.length === 0 && riskTier === 'novel' idi, çağrının bir egress allowlist'i yoktu, bu yüzden beyan edilmiş bir hedef olmadan potansiyel olarak exfiltrate ediyor diye reddedildi.
Bug ticket'ı moat-win olarak yeniden sınıflandırıldı. Mimari doğru çalışıyordu ve AI'ın kafa karışıklığı zaten kanıttı: sistem, komut içeriğine değil egress şekline göre reddetti ve AI'ın yanlış teşhisi bozuk bir refusal'ın değil, yardımcı olmayan bir tool açıklamasının bir sonucuydu. Açıklamayı düzenledik; gate yerinde kaldı.
Sürekli geri döndüğüm ders şu: AI'ın anlamadığı bir refusal, yine de bir refusal'dır. Mimari körlük, LLM'in buna razı olmasını gerektirmez.
Bunun ticari değeri ne
Bu tür bir işi düşünen founder'lara yaptığım pitch basit. SOC2 ve ISO alıcıları policy olarak "söz veriyoruz"u kabul etmez. "Yapamayız"ı kabul ederler. Canlı anahtarlarınızı görebilen ve görmemeye söz veren bir tedarikçi, zaman zaman logging stack'inde CVE'leri olan bir tedarikçidir. Şeması değeri göremeyen bir tedarikçi ise farklı bir risk kategorisidir ve o kategori farklı fiyatlanır.
İlk günden açık bıraktığım Phase-3 yolu, alıcı-başına wrap. Section key zaten bir indirection; bugün master DEK tarafından bir kez wrap ediliyor. Bir takım ortamında N kez wrap edilir, her alıcının public key'i başına bir kez, ve sunucu yine de şifresini çözemez çünkü hiçbir private key tutmaz. Bu wrap, bir yeniden yazım değil, eklemeli bir işlemdir; çünkü section-başına indirection ilk commit'ten beri generic tutuldu. Bu kapı, şu an için hiçbir özellik gönderilmeden açık kalıyor.
Sıradaki katman
Şema tasarımı secret'ı at-rest ve inject sırasında korur. Ama şemalar koddur ve kod düzenlenebilir. Sıradaki yazı, Regresyon Yaparsam Build'imi Kıran 32 Build-Time Invariant, Prisma şemasının geriye gitmesini, workflow engine'in bir eval büyütmesini ve MCP tool registry'sinin bir secret_value_get filizlendirmesini engelleyen script'i baştan sona anlatıyor. Refusal bir grep haline gelir ve bu grep, dişlerinin hâlâ keskin olduğunu kanıtlamak için kendi sahte ihlaliyle birlikte gönderilir.