Tüm yazılar
Strateji 7 dk okuma

Saklama Süresi Politikasını Koda Dökmek: Otomatik İmha ve İmha Kanıtı

KVKK saklama süresi politikasını çalışan koda dönüştürün: YAML kurallar, kuru çalıştırma, otomatik imha ve hash zincirli imha kanıtı için Python kodu.

#process #documentation #automation

Saklama süresi politikası çoğu kurumda bir Word belgesi olarak yaşar: kategori başına yıl sayısı yazılır, imzalanır, klasöre kaldırılır. Otomatik imha ise aynı belgeyi düzenli çalışan bir işe dönüştürmek demektir; süresi dolan kaydı bulan, önce raporlayan, sonra silen veya anonimleştiren ve yaptığını kanıtlayan bir betik.

Belge ile çalışan iş arasındaki fark denetimde ortaya çıkar. Politikada "başvuru formları iki yıl sonra silinir" yazması, veritabanında üç yıllık bir başvurunun durmadığını göstermez. Çalışan iş ise her koşumda hangi tabloda kaç kaydın süresinin dolduğunu ve bunlara ne yapıldığını yazılı bırakır.

Yazının geri kalanında dört adımı sırayla kuruyorum: veri envanteri, kategori başına saklama kuralı, kuru çalıştırmayla başlayan imha betiği ve değiştirilemez kanıt kaydı. Kod örnekleri Python 3.8 ve üzeriyle, yalnız standart kütüphane ve PyYAML ile çalışır.

Veri envanteri olmadan otomatik imha neden kurulamaz?

Veri envanteri, hangi kişisel veri kategorisinin hangi tabloda veya dosyada, hangi tarih sütunuyla ve hangi hukuki dayanakla tutulduğunu listeleyen tablodur. Otomatik imha betiği yalnız envanterde yazan yerlere bakar; envanterde olmayan bir kopya, süresi dolsa da yerinde kalır. İlk adım o yüzden kod değil, eksiksiz envanterdir.

Envanterin her satırında en az şu alanlar bulunmalıdır:

  • Kategori: başvuru formu, sipariş, personel dosyası, destek talebi gibi iş dilinde bir ad.
  • Konum: tablo adı, dosya yolu veya üçüncü taraf sistem; aynı kategori birden fazla konumda olabilir.
  • Tarih sütunu: sürenin neyden itibaren sayıldığı; oluşturma tarihi ile ilişkinin bitiş tarihi farklı sonuç verir.
  • Saklama süresi ve dayanağı: gün cinsinden süre ve o süreyi gerektiren mevzuat ya da iş gerekçesi.
  • İmha yöntemi ve sorumlu birim: silme, yok etme veya anonimleştirme; kararı kimin verdiği.

Envanter hazırlanırken en sık atlanan yerler asıl veritabanının dışındaki kopyalardır. Tipik örnekler şunlardır:

  • Yedekler: bugün silinen kayıt dün gece alınan yedekte durur; yedeğin kendi saklama süresi ayrı bir kural ister.
  • Log dosyaları: web sunucusu ve uygulama logları form içeriği, e-posta ve IP adresi barındırabilir.
  • Dışa aktarımlar: Excel raporu, e-posta eki, paylaşılan klasördeki CSV.
  • Test ortamları: üretimden kopyalanmış veriyle çalışan geliştirme veritabanları.

Log dosyalarında kalan kişisel veriyi bulmak için ayrı bir tarama gerekir; log analizinde düzenli ifade kalıpları yazısındaki desenler, e-posta, telefon ve kimlik numarası biçimindeki alanları log satırlarından çıkarmak için kullanılabilir. Bulunan alanlar ya loga hiç yazılmamalı ya da log rotasyonu envantere kendi saklama süresiyle girmelidir.

Envanterdeki yöntem sütunu üç değerden birini alır ve üçü aynı şey değildir:

YöntemNe yaparNe zaman uygunTipik hata
SilmeVeriyi ilgili kullanıcılar için erişilemez ve tekrar kullanılamaz hâle getirir (DELETE, dosya kaldırma).İşleme amacı bitti, kayıt yapısına istatistik için ihtiyaç yok.DELETE sonrası disk sayfalarında ve yedeklerde içeriğin kalması.
Yok etmeVeriyi hiç kimse tarafından geri getirilemez hâle getirir (güvenli üzerine yazma, ortam imhası).Fiziksel ortam, arşiv diskleri, elden çıkarılan cihazlar.Yalnız biçimlendirme yapıp yok etme saymak.
Anonim hâle getirmeVeriyi başka verilerle eşleştirilse bile belirli bir kişiyle ilişkilendirilemez hâle getirir.İstatistik, raporlama, kayıt yapısı korunacaksa.Takma adlandırmayı anonimleştirme sanmak.

