1. Başlangıç
Güvenlik Sıfırdan serisinin son yazısı. SQL Injection ve Always Encrypted’tan sonra şimdi ‘kim ne zaman ne yaptı’ ve ‘veri değişti mi’ sorularına cevap vereceğiz. SQL Server Audit ile aktiviteyi loglamak ve Ledger ile veriyi tamper-evident şekilde saklamak bu sorunları çözen iki ayrı feature; birlikte düşünüldüğünde compliance dünyasının en güçlü çiftlerinden biri.
Üç workshop yapacağız. Workshop 1’de server-level audit kurup başarılı/başarısız login’leri ve schema değişikliklerini izleyeceğiz. Workshop 2’de hassas bir tablo üzerinde database-level audit ile SELECT/UPDATE/DELETE operasyonlarını loglayacağız. Workshop 3’te Updatable Ledger Table kuracak, kriptografik olarak imzalanmış değişiklik geçmişini sql ledger digest mekanizmasıyla doğrulayacağız.
Yıllar içinde birçok denetim (audit) projesinde gördüm, Audit ve Ledger birlikte düşünülmediğinde her biri tek başına yetersiz kalıyor. Audit aktivite bilgisini verir ama veriyi tutmuyor; Ledger veri bütünlüğünü kanıtlar ama kim ne yaptığını söylemiyor. İkisi birlikte regulatör tarafında ‘separation of duties + tamper evidence’ kombinasyonunu mükemmel kapsıyor.
2. SQL Server Audit Nedir?
SQL Server Audit, instance veya database seviyesinde gerçekleşen aktiviteleri loglayan, Extended Events altyapısı üzerine kurulu bir feature. SQL Server 2008’den beri var, bütün edition’larda mevcut (Enterprise’da bazı ekstra özellikler var).

