29 Ağu 2025·5 dk okuma

PostgreSQL'de değişiklik-tespitli denetim kayıtları ile hash zincirleme

PostgreSQL'de ekleme-yalnız tablolar ve hash zincirleme kullanarak değişiklik-tespitli denetim kayıtları oluşturmayı öğrenin; böylece incelemeler ve soruşturmalar sırasında düzenlemeler kolayca fark edilir.

PostgreSQL'de değişiklik-tespitli denetim kayıtları ile hash zincirleme

Neden normal denetim günlükleri tartışmaya açık olur

Bir denetim izi, bir şey ters göründüğünde güveneceğiniz kayıttır: garip bir iade, kimsenin hatırlamadığı bir izin değişikliği veya “kaybolmuş” bir müşteri kaydı. Denetim izi düzenlenebilirse, delil olmaktan çıkar ve birisi yeniden yazabileceği başka bir veri parçası olur.

Birçok “denetim günlüğü” aslında normal tablolardır. Satırlar güncellenebiliyor veya silinebiliyorsa, hikâye de güncellenebilir ya da silinebilir.

Önemli bir ayrım: düzenlemeleri engellemek, düzenlemeleri tespit edilebilir hale getirmekle aynı şey değildir. İzinlerle değişiklikleri azaltabilirsiniz, ama yeterli erişimi olan (veya çalınmış bir yönetici kimliğiyle) yine de geçmişi değiştirebilecek biri olabilir. Değişiklik-tespiti bu gerçeği kabul eder. Her değişimi engellemeyebilirsiniz, ama değişikliklerin bariz bir iz bırakmasını sağlayabilirsiniz.

Normal denetim günlükleri öngörülebilir nedenlerle itiraz edilir. Ayrıcalıklı kullanıcılar sonradan günlüğü “düzeltebilir”. Ele geçirilmiş bir uygulama hesabı, normal trafiğe benzeyen inanılır girdiler yazabilir. Zaman damgaları geriye doldurularak geç yapılmış bir değişiklik gizlenebilir. Ya da biri sadece en zararlı satırları silebilir.

"Değiştirilmesi-tespitli" demek, denetim izini öyle tasarladığınız anlamına gelir: küçük bir düzenleme bile (bir alanı değiştirmek, bir satırı kaldırmak, olayları yeniden sıralamak) daha sonra tespit edilebilir hale gelir. Sihir vaat etmiyorsunuz. Birisi “Bu günlüğün gerçek olduğunu nasıl biliriz?” diye sorduğunda, günlüğün dokunulup dokunulmadığını gösteren kontroller çalıştırabileceğinizi vaat ediyorsunuz.

Kanıtlamanız gerekenin ne olduğuna karar verin

Bir değişiklik-tespitli denetim izi yalnızca daha sonra karşılaşacağınız soruları yanıtlıyorsa faydalıdır: kim ne yaptı, ne zaman yaptı ve ne değişti.

İşiniz için önemli olaylarla başlayın. Veri değişiklikleri (oluşturma, güncelleme, silme) temel düzeydir, ama soruşturmalar genellikle güvenlik ve erişime dair olaylara dayanır: girişler, parola sıfırlamaları, izin değişiklikleri ve hesap kilitlemeleri. Ödemeler, iadeler, krediler veya ödemelerle ilgileniyorsanız, para hareketini bir satır güncellemesinin yan etkisi olarak değil birinci sınıf olay olarak ele alın.

Sonra bir olayın güvenilir kılınması için ne gerektiğine karar verin. Denetçiler genellikle bir aktör (kullanıcı veya servis), sunucu tarafı zaman damgası, yapılan işlem ve etkilenen nesneyi bekler. Güncellemeler için önceki ve sonraki değerleri saklayın (veya en azından hassas alanları), ayrıca birçok küçük veritabanı değişikliğini tek bir kullanıcı eylemine bağlayabilmek için bir istek id'si veya korelasyon id'si ekleyin.