Takma adlandırma ile anonimleştirme karışıklığı en pahalı hatadır. E-postayı SHA-256 ile hash'leyip saklamak kaydı anonim yapmaz; aynı hash başka bir tabloda aynı kişiye işaret eder, tuz yoksa yaygın adresler sözlük saldırısıyla geri çözülür. Anonimleştirme iddiasının testi basittir: elinizdeki diğer verilerle birleştirince kişiye dönüyorsa anonim değildir.

Saklama kurallarını YAML'dan okuyan imha betiği nasıl kurulur?

İmha betiği, saklama kurallarını koddan ayrı bir YAML dosyasında tutar, her kural için süresi dolan kayıt kimliklerini SQL ile bulur ve varsayılan modda yalnız sayı raporlar. Silme veya anonimleştirme yalnız --uygula bayrağıyla çalışır. Kural dosyasını koddan ayırmak, politika değiştiğinde yalnız YAML'ın değişmesini ve incelenmesini sağlar.

Örnek kural dosyası betiğin başındaki yorum satırlarında duruyor; tek bağımlılık pip install pyyaml ile kurulur:

import argparse
import re
import sqlite3
from datetime import datetime, timedelta

import yaml

# kurallar.yaml:
# kurallar:
#   - tablo: basvuru
#     tarih_sutunu: olusturma_tarihi
#     saklama_gun: 730
#     yontem: sil
#   - tablo: siparis
#     tarih_sutunu: tamamlanma_tarihi
#     saklama_gun: 3650
#     yontem: anonimlestir
#     anonim_sutunlar: [ad_soyad, eposta, telefon]

TANIMLAYICI = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')


def tanimlayici(ad):
    if not TANIMLAYICI.match(ad):
        raise ValueError('gecersiz tablo/sutun adi: ' + repr(ad))
    return ad


def kurallari_yukle(yol):
    with open(yol, encoding='utf-8') as f:
        return yaml.safe_load(f)['kurallar']


def suresi_dolanlar(db, kural, simdi):
    esik = (simdi - timedelta(days=int(kural['saklama_gun']))).isoformat()
    sql = 'SELECT id FROM {} WHERE {} < ?'.format(
        tanimlayici(kural['tablo']), tanimlayici(kural['tarih_sutunu']))
    return [satir[0] for satir in db.execute(sql, (esik,))]


def uygula(db, kural, idler):
    if not idler:
        return
    tablo = tanimlayici(kural['tablo'])
    yer = ','.join('?' * len(idler))
    if kural['yontem'] == 'sil':
        db.execute('DELETE FROM {} WHERE id IN ({})'.format(tablo, yer), idler)
    elif kural['yontem'] == 'anonimlestir':
        atama = ', '.join(tanimlayici(s) + ' = NULL' for s in kural['anonim_sutunlar'])
        db.execute('UPDATE {} SET {} WHERE id IN ({})'.format(tablo, atama, yer), idler)
    else:
        raise ValueError('bilinmeyen yontem: ' + str(kural['yontem']))


def main():
    p = argparse.ArgumentParser(description='KVKK saklama suresi imha betigi')
    p.add_argument('--kurallar', default='kurallar.yaml')
    p.add_argument('--db', default='veri.db')
    p.add_argument('--uygula', action='store_true',
                   help='verilmezse yalnizca rapor uretir (kuru calistirma)')
    a = p.parse_args()

    simdi = datetime.now()
    db = sqlite3.connect(a.db)
    try:
        db.execute('PRAGMA secure_delete = ON')
        for kural in kurallari_yukle(a.kurallar):
            idler = suresi_dolanlar(db, kural, simdi)
            print('{:<16} {:<14} suresi dolan: {}'.format(
                kural['tablo'], kural['yontem'], len(idler)))
            if a.uygula:
                uygula(db, kural, idler)
        if a.uygula:
            db.commit()
            print('imha uygulandi')
        else:
            print('kuru calistirma: hicbir kayit degistirilmedi')
    finally:
        db.close()


if __name__ == '__main__':
    main()