SQL Server Audit hiyerarşisi: Audit object, Server Audit Spec, Database Audit Spec ve Target.
Üç bileşen var. Audit (top-level): bir instance üzerinde birden fazla audit tanımlayabilirsiniz; her birinin destination’ı (target) ayrı. Server Audit Specification: server-level event’leri (login, logout, schema değişiklikleri, DBCC) bir audit’e bağlar. Database Audit Specification: database-level event’leri (SELECT, UPDATE, DELETE, EXECUTE) bir audit’e bağlar.
Target üç seçenek: file (binary .sqlaudit dosyaları, en performanslı, en güvenli), Windows Security Event Log (regulator tercih ediyor genelde, ama permission ihtiyacı var), Windows Application Event Log (en kolay, en az güvenli, herkes okuyup yazabiliyor uygulama event log’una).
3. Action Groups ve Actions
Audit’te neyi izleyeceğimizi seçmek için action group ve action kavramları var. Action group, motorun önceden tanımladığı bir aktivite paketi, örneğin FAILED_LOGIN_GROUP başarısız login’leri kapsıyor. Action ise daha granüler, bir tablodaki SELECT veya UPDATE gibi spesifik T-SQL operasyonları.
Server-level’de en sık kullanılan action group’lar:
- FAILED_LOGIN_GROUP – başarısız login denemeleri (brute force tespiti).
- SUCCESSFUL_LOGIN_GROUP – başarılı login’ler (özellikle sa veya yüksek-yetkili user’lar için).
- DATABASE_CHANGE_GROUP – CREATE/DROP DATABASE.
- SERVER_OBJECT_CHANGE_GROUP – sunucu seviyesinde nesne değişiklikleri (linked server vb.).
- AUDIT_CHANGE_GROUP – audit konfigürasyonunun kendisinde değişiklik (kim audit’i kapatmaya çalıştı).
- DATABASE_PRINCIPAL_CHANGE_GROUP – user/role oluşturma/silme.
- SCHEMA_OBJECT_CHANGE_GROUP – DDL (CREATE/ALTER/DROP TABLE/PROCEDURE).
Database-level’de en sık kullanılan action’lar:
- SELECT – okuma (sensitive data tablosu için).
- UPDATE / INSERT / DELETE – DML.
- EXECUTE – stored procedure çağrıları.
- RECEIVE – Service Broker queue’lardan okuma.
- REFERENCES – FK reference kontrolü.
Action’lar bir kullanıcı/rol bağlamında ve bir tablo/şema scope’unda tanımlanır. Yani ‘app_user’ın HR.Employees tablosunda yaptığı her SELECT’i logla’ diyebiliyorsunuz.
4. Workshop 1 Server-Level Audit Kurmak
Şimdi pratik. Server-level audit kuracağız: başarılı/başarısız login’ler + DDL değişiklikleri + audit conf değişiklikleri (kim audit’i kapatmaya kalkarsa görelim). File target kullanacağız, file system permission üzerinde DBA hâkim olduğunda en güvenli yol.
USE master; GO-- Audit object (target = file)CREATE SERVER AUDIT srv_audit TO FILE ( FILEPATH = N'C:\Audit\', MAXSIZE = 100 MB, MAX_ROLLOVER_FILES = 10, RESERVE_DISK_SPACE = OFF ) WITH (QUEUE_DELAY = 1000, ON_FAILURE = SHUTDOWN);GO-- ON_FAILURE = SHUTDOWN — audit yazamiyorsa instance kapanir-- Compliance icin uygun, ama HA stratejinizle uyumlu olmali-- Server audit specificationCREATE SERVER AUDIT SPECIFICATION srv_spec FOR SERVER AUDIT srv_audit ADD (FAILED_LOGIN_GROUP), ADD (SUCCESSFUL_LOGIN_GROUP), ADD (AUDIT_CHANGE_GROUP), ADD (DATABASE_PRINCIPAL_CHANGE_GROUP), ADD (SCHEMA_OBJECT_CHANGE_GROUP) WITH (STATE = ON);GO-- Audit'i etkinlestirALTER SERVER AUDIT srv_audit WITH (STATE = ON);GO
Audit logla okuma:
-- Son 100 audit kaydiSELECT TOP (100) event_time, server_principal_name, database_name, object_name, action_id, succeeded, statementFROM sys.fn_get_audit_file('C:\Audit\srv_audit_*.sqlaudit', NULL, NULL)ORDER BY event_time DESC;
Bu audit dosyalarına uygulama user’ı erişemiyor (sadece DBA + audit reader rolünde olanlar). Tamper-evident değil ama tamper-resistant, zira audit log’unu silmek SCHEMA_OBJECT_CHANGE_GROUP veya AUDIT_CHANGE_GROUP’ta zaten loglanıyor. Yani bir saldırgan log’u temizlemeye kalkarsa, silme işleminin kendisi loglanır.
5. Workshop 2 Database-Level Audit (Hassas Tablo)
HR.Employees gibi hassas bir tablonun her SELECT/UPDATE/DELETE’ini loglayalım. GDPR, HIPAA gibi düzenlemelerde ‘personal data tablosuna her erişim audit edilmeli’ tipik bir gereksinim.
USE HRDB; GO-- Database audit specificationCREATE DATABASE AUDIT SPECIFICATION db_spec_hr FOR SERVER AUDIT srv_audit ADD (SELECT, UPDATE, DELETE, INSERT ON dbo.Employees BY public), ADD (EXECUTE ON dbo.spGetEmployee BY public) WITH (STATE = ON);GO-- TestSELECT * FROM dbo.Employees WHERE EmployeeID = 1;UPDATE dbo.Employees SET Salary = 9000 WHERE EmployeeID = 1;DELETE FROM dbo.Employees WHERE EmployeeID = 9999;-- Auditten okuSELECT TOP (10) event_time, server_principal_name, object_name, action_id, statementFROM sys.fn_get_audit_file('C:\Audit\srv_audit_*.sqlaudit', NULL, NULL)WHERE database_name = 'HRDB'ORDER BY event_time DESC;
BY public diyerek ‘kim olursa olsun bu tabloya erişen herkesi logla’ diyoruz. BY <specific_user> diyerek belirli bir hesabı izleyebilir veya BY <role> ile rol bazlı filtre koyabilirsiniz.
Bir önemli bilgi: audit bazı kolonları parametre olarak değil, statement’ın tamamı olarak loglar. Yani ‘WHERE SSN = ”123-45-6789”’ gibi sorgularınızda hassas veri audit log’una düşer. PII’yi audit log’unda tutmamak istiyorsanız parametre kullanın (parameterized statement parametre değerleri ayrı kolonda encrypted) veya filter predicate ile bu kolonu maskeleyin.
6. Ledger Nedir? Tamper-Evident Veri Saklama
Ledger, SQL Server 2022’den beri var ama tam olgunluğa SQL Server 2025’te ulaştı. Mantığı şu: tablodaki her değişiklik kriptografik olarak imzalanır, blockchain benzeri bir hash chain’e eklenir. Database digest dediğimiz şey bu chain’in son hash’i — periyodik olarak storage’a (Azure Blob, immutable) yazılır. Sonradan kim olursa olsun veriyi değiştirirse hash chain bozulur, doğrulamada anomali gözükür.

Ledger mimarisi: kullanıcı tablosu, history tablosu, ledger view, database digest, immutable storage.
İki ledger tablo tipi var:
- Updatable Ledger Table: standart tablo gibi UPDATE/DELETE yapabilirsin, ama her değişiklik history table’da kalıcı tutulur. Eski versiyonu görmek için ledger view’a bakarsın.
- Append-Only Ledger Table: sadece INSERT yapabilirsin. UPDATE/DELETE engine tarafından reddedilir. Mali işlemler, audit log’lar için ideal.
Üçüncü kavram: Database Ledger. Tüm ledger tabloların hash’lerini birleştiren bir block chain. Bu chain’in son hash’i database digest. Digest’i Azure Storage’a (WORM mode) yazıp, sonradan kanıt olarak kullanabilirsiniz.
7. Workshop 3 Updatable Ledger Table Kurmak ve Doğrulamak
Bir Updatable Ledger tablo kuralım, üzerinde işlem yapalım, ledger view üzerinden değişiklik geçmişini izleyelim, sonunda digest doğrulaması yapalım.
-- Database genelinde ledger ozelliklerini kontrol etSELECT name, is_ledger_on, is_ledger_databaseFROM sys.databasesWHERE name = DB_NAME();-- Updatable Ledger TableCREATE TABLE dbo.Account_Balances( AccountID INT PRIMARY KEY, Balance MONEY NOT NULL)WITH ( SYSTEM_VERSIONING = ON, LEDGER = ON);
Bu komut otomatik olarak history table ve ledger view oluşturuyor. Şu sorguyla görebilirsiniz:
SELECT OBJECT_NAME(t.object_id) AS table_name, OBJECT_NAME(t.history_table_id) AS history_table, OBJECT_NAME(t.ledger_view_id) AS ledger_view, t.ledger_type_descFROM sys.tables tWHERE t.name = 'Account_Balances';-- Bazi degisiklikler yapalimINSERT INTO dbo.Account_Balances VALUES (1001, 1000.00), (1002, 5000.00);UPDATE dbo.Account_Balances SET Balance = 1500.00 WHERE AccountID = 1001;UPDATE dbo.Account_Balances SET Balance = 4500.00 WHERE AccountID = 1002;DELETE FROM dbo.Account_Balances WHERE AccountID = 1002;-- Ledger view ile tum gecmisi gorSELECT *, lv.ledger_transaction_id, lv.ledger_sequence_number, lv.ledger_operation_type_descFROM dbo.Account_Balances_Ledger lvORDER BY lv.ledger_transaction_id;
Ledger view, current row + history’den birleştirilmiş kayıt veriyor. Her kaydın ledger_operation_type_desc kolonu UPDATE/INSERT/DELETE değerlerinden birini içeriyor. Hangi transaction’da hangi sıra numarasıyla yapıldığı da ayrı kolonlarda.
-- Ledger transaction'lari, kim yaptigi vsSELECT lt.transaction_id, lt.commit_time, lt.principal_name, lt.[block_id], lt.previous_block_idFROM sys.database_ledger_transactions ltORDER BY lt.commit_time DESC;-- Database digest yarat ve dogrulaEXEC sys.sp_generate_database_ledger_digest;-- JSON formatinda digest doner; bunu Azure Storage'a yazip sakliyacaksin-- Daha sonra dogrulamaEXEC sys.sp_verify_database_ledger @digest = N'<previously_saved_digest_json>';
Bu doğrulama mekanizması, herhangi biri tabloyu, history tabloyu veya metadata’yı manipüle etmeye kalkarsa hash chain’in bozulduğunu gösterir. Production’da digest’i otomatik olarak Azure Blob Storage’a (immutable mode) yazmak için sys.database_ledger_digest_locations konfigürasyonu var:
-- Digest'i Azure Blob'a yazacak path tanimlaALTER DATABASE [<your_db>] SET LEDGER_DIGEST_STORAGE_ENDPOINT = N'https://<storage>.blob.core.windows.net/<container>';-- Engine her ~30 dakikada otomatik digest yazar-- Verify icin de sys.sp_verify_database_ledger_from_digest_storage kullan
8. Append-Only Ledger Mali Kayıtlar İçin Mükemmel
Append-Only Ledger Table, sadece INSERT’i destekleyen bir ledger varyantı. UPDATE veya DELETE engine tarafından engellenir; uygulama kodunda hata olarak döner. Mali işlem kayıtları, audit log’lar, telemetri zaman serileri için ideal — değişmesi ‘imkansız’ tutmalı bir şey için.
CREATE TABLE dbo.TransactionLog( LogID BIGINT IDENTITY PRIMARY KEY, AccountFrom INT NOT NULL, AccountTo INT NOT NULL, Amount MONEY NOT NULL, TxTime DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(), Memo NVARCHAR(500))WITH ( LEDGER = ON (LEDGER_VIEW = dbo.TransactionLog_Ledger, APPEND_ONLY = ON));INSERT INTO dbo.TransactionLog (AccountFrom, AccountTo, Amount, Memo)VALUES (1001, 1002, 500.00, N'Aylik kira');-- UPDATE / DELETE deneyelim — engine reddetmeliUPDATE dbo.TransactionLog SET Amount = 0 WHERE LogID = 1;-- Msg 13513: Updates aren't allowed for the table 'dbo.TransactionLog'.DELETE FROM dbo.TransactionLog WHERE LogID = 1;-- Msg 13513: Deletes aren't allowed for the table 'dbo.TransactionLog'.
Bu, sysadmin DBA bile yapamadığı bir kısıt. Compliance auditor’unun duymak istediği kelimeler: ‘admin yetkisinde olsanız bile bu tablodaki kayıtları değiştiremezsiniz, kanıtlamaları kriptografik chain’e dahil.’
Append-Only ledger ile beraber digest doğrulaması yapıldığında, herhangi bir kaydın sonradan eklenmesi de tespit edilebilir — çünkü sıra numarası ve önceki block hash’i de doğrulamaya girer. Yani ‘satır eklendi mi’ veya ‘satır geriye dönük tarih atıldı mı’ soruları da cevaplanır.
9. Audit + Ledger Birlikte Kullanmak
İki feature farklı sorulara cevap veriyor:
- Audit: ‘Kim, ne zaman, ne yaptı?’ Aktivite log’u. Kim okudu, kim sildi, kim CREATE TABLE çekti.
- Ledger: ‘Bu satır ne zamandan beri böyle? Birisi geçmişe dönük değiştirmiş olabilir mi?’ Veri bütünlüğü kanıtı.
Compliance dünyasında ikisini birlikte kullanmak best practice. Audit, tipik ‘who did what’ sorularına cevap verir; ledger ise audit log’unun kendisinin manipüle edilmemiş olduğunu kanıtlar. Yani audit log’u ledger tablo olarak tutarsanız, sysadmin DBA bile log’a dokunamaz.

Audit + Ledger birlikte: kim ne yaptı (audit) + neyin değişip değişmediği (ledger) — compliance ikilisi.
-- Audit kayitlarini bir append-only ledger tabloda toplama ornegi-- Once tabloyu kurCREATE TABLE dbo.AuditTrail( EntryID BIGINT IDENTITY PRIMARY KEY, EventTime DATETIME2(7) NOT NULL, PrincipalName NVARCHAR(200), DatabaseName NVARCHAR(200), ObjectName NVARCHAR(200), ActionID VARCHAR(10), Statement NVARCHAR(MAX), Succeeded BIT)WITH (LEDGER = ON (APPEND_ONLY = ON));-- Otomatik populate icin scheduled job ile sys.fn_get_audit_file'i bu-- tabloya basabiliriz. Append-only oldugu icin sonradan kimse silemez.-- Digest periyodik olarak Azure Blob'a yaziliyor
Bu pattern özellikle finans, sağlık, public sector senaryolarında regulator’ın çok sevdiği bir yaklaşım: audit log’u tamper-evident bir ledger tabloda saklanıyor, dolayısıyla ‘sysadmin’in audit log’u silebileceği’ endişesi otomatik olarak ortadan kalkıyor.
10. İzleme ve Bakım
Audit ve ledger production’da kurulduktan sonra düzenli izlemeye ihtiyaç var. Aşağıdaki sorgular günlük dashboard’da olabilecek tipte.
-- 1. Audit konfigurasyon ozetSELECT sa.name AS audit_name, sa.is_state_enabled, sa.queue_delay, sa.on_failure_desc, sa.audit_file_path, sa.max_file_size_mbFROM sys.server_audits sa;-- 2. Aktif server audit specsSELECT sas.name AS spec_name, sa.name AS audit_name, sas.is_state_enabledFROM sys.server_audit_specifications sasJOIN sys.server_audits sa ON sas.audit_guid = sa.audit_guid;-- 3. Son 100 failed loginSELECT TOP (100) event_time, server_principal_name, client_ip, statementFROM sys.fn_get_audit_file('C:\Audit\srv_audit_*.sqlaudit', NULL, NULL)WHERE action_id = 'LGIF' -- Login FailedORDER BY event_time DESC;
Audit dosyaları zamanla şişer. MAX_ROLLOVER_FILES + MAXSIZE ayarlarıyla rotation otomatik. Compliance gereği N gün/yıl saklamanız gerekiyorsa, rolling .sqlaudit dosyalarını ayrı bir archive sistemine (Azure Blob, S3) periyodik olarak kopyalamayı kuracaksınız. Audit ON_FAILURE = SHUTDOWN ile kuruldu — yani disk dolarsa instance kapanır. Production’da buna karşı kapasiteyi izlemek şart.
-- Ledger digest dogrulamasi (script ile periyodik yap)DECLARE @stored_digest NVARCHAR(MAX) = N'<azure-blob-storage-endpointten-cekilen-onceki-digest>';EXEC sys.sp_verify_database_ledger @digest = @stored_digest;-- Sonuc 0 = OK, exception = tamper detected
sp_verify_database_ledger normalde sessizce başarılı döner; bir tamper detected olursa exception raise eder. Production’da bu doğrulamayı bir scheduled job + alerting ile çalıştırın; saatte bir/günde bir, compliance gereksinimine göre.
11. Sınırlar ve Pratik Uyarılar
- Audit dosyaları SQL Server service account tarafından yazılır — file path’e bu account’un write yetkisi olmalı.
- Audit’i bir reader rolü olmadan kimse okumamalı — server-level rolü ‘AUDIT_VIEW’ veya server-permission CONTROL SERVER var olanlar.
- Audit log’u silmek için elevated yetki gerek; ama AUDIT_CHANGE_GROUP açıksa silmenin kendisi loglanır.
- Audit performans yiyor — server-level, ON_FAILURE=SHUTDOWN ile çok agresif kurarsanız yüksek-yük instance’larda CPU etkisi olabilir.
- Database audit’i tempdb için tanımlanamaz; tempdb action’ları izlenmez.
- Ledger tablo OPENJSON, OUTPUT clause, MERGE gibi bazı operasyonlarda kısıtlı.
- Ledger LEDGER = OFF demek mümkün değil — bir kez açtın mı geri alamazsın (tamper-evident’in mantığı bu).
- Updatable Ledger sistem-versioned + ledger; system_versioning OFF deneyemezsin.
- Append-Only Ledger için INSERT performansı standart tabloya yakın, ama UPDATE/DELETE deneyince hata dönüyor — uygulama logic’i buna hazır olmalı.
- Ledger digest storage path’e SQL Server service account read+write erişmeli.
12. Bölüm Sonu
Audit ve Ledger, compliance dünyasının iki ayrı sütunu. Audit ile ‘kim ne yaptı’yı, Ledger ile ‘veriye dokunulmuş mu’yu cevaplıyoruz. İkisi birlikte regulatör tarafına ‘separation of duties + tamper evidence’ kombinasyonunu vermek için yeterli.
Workshop 1-3’ü kendi laboratuvarınızda mutlaka çalıştırın. Özellikle Workshop 3’teki ledger digest doğrulaması, herhangi bir auditörle konuşurken net bir mesele oluyor — ‘biz veri bütünlüğünü kriptografik olarak kanıtlayabiliyoruz’ demek için somut bir araç.
Sonraki ve son yazıda Cloud Kararı’na geçeceğiz. Azure SQL MI vs On-Premise SQL Server karşılaştırması, update policy’ler (SQL 2022 / 2025 / Always-up-to-date), Fabric mirroring opsiyonu, maliyet ve feature parity. 12 haftalık serinin kapanışı bir karar matrisi olacak.
Audit ve Ledger üzerine sorularınızı yorumlara yazın. Özellikle production’da audit log archival stratejileri, ledger digest verification automation, compliance auditor’larla iletişim, bu deneyimleri bir araya getirirsem ikinci bölümde vaka çalışması yapabilirim.
Yazar hakkında
Yavuz Filizlibay — Database Solution Architect
SQL Server ekosisteminde uzun yıllardır performans, güvenlik, yüksek erişilebilirlik üzerine çalışıyorum. SQL Server Administration, Querying, Performans ve Security eğitimleri ile danışmanlık hizmeti veriyorum. Yeni makalelerden haberdar olmak için LinkedIn’de bağlanabilir, eğitim veya danışmanlık için iletişime geçebilirsiniz.