Son olarak, sisteminizde "değiştirilemez" ifadesinin ne anlama geldiğini açıkça belirtin. En basit kural: denetim satırlarını asla güncelleme veya silme, sadece ekle. Bir şey yanlışsa, eskiyi düzelten veya geçersiz kılan yeni bir olay yazın ve orijinali görünür tutun.

Ekleme-yalnız bir denetim tablosu oluşturun

Denetim verisini normal tablolarınızdan ayrı tutun. Ayrılmış bir audit şeması kazara düzenlemeleri azaltır ve izinlerin yönetilmesini kolaylaştırır.

Hedef basit: satırlar eklenebilir ama değiştirilemez veya kaldırılamaz. PostgreSQL'de bunu ayrıcalıklarla (kimin ne yapabildiği) ve tablo tasarımınızda birkaç güvenlik önlemiyle uygularsınız.

İşte pratik bir başlangıç tablosu:

CREATE SCHEMA IF NOT EXISTS audit;

CREATE TABLE audit.events (
  id            bigserial PRIMARY KEY,
  entity_type   text        NOT NULL,
  entity_id     text        NOT NULL,
  event_type    text        NOT NULL CHECK (event_type IN ('INSERT','UPDATE','DELETE')),
  actor_id      text,
  occurred_at   timestamptz NOT NULL DEFAULT now(),
  request_id    text,
  before_data   jsonb,
  after_data    jsonb,
  notes         text
);

Bazı alanlar soruşturmalar sırasında özellikle kullanışlıdır:

  • occurred_at için DEFAULT now(); böylece zaman istemci tarafından değil veritabanı tarafından damgalanır.
  • Bir kaydı değişiklikler boyunca takip edebilmek için entity_type ve entity_id.
  • Tek bir kullanıcı eylemini birçok satıra bağlayabilmek için request_id.

Rollerle kilitleyin. Uygulama rolünüz INSERT ve SELECT yapabilmeli ama UPDATE veya DELETE yapamamalı. Şema değişiklikleri ve daha güçlü izinler, uygulama tarafından kullanılmayan bir yönetici rolünde tutulmalı.

Değişiklikleri tetikleyicilerle yakalayın (temiz ve öngörülebilir)

Değişiklik-tespitli bir denetim izi istiyorsanız, değişiklikleri yakalamanın en güvenilir yeri veritabanıdır. Uygulama günlükleri atlanabilir, filtrelenebilir veya yeniden yazılabilir. Bir tetikleyici, hangi uygulama, betik veya yönetici aracı tabloya dokunursa dokunsun tetiklenir.

Tetikleyicileri sıkıcı tutun. İşleri tek olsun: önemli tablolarda her INSERT, UPDATE ve DELETE için bir denetim olayı eklemek.

Pratik bir denetim kaydı genellikle tablo adını, işlem türünü, birincil anahtarı, önceki ve sonraki değerleri, bir zaman damgasını ve ilişkili değişiklikleri gruplayabilen tanımlayıcıları (işlem id'si ve korelasyon id'si) içerir.

Korelasyon id'leri “20 satır güncellendi” ile “Bu bir butona tıklamaydı” arasındaki farktır. Uygulamanız her istek için bir korelasyon id ayarlayabilir (örneğin bir DB oturum ayarında) ve tetikleyici bunu okuyabilir. Korelasyon id eksik olduğunda bile gruplama yapabilmek için txid_current()'i de saklayın.

İşte denetim tablosuna sadece ekleme yapan ve bu yüzden öngörülebilir kalan basit bir tetik düzeni (isimleri şemanıza göre ayarlayın):

CREATE OR REPLACE FUNCTION audit_row_change() RETURNS trigger AS $$
DECLARE
  corr_id text;
BEGIN
  corr_id := current_setting('app.correlation_id', true);

  INSERT INTO audit_events(
    occurred_at, table_name, op, row_pk,
    old_row, new_row, db_user, txid, correlation_id
  ) VALUES (
    now(), TG_TABLE_NAME, TG_OP, COALESCE(NEW.id, OLD.id),
    to_jsonb(OLD), to_jsonb(NEW), current_user, txid_current(), corr_id
  );

  RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