Betiğin üç tasarım kararı dikkat ister. Tablo ve sütun adları YAML'dan geldiği için SQL'e parametre olarak bağlanamaz; tanimlayici() fonksiyonu yalnız harf, rakam ve alt çizgiye izin vererek kural dosyasının SQL enjeksiyonu kanalına dönüşmesini engeller. Kural dosyası yine de kod gibi incelenmeli ve sürüm kontrolünde tutulmalıdır.

Kuru çalıştırma çıktısı ilk haftalarda politika ile envanteri karşılaştırmanın aracıdır. Bir tabloda beklenmedik kadar çok kayıt "süresi dolan" görünüyorsa sorun çoğunlukla tarih sütunu seçimindedir: sözleşme bitiş tarihi sayılması gerekirken oluşturma tarihi kurala girmiştir. Rakamlar açıklanabilir olana kadar --uygula verilmez; tarih sütununun ISO biçiminde metin tuttuğu da ayrıca doğrulanmalıdır.

Silme sonrası veritabanı dosyasında iz kalabilir. SQLite'ta silinen sayfalar yeniden kullanılana kadar diskte durur; PRAGMA secure_delete = ON silinen içeriğin üzerine sıfır yazdırır, VACUUM dosyayı yeniden kurar. Diğer veritabanlarında eşdeğer davranış ürün belgelerinden doğrulanmalı, "DELETE çalıştı" tek başına yok etme sayılmamalıdır.

İmha kanıtı hash zinciriyle nasıl değiştirilemez tutulur?

İmha kanıtı, her imha koşumu için tarih, kategori, tablo, yöntem ve kayıt sayısını içeren bir JSON satırıdır; her satır bir önceki satırın SHA-256 hash'ini taşır. Ortadaki bir satır değiştirilir veya silinirse sonraki tüm hash'ler tutmaz ve doğrulama fonksiyonu kopuk noktayı gösterir. Kanıt kaydı kişisel verinin kendisini içermez.

Kimliği bir biçimde tutma ihtiyacı genellikle vardır, çünkü "şu kayıt gerçekten imha edildi mi" sorusu sorulur. Çözüm, kayıt kimliğinin gizli anahtarlı HMAC değerini yazmaktır. Anahtar sır kasasında durur; anahtarı bilen kişi bir kimliğin zincirde olup olmadığını doğrulayabilir, kanıt dosyasını ele geçiren kişi ise kimliklere geri dönemez.

import hashlib
import hmac
import json
import os
from datetime import datetime, timezone

KANIT_DOSYASI = 'imha-kaniti.jsonl'
BASLANGIC = '0' * 64
ANAHTAR = os.environ['IMHA_HMAC_ANAHTARI'].encode()  # sir kasasindan gelir, koda gomulmez


def kimlik_hashle(kayit_id):
    return hmac.new(ANAHTAR, str(kayit_id).encode(), hashlib.sha256).hexdigest()


def satir_hashle(kayit):
    govde = json.dumps(kayit, sort_keys=True, ensure_ascii=False, separators=(',', ':'))
    return hashlib.sha256(govde.encode('utf-8')).hexdigest()


def son_hash():
    if not os.path.exists(KANIT_DOSYASI):
        return BASLANGIC
    with open(KANIT_DOSYASI, encoding='utf-8') as f:
        satirlar = [s for s in f.read().splitlines() if s.strip()]
    return json.loads(satirlar[-1])['hash'] if satirlar else BASLANGIC


def kanit_yaz(kategori, tablo, yontem, idler):
    kayit = {
        'zaman': datetime.now(timezone.utc).isoformat(),
        'kategori': kategori,
        'tablo': tablo,
        'yontem': yontem,
        'sayi': len(idler),
        'kimlik_hashleri': sorted(kimlik_hashle(i) for i in idler),
        'onceki_hash': son_hash(),
    }
    kayit['hash'] = satir_hashle(kayit)
    with open(KANIT_DOSYASI, 'a', encoding='utf-8') as f:
        f.write(json.dumps(kayit, sort_keys=True, ensure_ascii=False) + '\n')
    return kayit['hash']


def zinciri_dogrula():
    beklenen = BASLANGIC
    with open(KANIT_DOSYASI, encoding='utf-8') as f:
        for no, satir in enumerate(f, 1):
            if not satir.strip():
                continue
            kayit = json.loads(satir)
            hash_degeri = kayit.pop('hash')
            if kayit['onceki_hash'] != beklenen:
                return False, 'satir {}: zincir kopuk'.format(no)
            if satir_hashle(kayit) != hash_degeri:
                return False, 'satir {}: icerik degistirilmis'.format(no)
            beklenen = hash_degeri
    return True, 'zincir saglam'