Tetikleyicilerin içinde daha fazlasını yapma dürtüsüne direnin. Ek sorgulardan, ağ çağrılarından veya karmaşık dallanmalardan kaçının. Küçük tetikleyiciler test etmesi daha kolay, çalıştırması daha hızlı ve inceleme sırasında itiraz edilmesi daha zor olur.

Düzenlemeler iz bıraksın diye hash zincirleme ekleyin

Denetim günlüklerini merkezileştirin
Dağınık uygulama günlükleri yerine veri değişikliklerini tek yerden kaydeden Go backend servisleri oluşturun.
Backend Oluştur

Ekleme-yalnız bir tablo yardımcı olur, ama yeterli erişimi olan biri geçmiş satırları yine de yeniden yazabilir. Hash zincirleme bu tür bir tahribatı görünür kılar.

Her denetim satırına iki sütun ekleyin: prev_hash ve row_hash (bazen chain_hash olarak da anılır). prev_hash, aynı zincirdeki önceki satırın hash'ini saklar. row_hash ise mevcut satırın hash'ini, satır verisi artı prev_hash kullanılarak hesaplanmış şekilde saklar.

Ne hashlediğiniz önemlidir. Aynı satır her zaman aynı hash'i üretmeli; bunun için stabil, tekrarlanabilir bir girdi istersiniz.

Pratik bir yaklaşım, sabit sütunlardan (zaman damgası, aktör, eylem, varlık id'si), kanonik bir yükten (çoğunlukla jsonb, çünkü anahtarlar tutarlı saklanır) ve prev_hash'ten oluşturulmuş bir kanonik dizeyi hashlemektir.

Boşluk, JSON anahtar sırası veya yerel biçimlendirme gibi anlamsız değişikliklere dikkat edin. Türleri tutarlı tutun ve tek, öngörülebilir bir şekilde serileştirin.

Tüm veritabanı yerine akış başına zincirleyin

Tüm denetim olaylarını tek bir küresel sırada zincirleseniz, yazmalar darboğaza yol açabilir. Birçok sistem zinciri “akış” içinde tutar; örneğin tenant başına, varlık türü başına veya iş nesnesi başına.

Her yeni satır, akışı için en son row_hash'i arar, bunu prev_hash olarak saklar, sonra kendi row_hash'ini hesaplar.

-- Requires pgcrypto
-- digest() returns bytea; store hashes as bytea
row_hash = digest(
  concat_ws('|',
    stream_key,
    occurred_at::text,
    actor_id::text,
    action,
    entity,
    entity_id::text,
    payload::jsonb::text,
    encode(prev_hash, 'hex')
  ),
  'sha256'
);

Zincir başını anlık görüntüleyin

İncelemeler için daha hızlı olmak adına, periyodik olarak (örneğin günlük olarak akış başı için) en son row_hash'i küçük bir snapshot tablosunda saklayın. Bir soruşturmada, tüm geçmişi taramak yerine zinciri her snapshot noktasına kadar doğrulayabilirsiniz. Snapshot'lar ayrıca dışa aktarımları karşılaştırmayı ve şüpheli boşlukları tespit etmeyi kolaylaştırır.

Zinciri bozmadan eşzamanlılık ve sıralama

Gerçek trafik altında hash zincirleme karmaşıklaşır. İki işlem aynı anda denetim satırı yazıp her ikisi de aynı prev_hash'i kullanırsa çatallar oluşabilir. Bu, tek, temiz bir diziyi kanıtlamanızı zayıflatır.

Önce zincirinizin neyi temsil ettiğine karar verin. Tek bir küresel zincir açıklaması en kolay olanıdır ama en yüksek içeriğe sahiptir. Birden çok zincir içeriği azaltır, ama her zincirin neyi kanıtladığı konusunda açık olmalısınız.

Hangi modeli seçerseniz seçin, monoton bir olay id'si (genellikle sequence destekli bir id) ile katı bir sıra tanımlayın. Zaman damgaları çakışabilir ve değiştirilebilir olduğu için yeterli değildir.

prev_hash hesaplanırken yarış koşullarını önlemek için her akış için “son hash al + bir sonraki satırı ekle” işlemini seri hale getirin. Yaygın yaklaşımlar, akış başını temsil eden tek bir satırı kilitlemek veya akış id'si ile anahtarlanmış advisory lock kullanmaktır. Amaç, aynı akışa eşzamanlı iki yazıcının aynı son hash'i okumasını engellemektir.

Partitioning ve sharding, “son satırın” nerede olduğuna etki eder. Denetim verilerini partition etmeyi planlıyorsanız, her zinciri aynı partition içinde tutun; örneğin tenant id'sini hem partition anahtarı hem de akış anahtarı olarak kullanın. Böylece tenant zincirleri, tenantlar daha sonra sunucular arasında taşınsa bile doğrulanabilir kalır.

Soruşturma sırasında zinciri nasıl doğrularsınız

Denetim uygulamanızın sahibi olun
Daha sonra tam kontrol mü istiyorsunuz? Kaynak kodunu dışa aktarın ve denetim mantığınızı şeffaf ve incelenebilir tutun.
Kodu Dışa Aktar

Hash zincirleme, biri sorduğunda zincirin hâlâ sağlam olduğunu kanıtlayabiliyorsanız işe yarar. En güvenli yaklaşım, her satırın hash'ini depolanmış veriden yeniden hesaplayan ve kaydedilmiş değerle karşılaştıran salt okunur bir doğrulama sorgusu (veya iş) çalıştırmaktır.

İstendiğinde çalıştırabileceğiniz basit bir doğrulayıcı

Bir doğrulayıcı şunları yapmalıdır: her satır için beklenen hash'i yeniden oluşturmak, her satırın öncekiye bağlandığını doğrulamak ve herhangi bir tutarsızlıkta işaretlemek.

Aşağıda pencere fonksiyonları kullanan yaygın bir desen var. Sütun adlarını kendi tablonuza göre ayarlayın.

WITH ordered AS (
  SELECT
    id,
    created_at,
    actor_id,
    action,
    entity,
    entity_id,
    payload,
    prev_hash,
    row_hash,
    LAG(row_hash) OVER (ORDER BY created_at, id) AS expected_prev_hash,
    /* expected row hash, computed the same way as in your insert trigger */
    encode(
      digest(
        coalesce(prev_hash, '') || '|' ||
        id::text || '|' ||
        created_at::text || '|' ||
        coalesce(actor_id::text, '') || '|' ||
        action || '|' ||
        entity || '|' ||
        entity_id::text || '|' ||
        payload::text,
        'sha256'
      ),
      'hex'
    ) AS expected_row_hash
  FROM audit_log
)
SELECT
  id,
  created_at,
  CASE
    WHEN prev_hash IS DISTINCT FROM expected_prev_hash THEN 'BROKEN_LINK'
    WHEN row_hash IS DISTINCT FROM expected_row_hash THEN 'HASH_MISMATCH'
    ELSE 'OK'
  END AS status
FROM ordered
WHERE prev_hash IS DISTINCT FROM expected_prev_hash
   OR row_hash IS DISTINCT FROM expected_row_hash
ORDER BY created_at, id;

“Bozuk veya değil”ten öte, boşluklar (bir aralıktaki eksik id'ler), sıradışı bağlantılar ve gerçek iş akışlarıyla eşleşmeyen şüpheli çoğaltmalar için kontrol etmek faydalıdır.

Doğrulama sonuçlarını değiştirilemez olaylar olarak kaydedin

Sorguyu çalıştırıp çıktısını bir ticket'ın içine gömmeyin. Doğrulama sonuçlarını ayrı bir ekleme-yalnız tabloda (örneğin audit_verification_runs) çalıştırma zamanı, doğrulayıcı sürümü, tetikleyen kişi, kontrol edilen aralık ve bozuk bağlantı / hash uyuşmazlığı sayıları ile saklayın.

Bu size ikinci bir iz sağlar: sadece denetim günlüğünün sağlam olduğunu göstermekle kalmaz, aynı zamanda bunu kontrol ettiğinizi de gösterebilirsiniz.

Pratik bir ritim: denetim mantığını etkileyen her deploy sonrası çalıştırın, aktif sistemler için gece çalıştırın ve planlı bir denetimden önce mutlaka çalıştırın.

Değişiklik-tespitini bozacak yaygın hatalar

Tasarım olarak değişiklik tespiti ekleyin
Düzenlemeler tespit edilebilir izler bırakacak şekilde hash zincirleme mantığını backend süreçlerinize uygulayın.
Prototip Oluştur

Çoğu başarısızlık hash algoritmasından değil, istisnalar ve insanlara itiraz etme alanı bırakan boşluklardan kaynaklanır.

Güveni kaybetmenin en hızlı yolu denetim satırlarına güncelleme izni vermektir. "Sadece bu sefer" olsa bile, hem emsal oluşturursunuz hem de geçmişi yeniden yazma yolu açılır. Bir şeyi düzeltmeniz gerekiyorsa, açıklayan yeni bir denetim olayı ekleyin ve orijinali saklayın.

Hash zincirleme ayrıca değişken veri hash'lediğinizde başarısız olur. JSON yaygın bir tuzaktır. Bir JSON dizesini hash'lerseniz, anlamsız farklar (anahtar sırası, boşluk, sayı formatı) hash'i değiştirir ve doğrulamayı gürültülü hale getirir. Kanonik bir biçim tercih edin: normalize edilmiş alanlar, jsonb veya başka tutarlı bir serileştirme.

Savunulabilir izi zayıflatan diğer desenler:

  • Yalnızca yükü hashleyip bağlamı (zaman damgası, aktör, nesne id'si, eylem) atlamak.
  • Değişiklikleri yalnızca uygulamada yakalayıp veritabanının sonsuza dek eşleştiğini varsaymak.
  • İş verisini yazabilen ve aynı zamanda denetim geçmişini değiştirebilen tek bir veritabanı rolü kullanmak.
  • Zincir içinde prev_hash için NULL'lara izin vermek, açık ve belgelenmiş bir kural olmadan.

Görevlerin ayrılması önemlidir. Aynı rol hem denetim olayları ekleyebiliyorsa hem de bunları değiştirebiliyorsa, değişiklik-tespiti bir kontrol yerine bir vaat haline gelir.

Savunulabilir bir denetim izi için hızlı kontrol listesi

Savunulabilir bir denetim izi değiştirmesi zor ve doğrulaması kolay olmalıdır.

Erişim kontrolüyle başlayın: denetim tablosu pratikte ekleme-yalnız olmalı. Uygulama rolü ekleyebilmeli (ve genellikle okuyabilmeli), ama güncelleme veya silme yapmamalı. Şema değişikliklerini sıkı tutun.

Her satırın bir soruşturmacının soracağı soruları yanıtladığından emin olun: kim yaptı, ne zaman oldu (sunucu tarafı), ne oldu (açık olay adı ve işlem), ne etkilendi (varlık adı ve id) ve nasıl bağlandığı (istek/korelasyon id ve işlem id).

Sonra bütünlük katmanını doğrulayın. Hızlı bir test, bir segmenti replay etmek ve her prev_hash'in önceki satırın hash'iyle eşleştiğini ve her saklanmış hash'in yeniden hesaplanmış olanla eşleştiğini doğrulamaktır.

Operasyonel olarak, doğrulamayı normal bir işmiş gibi ele alın:

  • Planlı bütünlük kontrolleri çalıştırın ve başarılı/başarısız sonuçları ve aralıkları saklayın.
  • Uyuşmazlıklar, boşluklar ve kırık bağlantılar için alarm kurun.
  • Yedekleri saklama süreniz boyunca yeterince uzun tutun ve denetim geçmişinin erken "temizlenmesine" izin vermeyecek şekilde yetkileri kilitleyin.

Örnek: uyum incelemesinde şüpheli bir düzenlemeyi tespit etme

Uyumlu bir yönetici paneli gönderin
Rol tabanlı erişim ve hassas tablolar için ekleme-yalnız olay geçmişiyle güvenli yönetici panelleri oluşturun.
Uygulama Oluştur

Yaygın bir test vakası iade anlaşmazlığıdır. Bir müşteri 250$ iade onayı aldığını iddia ediyor, ama sistem şimdi 25$ gösteriyor. Destek onayın doğru olduğunu ısrar ediyor ve uyum bir cevap istiyor.

Aramayı bir korelasyon id (sipariş id'si, ticket id'si veya refund_request_id) ve bir zaman penceresi kullanarak daraltın. O korelasyon id'si için denetim satırlarını alın ve onay zamanı etrafında sınırlandırın.

Tam olay setini arayın: istek oluşturuldu, iade onaylandı, iade miktarı ayarlandı ve olası sonraki güncellemeler. Değişiklik-tespitli bir tasarımla, ayrıca dizinin sağlam kalıp kalmadığını kontrol ediyorsunuz.

Basit bir soruşturma akışı:

  • Korelasyon id için tüm denetim satırlarını zaman sırasına göre çekin.
  • Her satırın hash'ini kayıtlı alanlarından (incl. prev_hash) yeniden hesaplayın.
  • Hesaplanan hash'leri saklanan hash'lerle karşılaştırın.
  • İlk farklı satırı belirleyin ve sonraki satırların da başarısız olup olmadığını kontrol edin.

Birisi tek bir denetim satırını düzenlediyse (örneğin miktarı 250'den 25'e değiştirmek), o satırın hash'i üretken olmayacaktır. Sonraki satır, önceki hash'i içerdiği için uyumsuzluk genellikle ileri doğru kaskad yapar. Bu kaskad parmak izi gibidir: denetim kaydının sonradan değiştirildiğini gösterir.

Zincirin söyleyebilecekleri: bir düzenleme yapıldığı, zincirin ilk nerede kırıldığı ve etkilenen satırların kapsamı. Zincir tek başına söyleyemeyeceği şeyler: düzenlemeyi kimin yaptığı, eğer üzerine yazıldıysa orijinal değerin ne olduğu veya diğer tabloların da değişip değişmediği.

Sonraki adımlar: güvenli yayılma ve sürdürülebilirlik

Denetim izini diğer güvenlik kontrolleri gibi ele alın. Küçük adımlarla yayına alın, çalıştığını kanıtlayın, sonra genişletin.

İlk olarak itiraz edilirse sizi en çok üzecek eylemlerle başlayın: izin değişiklikleri, ödemeler, iadeler, veri dışa aktarımları ve manuel müdahaleler. Bunlar kapsandıktan sonra temel tasarımı değiştirmeden daha düşük riskli olayları ekleyin.

Denetim olayları için hangi alanların kaydedildiğini, her olay türünün ne anlama geldiğini, hash'in nasıl hesaplandığını ve doğrulamanın nasıl çalıştırılacağını içeren anlaşmayı yazılı hale getirin. Bu dokümantasyonu veritabanı migration'larınızın yanına koyun ve doğrulama prosedürünü tekrarlanabilir tutun.

Geri yükleme tatbikatları önemlidir çünkü soruşturmalar genellikle canlı sistemden değil yedeklerden başlar. Düzenli olarak bir test veritabanına geri yükleme yapın ve zinciri uçtan uca doğrulayın. Geri yükleme sonrası aynı doğrulama sonucunu üretemiyorsanız, değişiklik-tespitiniz savunması zor olacaktır.

Eğer dahili araçlar ve yönetici iş akışları AppMaster (appmaster.io) ile inşa ediyorsanız, sunucu taraflı süreçler aracılığıyla denetim olay yazımını standartlaştırmak olay şemasını ve korelasyon id'leri özellikler genelinde tutarlı kılar; bu da doğrulamayı ve soruşturmaları çok daha basit hale getirir.

Bu sistem için bakım zamanını planlayın. Özellikle ekipler yeni özellikler yayımladığında denetim olaylarını eklemeyi, hash girdilerini güncellemeyi veya doğrulama işleri ve geri yükleme tatbikatlarını çalışır tutmayı unutursa denetim izleri sessizce başarısız olur.

Başlaması kolay
Harika bir şey yaratın

Ücretsiz planla AppMaster ile denemeler yapın.
Hazır olduğunuzda uygun aboneliği seçebilirsiniz.

Başlayın