if __name__ == '__main__':
    kanit_yaz('basvuru_formu', 'basvuru', 'sil', [101, 102, 103])
    print(zinciri_dogrula())

Doğrulama fonksiyonu iki şeyi ayrı ayrı kontrol eder: her satırın onceki_hash alanı gerçekten bir önceki satırın hash'i mi (zincir bütünlüğü) ve satırın kendi hash'i içeriğiyle tutuyor mu (içerik bütünlüğü). Doğrulamayı imha betiğinden bağımsız bir zamanlanmış görev olarak koşturmak ve sonucu izleme kanalına yazmak, sessiz bozulmayı erken yakalar.

Hash zinciri dosyanın kendisini korumaz; dosya bütünüyle silinirse zincir de gider. Kanıt dosyasının yalnız ekleme yetkisi olan ayrı bir kullanıcıya ait olması, düzenli olarak imha sisteminin dışına kopyalanması ve son hash değerinin ayrı bir yere not edilmesi gerekir. Yönetmelik imha kayıtlarının en az üç yıl saklanmasını istediği için kanıt dosyasının kendi saklama süresi de envantere girer.

Hukuki ve etik çerçeve otomasyondan ne bekler?

6698 sayılı KVKK'nın 7. maddesi, işlenmesini gerektiren sebepler ortadan kalkan kişisel verinin silinmesini, yok edilmesini veya anonim hâle getirilmesini zorunlu kılar. Kişisel Verilerin Silinmesi, Yok Edilmesi veya Anonim Hale Getirilmesi Hakkında Yönetmelik (28.10.2017 tarihli Resmî Gazete) ise saklama ve imha politikası hazırlanmasını, periyodik imhanın en fazla altı aylık aralıklarla yapılmasını ve imha işlemlerinin kayıt altına alınıp en az üç yıl saklanmasını düzenler. Bu yazı hukuki danışmanlık değildir; politikayı hukukçunuzla birlikte belirleyin.

Otomasyon bakış açısıyla üç yükümlülük doğrudan tasarım gereksinimine çevrilir:

  • Politika ile kural dosyası aynı süreyi söylemeli: ikisini aynı değişiklikte güncelleyen bir süreç kurulur; aksi hâlde belge imzalıdır ama kod başka süreyle siler.
  • Altı aylık üst sınır kanıt ister: zamanlanmış görevin var olması yetmez, kanıt zincirindeki tarihli satır betiğin gerçekten koştuğunu gösterir.
  • Kayıt yükümlülüğü ayrı yetki ister: kanıt dosyası silme betiğinin kullanıcısından farklı bir hesapla korunur ki imha yetkisi kanıtı da silemesin.

Etik boyut hukuki asgariden ileri gider. Saklama süresini "nasıl olsa gerekir" diye uzun tutmak, veri sahibine karşı taşınan riski büyütür: sızıntı olduğunda etkilenen kişi sayısını, süresi dolmuş ama silinmemiş kayıtlar belirler. Aynı ilke ödeme verisinde daha sert işler; Türkiye'deki ödeme sağlayıcıları ve uyumluluk araştırması yazısı, hangi verinin sağlayıcıda kalıp hangisinin sizin sistemlerinizde hiç tutulmaması gerektiğini ele alır.

Sonuç: saklama politikası ne zaman çalışıyor sayılır?

Saklama politikası, süresi dolan kaydı bulan betik düzenli koştuğunda, yaptığını kanıt zincirine yazdığında ve zincir bağımsız bir doğrulamadan geçtiğinde çalışıyor sayılır. Belgede yazan süre ile YAML'daki süre aynıysa ve her koşumun çıktısı kanıt dosyasında tarihli bir satır olarak duruyorsa politika kâğıttan çıkmıştır.

Kurulum sırası önemlidir: önce envanter, sonra kuru çalıştırma, ancak rakamlar açıklanabilir olduğunda --uygula, en son kanıt zinciri ve bağımsız doğrulama. Sırayı tersine çevirmek, silinmemesi gereken veriyi silen ya da sildiğini kanıtlayamayan bir sistem üretir.

Mevcut veritabanınız ve dosya sistemleriniz için envanter çıkarma, kural dosyasını politikanızla eşleştirme ve imha hattını kanıt kaydıyla birlikte kurma konusunda destek isterseniz iletişim bölümünden ulaşabilirsiniz.