K8s'te .NET Security Telemetry: Gerçek Ziyaretçi Tespiti

0 yorum 519 görüntülenme

Siber Güvenlik kubernetes dotnet Security Telemetry ThreatScore MaxMind GeoIP SQL Server Prometheus KVKK

23 dk okuma 4514 kelime

TL;DR

Analytics dashboard'un günde 100 ziyaretçi gösteriyorsa gerçek insan sayın yaklaşık 33'tür. Geri kalan %67 datacenter, VPN ve null ASN. Canlı prod sistemde PageHits + AttackProbes + IsSuspectNetwork üçlüsüyle bot/insan ayrımını yapıyorum, MERGE+OUTPUT sliding-window burst counter'la otomatik block ediyorum, KVKK uyumlu PII'siz tasarımla 90 gün saklıyorum. Tam SQL şema, kod ve gerçek 24 saatlik prod ölçümleri aşağıda.

Yazar notu: Bu yazıdaki SQL şemaları, kod ve metrikler bilalkose.com.tr canlı sistemden alındı. PageHits + AttackProbes tabloları 2026-05-10'da prod'a girdi; 24 saat içinde %33/%67 oranı, 157 attack probe, 246 backfill telemetry kaydı ölçüldü. Sub-Faz A++/A+++ tasarım kararları (datacenter ASN registry, IsSuspectNetwork flag, AttackBurstCounter MERGE+OUTPUT pattern) doğrudan üretim deneyiminden.


İçindekiler

  1. Security Telemetry Nedir, Neden Origin-Side Lazım?
  2. PageHits Middleware Mimarisi
  3. AttackProbes + ThreatScore Formülü
  4. AttackBurstCounter: MERGE+OUTPUT Sliding Window
  5. IsSuspectNetwork Flag: Datacenter / VPN / Null ASN
  6. MaxMind GeoIP Singleton Reader Pattern
  7. Log → AttackProbes Backfill ETL
  8. Origin Engelleme Döngüsü
  9. Real Visitor Admin Dashboard
  10. Prometheus Metric Export
  11. Privacy + KVKK: PII'siz Tasarım
  12. Sıkça Sorulan Sorular
  13. İlgili Yazılar

Security Telemetry Nedir, Neden Origin-Side Lazım?

Cloudflare gibi bir CDN edge'i kullanıyorsan saldırıların büyük çoğunluğunu zaten edge'de durduruyorsundur — Pillar 1 yazısında bunu detaylıca ele aldım. Ama edge stateless: pattern match, IP lookup, UA contains. Burst behavior, session geçmişi, ThreatScore birikimi gibi stateful kararlar origin'de verilmeli. Origin-side security telemetry buranın merkezi.

Edge yetmediği yerler

Edge filter'ı şu üç senaryoda yetersiz kalır:

  1. Low-and-slow attacker: Saatte 1 request, ama saatte farklı bir sensitive path. Edge "tek request" görüyor, pattern eşleştirmiyor; origin "aynı IP'den 8 saatte 8 farklı admin path" görüyor.
  2. Distributed credential stuffing: Farklı IP'lerden aynı user'a brute force. Edge IP rate limit'i aşmaz, origin user-bound lockout uygular.
  3. Behavioral anomaly: Authenticated kullanıcı normal davranıştan saparsa (gece 03:00'te 1000 page view), edge bilmez — origin user session context'iyle yakalar.

Analytics dashboard yanıltır

Daha temel bir problem: standart analytics tool'ları (GA, Plausible, CF Analytics) bot/datacenter trafiğini "gerçek visitor" olarak sayıyor. Sub-Faz A telemetry'mde 24 saat içinde 126 unique IP görünüyordu, ama detaylı analizde gerçek insan sayısı 25-30 civarıydı. Geriye kalan %75-80 datacenter scraper, VPN, null ASN trafiği — özellikle Mart 2026 sonrası AI crawler patlamasıyla artan oran.

Bu fark sadece "egom kırıldı" meselesi değil; iş kararlarını yanlış yere yönlendiriyor: hangi içerik gerçekten popüler, conversion rate ne, hangi locale yatırım yapmaya değer? Doğru karar için doğru sayıma ihtiyaç var.

KVKK m.5/2-(f) çerçevesinde meşru menfaat

Türkiye'de Raw IP saklamak, KVKK madde 5/2-(f) "veri sorumlusunun meşru menfaatleri için veri işlenmesinin zorunlu olması" hükmüne dayanıyor — sistem güvenliği meşru menfaat. AttackProbes tablosunda onay aranmıyor (saldırgan rıza istemiyor); PageHits için ise opsiyonel ConsentGranted flag'i var. 90 gün retention sonrası otomatik DELETE.

PageHits son 7 gün: %33 gerçek ziyaretçi vs %67 şüpheli (DC/VPN/null ASN) — Sub-Faz A++ telemetri


PageHits Middleware Mimarisi

PageHits + AttackProbes + IpBlocklist + AttackBurstCounter: Sub-Faz A++ schema ER diagram
PageHits + AttackProbes + IpBlocklist + AttackBurstCounter: Sub-Faz A++ schema ER diagram

Sistemin kalbi tek bir ASP.NET Core middleware: TrafficClassifierMiddleware. Her HTTP request'i 4 karar noktasından geçiriyor ve uygun tabloya yazıyor. Önceki VisitorAnalyticsMiddleware'i yerine geçti, gerçek visitor vs bot ayrımını burada yapıyorum.

4 karar noktası

Request gelir
   ↓
1. IpBlocklist aktif kayıt? ──── evet ──→ 403 Forbidden + log
   ↓ hayır
2. Attack pattern eşleşmesi?  ── evet ──→ dbo.AttackProbes INSERT
   ↓ hayır
3. Meşru bot (Googlebot, Bingbot, IndexNowBot)? ── evet ──→ atla
   ↓ hayır
4. Gerçek-insan kabul → dbo.PageHits INSERT

Tek middleware, tek source-of-truth. Multiple middleware'lere bölünmüyor çünkü karar sıralaması kritik — IpBlocklist'i AttackProbes'tan önce kontrol etmen lazım yoksa bloklanmış IP DB'ye yine kayıt geçiyor.

dbo.PageHits şeması

CREATE TABLE dbo.PageHits (
    Id              BIGINT IDENTITY(1,1) PRIMARY KEY,
    DayIso          CHAR(10)     NOT NULL,  -- "2026-05-11"
    VisitorHash     CHAR(32)     NOT NULL,  -- MD5(IP + daily salt)
    Path            NVARCHAR(256) NOT NULL,
    Culture         CHAR(2)      NULL,       -- tr/en/ar
    Referer         NVARCHAR(512) NULL,
    Country         CHAR(2)      NULL,       -- MaxMind GeoLite2-City
    City            NVARCHAR(80) NULL,
    Asn             NVARCHAR(120) NULL,
    AsnNumber       INT          NULL,       -- MaxMind GeoLite2-ASN
    StatusCode      SMALLINT     NULL,
    RawIp           NVARCHAR(45) NULL,       -- IPv4 + IPv6
    ConsentGranted  BIT          NOT NULL DEFAULT 0,
    HitTime         DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME()
);

CREATE NONCLUSTERED INDEX IX_PageHits_DayIso_Hash
    ON dbo.PageHits(DayIso, VisitorHash) INCLUDE (Path, HitTime);

İki kritik karar: (1) VisitorHash günlük rotating salt'lı MD5 — gün-aşırı korelasyon engelleniyor, KVKK uyumu kolaylaşıyor; (2) RawIp saklanıyor ama 90 gün retention sonrası otomatik DELETE. AttackProbes ve PageHits ayrı tablolar — saldırı kaydı analytics'i bozmuyor.

Dedup: 30 saniyelik in-memory cache

Yüksek-trafik bir page için aynı kullanıcı 30 saniye içinde 10-20 request atabilir (resim yüklenmesi, AJAX poll, vb.). Hepsini DB'ye yazmak gereksiz. IMemoryCache vh: prefix'iyle (IP, path) tuple'ı cache'liyorum, 30 saniye TTL:

var cacheKey = $"vh:{ipHash}:{path}";
if (_cache.TryGetValue(cacheKey, out _)) return; // duplicate atla
_cache.Set(cacheKey, true, PageDedupTtl);
// ... PageHits INSERT

Bu basit kontrol DB yükünün yaklaşık %70'ini düşürüyor — high-traffic page'lerde özellikle.

VisitorHash hesaplama

private static string ComputeVisitorHash(string ip, string dayIso)
{
    var salt = _config["Security:VisitorHashSalt"]; // Vault'tan
    var input = $"{ip}|{dayIso}|{salt}";
    var bytes = Encoding.UTF8.GetBytes(input);
    var hash = MD5.HashData(bytes);
    return Convert.ToHexString(hash).ToLowerInvariant();
}

MD5 cryptographic değil ama VisitorHash'ı reverse engineering yapacak bir attacker zaten Raw IP'ye sahip olur (PageHits'te de var). Burada amaç salt'lı unique ID — sha256 overkill, MD5 yeterli. Salt günlük rotating olsa daha güvenli ama o noktada daily aggregate query'leri zorlaşıyor. Mevcut salt prod salt'ı sabit; tek-yön hash.

dbo.PageHits son 7 gün ülke + ASN dağılımı: top 15 satır, IsSuspectNetwork yüzdesi sütunu


AttackProbes + ThreatScore Formülü

Saldırı denemelerinin kaydı için ayrı tablo. Niye ayrı? Üç sebep: (1) AttackProbes 19 kolon, PageHits 13 kolon — schema overlap az; (2) farklı retention politikaları (AttackProbes evidence, daha uzun tutulabilir); (3) farklı sorgu pattern'ları (AttackProbes ASN/category aggregate, PageHits VisitorHash daily count).

dbo.AttackProbes şeması ve PERSISTED ThreatScore

CREATE TABLE dbo.AttackProbes (
    Id              BIGINT IDENTITY(1,1) PRIMARY KEY,
    HitTime         DATETIME2(0) NOT NULL,
    DayIso          CHAR(10)     NOT NULL,
    Host            NVARCHAR(80) NULL,
    Source          VARCHAR(20)  NOT NULL,    -- 'middleware' veya 'log_backfill'
    RawIp           NVARCHAR(45) NOT NULL,
    Country         CHAR(2)      NULL,
    City            NVARCHAR(80) NULL,
    Asn             NVARCHAR(120) NULL,
    AsnNumber       INT          NULL,
    Method          VARCHAR(10)  NOT NULL,
    Path            NVARCHAR(512) NOT NULL,
    QueryString     NVARCHAR(1024) NULL,
    UserAgent       NVARCHAR(512) NULL,
    Referer         NVARCHAR(512) NULL,
    StatusCode      SMALLINT     NULL,
    Category        VARCHAR(40)  NOT NULL,
    Severity        TINYINT      NOT NULL,    -- 1-5
    Confidence      TINYINT      NOT NULL,    -- 1-100
    ThreatScore     AS (CAST(Severity AS INT) * Confidence / 20) PERSISTED,
    PatternMatched  NVARCHAR(120) NULL,
    CloudflareRayId VARCHAR(20)  NULL,
    CONSTRAINT CK_AttackProbes_Severity   CHECK (Severity   BETWEEN 1 AND 5),
    CONSTRAINT CK_AttackProbes_Confidence CHECK (Confidence BETWEEN 1 AND 100)
);

Kritik nokta: ThreatScore computed PERSISTED column. Insert anında hesaplanıyor, index'leyebiliyor, sorguda re-compute olmuyor. Bu, "threat ≥ 11" filter sorgularını hızlandıran detay.

ThreatScore formülü ve eşik kararı

ThreatScore = (Severity × Confidence) / 20
  • Severity (1-5): saldırının teknik ciddiyeti (1=düşük, 5=kritik)
  • Confidence (1-100): pattern match güveni (50=eşit ihtimal, 100=kesin)

Range: 0 ile 25 arası. AutoBlockThreshold = 11. Eski projede 12 idi ama integer division gözden kaçırıyordu: 3 × 75 / 20 = 11 (border-zone admin probe gibi orta-Severity yüksek-Confidence kombinasyonu), eşik 12 olunca bu yakalanmıyordu. 11'e indirilince false-positive de görülmedi.

Attack path pattern tablosu

AttackPaths static array, middleware'de path başlangıcı match'i yapıyor. Aşağıda yüksek-ThreatScore subset:

Path Kategori Severity Confidence ThreatScore
/.env secret_disclosure 5 95 23
/.aws cloud_creds_probe 5 95 23
/.ssh secret_disclosure 5 95 23
/.htpasswd secret_disclosure 5 90 22
/credentials cloud_creds_probe 5 90 22
/database.yml secret_disclosure 5 90 22
/.git/ vcs_disclosure 4 95 19
/xmlrpc.php wordpress_probe 4 90 18
/phpmyadmin phpmyadmin_probe 4 90 18
/wp-admin wordpress_probe 3 85 12
/admin.php admin_panel_probe 3 80 12

Tam liste yaklaşık 50 pattern, 9 kategori. Yeni pattern eklemek statik array'e bir satır eklemek + pod restart kadar basit.

9 kategori kümesi

Kategori Anlam Örnek pattern
secret_disclosure Dosya açığa çıkarma /.env, /.ssh, /.htpasswd
cloud_creds_probe Bulut kimlik tarama /.aws, /credentials, /config.json
vcs_disclosure Sürüm kontrol açığa çıkarma /.git/, /.svn/
wordpress_probe WP varsayım tarama /wp-admin, /wp-login, /xmlrpc.php
phpmyadmin_probe PMA paneli tarama /phpmyadmin, /pma/, /myadmin
admin_panel_probe Generic admin endpoint /admin.php, /login.php, /console
exchange_probe MS Exchange / OWA /owa/, /ews/
sqli_payload Query string SQL injection union select, ' or 1=1
log4shell Log4j JNDI exploit ${jndi:ldap://...}

Her kategori için ayrı Prometheus counter label var (bilal_attack_probe_blocks_total{category="..."}) — Grafana panel'inde kategori bazlı kırılım rahat.

📊 [GRAFİK: Son 24h kategori dağılım pie chart — secret_disclosure %48, cms-probe %29, sqli %10, vb.]


AttackBurstCounter: MERGE+OUTPUT Sliding Window

ThreatScore eşiği saldırı pattern'inin tek istekini yakalıyor. Ama bazı saldırgan davranışlar bunu aşıyor: trickle attacker dakikada 1 düşük-Severity request gönderiyor, hiçbiri tek başına 11 puanı aşmıyor ama 15 dakikada 5-10 farklı sensitive path tarıyor. Bunu yakalamak için sliding-window counter lazım.

dbo.AttackBurstCounter tablosu

CREATE TABLE dbo.AttackBurstCounter (
    RawIp        NVARCHAR(45) NOT NULL PRIMARY KEY,
    WindowStart  DATETIME2(0) NOT NULL,
    Count        INT          NOT NULL,
    LastHitAt    DATETIME2(0) NOT NULL
);

Tek satır per IP. Pencere genişliği 15 dakika (BurstWindow = TimeSpan.FromMinutes(15)). Trigger eşiği 3 (AttackBurstThreshold = 3).

Atomic MERGE + OUTPUT SQL pattern

Race condition'a karşı atomic increment lazım — multi-replica K8s pod setup'ında pod-A ve pod-B aynı IP için aynı anda gelirse counter yarış kaybeder. SQL Server'ın MERGE + OUTPUT inserted.Count kombinasyonu bunu tek roundtrip'te yapıyor:

DECLARE @Now DATETIME2 = SYSUTCDATETIME();
DECLARE @WindowStart DATETIME2 = DATEADD(MINUTE, -15, @Now);

MERGE dbo.AttackBurstCounter AS target
USING (SELECT @ip AS RawIp, @Now AS NowTs) AS source
ON target.RawIp = source.RawIp
WHEN MATCHED AND target.WindowStart > @WindowStart
    -- Aynı pencere içinde: counter++
    THEN UPDATE SET Count = target.Count + 1, LastHitAt = @Now
WHEN MATCHED AND target.WindowStart <= @WindowStart
    -- Pencere kaymış: counter=1, yeni window
    THEN UPDATE SET Count = 1, WindowStart = @Now, LastHitAt = @Now
WHEN NOT MATCHED
    -- İlk attack: insert
    THEN INSERT (RawIp, WindowStart, Count, LastHitAt)
         VALUES (@ip, @Now, 1, @Now)
OUTPUT inserted.Count AS NewCount;

OUTPUT inserted.Count sayesinde middleware tek query'de yeni counter'ı alıyor; if NewCount >= 3 → block trigger. Eski "SELECT then UPDATE" yaklaşımı multi-replica'da race condition'a düşüyordu, bu sorunu yakaladık ve MERGE'e geçtik.

Niye DB'de, in-memory dictionary'de değil?

Üç sebep:

  1. Multi-replica tutarlılık: Pod-A counter'ı pod-B görmüyor; DB tek source-of-truth
  2. Pod restart direnci: in-memory restart'ta sıfırlanır; DB persist
  3. Audit trail: "saldırgan ne zaman, kaç defa, hangi pencerede" sorgulanabilir

Trade-off: her saldırı request'ine DB roundtrip ekleniyor (~3-5ms). Ama saldırı request'leri zaten yavaş cevap alacak — origin'e gelmiş olması bile zaman kazandırmıyor. Normal request'ler bu yolu hiç görmüyor.

In-memory fallback (DB unreachable)

DB connection problem'i durumunda middleware crash etmemeli. IMemoryCache ab: prefix'iyle pod-local fallback:

if (dbUnavailable) {
    var cacheKey = $"ab:{ip}";
    int count = _cache.GetOrCreate(cacheKey, entry => {
        entry.AbsoluteExpirationRelativeToNow = BurstWindow;
        return 0;
    });
    _cache.Set(cacheKey, count + 1, BurstWindow);
    return count + 1;
}

Multi-replica'da kayıp tolere ediliyor — pod-local counter pod-B'nin counter'ını bilmiyor. Trade-off: tam doğru sayım yerine "geçici çözüm". DB recovery'den sonra normal yol.

Hourly cleanup hosted service

AttackBurstCounterCleanupHostedService her saat çalışıyor, expired window'ları siliyor:

DELETE FROM dbo.AttackBurstCounter
WHERE WindowStart < DATEADD(MINUTE, -30, SYSUTCDATETIME());

30 dakika güvenlik marjı (window 15 dakika + 15 dakika buffer). DB büyümesi sınırlı kalıyor, query performance bozulmuyor.

📊 [METRIK: 24h burst-triggered block sayısı vs threshold-triggered block sayısı]


IsSuspectNetwork Flag: Datacenter / VPN / Null ASN

Sub-Faz A'da PageHits prod'a girdiğinde ilk 24 saatte sürpriz çıktı: 126 unique IP analytics'te göründü ama gerçek insan sayısı yaklaşık 25-30. Bu sayının ne kadar kirli olduğunu anında fark ettim: gerçek kullanıcı sayısını ölçemediğim sürece dashboard'a güvenemiyordum. Bu noktada Sub-Faz A+++ tasarımı çıktı.

Karar: ayır, silme

İki yaklaşım vardı: (1) datacenter trafiğini PageHits'ten dışlamak (filter at write), veya (2) tüm trafiği saklayıp flag'lemek (filter at read). İkincisi seçildi çünkü:

  • Datacenter trafiği da bilgi taşıyor (scraper davranışı, hangi sayfalar tarınıyor)
  • Karar geriye dönük değiştirilebilir (flag'i UPDATE'lemek silmekten kolay)
  • Privacy audit için "ne tuttuğumuz" netlik daha önemli

PageHits.IsSuspectNetwork BIT kolonu

ALTER TABLE ile eklendi:

ALTER TABLE dbo.PageHits
ADD IsSuspectNetwork BIT NOT NULL CONSTRAINT DF_PageHits_IsSuspect DEFAULT 0;

-- Backfill mevcut kayıtları
UPDATE dbo.PageHits
SET IsSuspectNetwork = 1
WHERE AsnNumber IS NULL
   OR AsnNumber IN (16509, 14618, 14061, 24940, 8075, /* ... 42 ASN */);

Middleware Classify step 8'de set ediliyor:

var asnNumber = geoIpResult?.AsnNumber;
bool isSuspect = DatacenterAsnRegistry.IsAnonymousOrDataCenter(asnNumber);
// PageHits INSERT'inde IsSuspectNetwork = isSuspect

IsAnonymousOrDataCenter karar mantığı

public static bool IsAnonymousOrDataCenter(int? asnNumber)
    => !asnNumber.HasValue || DataCenterAsns.Contains(asnNumber.Value);

İki koşul:

  1. !asnNumber.HasValue (null ASN): MaxMind ASN database'inde yok. Modern internet'te tüm meşru ISP'leri çözüyor (Turkcell, TTNet, Vodafone TR, vb.), null = yeni allocation gap veya garbage IP. Sub-Faz A++'da bu kararla 60'a yakın sızıntı kaynağı düzeldi.
  2. DataCenterAsns.Contains(asnNumber.Value): 42 ASN listesi (tam liste Pillar 1'de) — AWS, GCP, Azure, DigitalOcean, OVH, Hetzner, M247 (VPN backbone), HostHero, IPXO, vb.

ISP'ler (Turkcell 16135, TTNet 9121, Vodafone TR 15897, Bharti Airtel IN 45609) dışında, false-positive riski sıfır.

Admin dashboard "Trafik özeti" kartı

┌─ Trafik Özeti (son 24 saat) ───────────────────────────┐
│                                                         │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│   │   15     │ │    5     │ │   10     │ │   15     ││
│   │ Toplam   │ │ Gerçek   │ │ Şüpheli  │ │  Block   ││
│   │  unique  │ │  (%33)   │ │  (%67)   │ │  (CF)    ││
│   └──────────┘ └──────────┘ └──────────┘ └──────────┘│
│                                                         │
│   Gerçek vs Şüpheli oran:                              │
│   ████░░░░░░░░░░░░░░░░░░░░░░░░░░░ 33% gerçek           │
│                                                         │
│   Süre: [Bugün] [Son 7 gün] [Son 30 gün]               │
└─────────────────────────────────────────────────────────┘

7 günlük rolling: 117 gerçek vs 246 toplam, %67.8 şüpheli oranı. Aylık trend takibi için zaman serisi tablosu altta.

Backfill 246 satır

Mevcut PageHits'te 246 satır vardı. Toplu UPDATE ile IsSuspectNetwork set edildi:

UPDATE dbo.PageHits
SET IsSuspectNetwork = 1
WHERE AsnNumber IS NULL OR AsnNumber IN (/* 42 ASN listesi */);

-- Doğrulama
SELECT
    COUNT(*) AS Total,
    SUM(CASE WHEN IsSuspectNetwork = 1 THEN 1 ELSE 0 END) AS Suspect,
    SUM(CASE WHEN IsSuspectNetwork = 0 THEN 1 ELSE 0 END) AS Real
FROM dbo.PageHits;
-- Sonuç: 246 / 165 suspect / 81 real

İlk hafta gerçek insan sayısının %33 civarında olduğu doğrulandı. Bu Pillar 2 yazısının ortaya çıkma sebebi: orijinal data, fact-based içerik.

/admin/dashboard "Trafik özeti" kartı: 4 sayı (bugün/7 gün × gerçek/şüpheli) + %67.8 suspect progress bar



MaxMind GeoIP Singleton Reader Pattern

PageHits ve AttackProbes tablolarında Country, City, ASN, AsnNumber kolonları var. Bu enrichment'ı MaxMind.GeoIP2.DatabaseReader ile yapıyorum. Doğru kurulum kritik: yanlış yapılırsa her request'te file open/close ile dakikada binlerce IO yer.

DatabaseReader thread safety

MaxMind dokümantasyonu net: DatabaseReader thread-safe, tek bir instance tüm uygulamada paylaşılabilir. Hatta paylaşılmalı — file-backed memory-mapped struct, açma maliyetli (~80MB City + ~6MB ASN). Singleton pattern doğru çözüm.

GeoIPDatabaseReaders wrapper

DI'ye singleton kaydı için minimal wrapper:

public sealed class GeoIPDatabaseReaders : IDisposable
{
    public DatabaseReader CityReader { get; }
    public DatabaseReader AsnReader { get; }

    public GeoIPDatabaseReaders(IWebHostEnvironment env)
    {
        var dir = Path.Combine(env.ContentRootPath, "data", "geoip");
        CityReader = new DatabaseReader(Path.Combine(dir, "GeoLite2-City.mmdb"));
        AsnReader  = new DatabaseReader(Path.Combine(dir, "GeoLite2-ASN.mmdb"));
    }

    public void Dispose() { CityReader?.Dispose(); AsnReader?.Dispose(); }
}

// Program.cs
services.AddSingleton<GeoIPDatabaseReaders>();

Disposable çünkü process exit anında file handle'lar düzgün kapansın. Normal request flow'da Dispose tetiklenmez.

Performans ölçümü

Tek IP enrichment latency:

Metrik p50 p95 p99
City lookup 0.12ms 0.34ms 0.81ms
ASN lookup 0.08ms 0.21ms 0.52ms
Toplam 0.20ms 0.55ms 1.33ms

Pod başına ~85MB memory mapped (City + ASN dosyaları). 24/7 prod'da bu rakamlar stabil; spike yok.

DB güncelleme stratejisi

MaxMind GeoLite2 ücretsiz tier'ı haftalık güncelleniyor. İki yaklaşım:

  1. Cron + pod restart: wget yeni mmdb dosyasını çek, k8s ConfigMap update, pod restart
  2. Init container: Pod boot'ta init-geoip container mmdb'yi indirir, main container singleton ile yükler

Bizim seçim #1: kontrolü açık, audit log var, restart ihtiyacında zaten oluyor. Yeni ISP eklemeleri haftada ~50 satır; bizim ASN registry zaten 42 ASN, ISP değişimi minimal etkili.

Boot süresi

İlk DB load'da pod ~1.5 saniye daha geç hazır oluyor (Kestrel start + GeoIP load). Liveness probe initial delay 10 saniyeye ayarlı, sorun değil. Readiness probe DB connection check'i bekliyor, GeoIP bekleyiştende değil — bağımsız.

📊 [METRIK: GeoIP lookup p50/p95/p99 Grafana panel]


Log → AttackProbes Backfill ETL

AttackProbes tablosu 2026-05-10'da prod'a girdi. Önceki dönem saldırı kayıtları dbo.Logs tablosunda (Serilog) var; geriye dönük analiz için bunları AttackProbes'a aktarmak gerekiyor. Idempotent ETL ile yapıldı.

Niye backfill?

Üç değer ekledi:

  1. Trend analizi: "Mart 2026 öncesi vs sonrası" saldırı pattern değişimi grafiği
  2. Audit: geçmişteki saldırgan IP'lerin block listesine girip girmediği teyit
  3. Veri tutarlılığı: Pillar 2 yazısının "157 probe 24h" rakamı geçmiş ile karşılaştırılabilir olsun

Source kolonu marker'ı

Backfill kayıtlarını canlı middleware insertlerinden ayırmak için AttackProbes.Source kolonu var:

  • Source = 'middleware' → canlı, TrafficClassifierMiddleware'den
  • Source = 'log_backfill' → ETL, geçmiş log'lardan

Reporting query'leri WHERE Source = 'middleware' ile filtreliyor; geriye dönük temizlik gerekirse DELETE FROM AttackProbes WHERE Source = 'log_backfill' tek satırla yapılır.

ETL script: 2026-05-10-backfill-attack-probes-from-logs.sql

-- Idempotent: zaten varsa atlar (NOT EXISTS guard)
INSERT INTO dbo.AttackProbes (
    HitTime, DayIso, Source, RawIp, Method, Path,
    UserAgent, Referer, StatusCode,
    Category, Severity, Confidence, PatternMatched
)
SELECT
    e.LogEvent_Date AS HitTime,
    CONVERT(CHAR(10), e.LogEvent_Date, 23) AS DayIso,
    'log_backfill' AS Source,
    -- IP regex extract from log message
    SUBSTRING(e.Message,
        CHARINDEX('"ClientIp":"', e.Message) + 12,
        CHARINDEX('"', e.Message, CHARINDEX('"ClientIp":"', e.Message) + 12)
            - CHARINDEX('"ClientIp":"', e.Message) - 12
    ) AS RawIp,
    'GET' AS Method,
    -- Path regex extract
    SUBSTRING(e.Message, /* ... */) AS Path,
    NULL AS UserAgent,
    NULL AS Referer,
    NULL AS StatusCode,
    -- Pattern → category mapping
    CASE
        WHEN e.Message LIKE '%/.env%'      THEN 'secret_disclosure'
        WHEN e.Message LIKE '%/.aws%'      THEN 'cloud_creds_probe'
        WHEN e.Message LIKE '%/.git/%'     THEN 'vcs_disclosure'
        WHEN e.Message LIKE '%/wp-admin%'  THEN 'wordpress_probe'
        WHEN e.Message LIKE '%/phpmyadmin%' THEN 'phpmyadmin_probe'
        WHEN e.Message LIKE '%/admin.php%' THEN 'admin_panel_probe'
        ELSE 'unknown'
    END AS Category,
    -- Severity / confidence ortalama (path'e göre fine-tune)
    3 AS Severity, 75 AS Confidence,
    'log-pattern-match' AS PatternMatched
FROM dbo.Logs e
WHERE e.Level = 'Warning'
  AND e.Message LIKE '%TrafficClassifier%'
  AND NOT EXISTS (
      SELECT 1 FROM dbo.AttackProbes ap
      WHERE ap.Source = 'log_backfill'
        AND ap.HitTime = e.LogEvent_Date
        AND ap.RawIp = SUBSTRING(e.Message, /* ... */)
  );

Sonuç

Metrik Sayı
Toplam log satırı taranan ~8500
AttackProbes'a eklenen satır 503
Unique IP 53
Unique kategori 9
ETL süresi ~12 saniye
Re-run güvenli ✅ NOT EXISTS guard

503 yeni satır canlı 246 ile birleşince 24 saatlik bugünkü 157 + geçmiş 503 = 660 kayıt analiz edilebilir oldu — temel istatistiğe yeterli.

Backfill öncesi/sonrası karşılaştırma

SELECT Source, COUNT(*) AS Records,
       MIN(HitTime) AS FirstHit, MAX(HitTime) AS LastHit
FROM dbo.AttackProbes
GROUP BY Source;

Sonuç:

Source Records First Last
middleware 157 2026-05-10 09:14 2026-05-11 09:14
log_backfill 503 2026-04-22 03:51 2026-05-10 08:55

📊 [GRAFİK: Source bazlı cumulative attack probes line chart]


Origin Engelleme Döngüsü: TrafficClassifier → IpBlocklist → CF List

Sistemin uç noktası: saldırı pattern'i yakalandığında otomatik block. Origin classifier → DB blocklist → Cloudflare edge push üç-adımlı zincir.

Akış diyagramı

Request → TrafficClassifier → Attack pattern eşleşti?
                                    ↓ evet
                            AttackProbe INSERT
                                    ↓
                       AttackBurstCounter MERGE+OUTPUT
                                    ↓
                  ThreatScore ≥ 11 VEYA Count ≥ 3?
                                    ↓ evet
                       IpBlocklist INSERT (24h TTL)
                                    ↓ fire-and-forget
              CloudflareIpListService.AddBlockedIpAsync()
                                    ↓
       UPDATE dbo.IpBlocklist SET CloudflareListItemId = @id
                                    ↓
            (sonraki request edge'de WAF rule ile durur)

Çift-katman block

İlk request bloklamadan SONRA origin'e bir kez daha gelir (24h içinde başka bir IP'den) — DB lookup yapar, IMemoryCache bl: prefix 60 saniye cache'ler. Aynı IP 60 saniye içinde 100 request atsa bile sadece 1 DB query — sonrası cache hit, 403 hızlı dönüyor.

Cloudflare edge tarafında ise WAF rule "ip.src in $bilalkose_auto_block" zaten yakalıyor (Pillar 1 H2-3 detayı). İlk request origin'e gelir, sonraki edge'de durur — origin trafiği temizlenir.

IpBlocklist schema

CREATE TABLE dbo.IpBlocklist (
    RawIp                NVARCHAR(45) PRIMARY KEY,
    BlockedAt            DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(),
    ExpiresAt            DATETIME2(0) NOT NULL,
    Reason               NVARCHAR(200) NOT NULL,
    ThreatScore          INT NOT NULL,
    HitCount             INT NOT NULL DEFAULT 1,
    LastHitAt            DATETIME2(0) NOT NULL,
    CloudflareListItemId NVARCHAR(64) NULL
);

Tek-satır per IP. CloudflareListItemId ilk null, Cloudflare push başarılı olduğunda UPDATE ile dolduruluyor — TTL expirer için zorunlu (item ID ile silme).

TTL expirer hosted service

IpBlocklistExpiryHostedService her dakika çalışıyor:

public async Task ExecuteAsync(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        var expired = await _db.IpBlocklist
            .Where(x => x.ExpiresAt < DateTime.UtcNow)
            .ToListAsync(ct);

        foreach (var entry in expired)
        {
            if (entry.CloudflareListItemId != null)
                await _cf.RemoveBlockedIpAsync(entry.CloudflareListItemId, ct);
            _db.IpBlocklist.Remove(entry);
        }
        await _db.SaveChangesAsync(ct);
        await Task.Delay(TimeSpan.FromMinutes(1), ct);
    }
}

DB'den silinen kayıt audit için AttackProbes'ta zaten var; saldırı geçmişi kaybolmuyor, sadece aktif block listesi temizleniyor.

Manuel override

/admin/security/ip-blocklist Razor view dört aksiyon sunuyor (Pillar 1 H2-4 paralel):

  • Index: Aktif block listesi tablosu
  • PushCf: Tek IP push (CF List'e)
  • PushAll: CloudflareListItemId boş tüm IP'leri toplu push
  • Remove: DB + CF List senkron silme

/admin/security/ip-blocklist Razor sayfası: 15 IP, hepsi Cloudflare edge'de aktif + "CF'ye push" CTA

Cross-pillar: Bu döngünün edge tarafı Pillar 1 H2-4'te. İki pillar birlikte tam pipeline'ı kapsıyor.


Real Visitor Admin Dashboard

Veriyi DB'ye yazmak yetmez — admin için anlamlı bir görselleştirme katmanı gerekiyor. Razor + Chart.js minimal yaklaşımla kuruldu, hiçbir SPA framework yok.

Sayfa yapısı

┌─────────────────────────────────────────────────────┐
│ Header — 4-sayı kart (Toplam, Gerçek, Şüpheli, Block)│
├─────────────────────────────────────────────────────┤
│ Trafik saatlik line chart (24h)                     │
├─────────────────────────────────────────────────────┤
│ Top 10 Country │ Top 10 ASN │ Top 10 Path           │
├─────────────────────────────────────────────────────┤
│ Son 50 Attack Probe (real-time table)               │
├─────────────────────────────────────────────────────┤
│ 7-gün özet tablo                                    │
└─────────────────────────────────────────────────────┘

Razor + Chart.js minimal stack

Hiç React, hiç Vue, hiç SignalR (henüz). Razor view inline <script> ile Chart.js initialize. AJAX endpoint JSON döndürüyor, polling 30 saniye.

<canvas id="hourlyChart" height="100"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
fetch('/admin/security/api/hourly?days=1')
  .then(r => r.json())
  .then(data => new Chart('hourlyChart', {
    type: 'line',
    data: {
      labels: data.hours,
      datasets: [
        { label: 'Gerçek', data: data.real, borderColor: '#10b981' },
        { label: 'Şüpheli', data: data.suspect, borderColor: '#6b7280' }
      ]
    }
  }));
</script>

Performans

Aggregate query'leri SQL Server'da yapıyor (window function, GROUP BY DayIso). Output cache 60 saniye — admin sayfası dakikada 60 hit alsa bile 1 DB query yetiyor.

Pod başına ~5MB memory overhead, network ~30KB/page (Chart.js CDN).

Auth + RBAC

ASP.NET Core Identity, [Authorize(Roles = "Admin")]. Cookie-based auth, 12 saat session. Admin panelinin tüm endpoint'leri bu attribute ile korunuyor:

[Authorize(Roles = "Admin")]
[Route("admin/security")]
public class SecurityController : Controller { /* ... */ }

/admin/dashboard Trafik özeti kartı yakın çekim — aggregate sayılar, IP yok


Prometheus Metric Export

DB'deki veri admin için; canlı/alerting için Prometheus + Grafana yolu lazım. Pull-based, K8s-native, ücretsiz.

prometheus-net library entegrasyonu

// Program.cs
app.UseMetricServer();   // /metrics endpoint açar
app.UseHttpMetrics();    // ASP.NET Core request count + latency otomatik

// K8s Service annotation
metadata:
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "8080"
    prometheus.io/path: "/metrics"

Prometheus 15 saniyede bir /metrics'i scrape ediyor, time-series storage'a yazıyor (default retention 15 gün).

Custom counter'lar

public static class SecurityMetrics
{
    public static readonly Counter AttackProbeBlocksTotal =
        Metrics.CreateCounter(
            "bilal_attack_probe_blocks_total",
            "Origin-side attack probe blocks",
            "category");

    public static readonly Counter IpListItemsAddedTotal =
        Metrics.CreateCounter(
            "bilal_cf_ip_list_items_added_total",
            "Cloudflare List item additions",
            "status");

    public static readonly Histogram ClassifyLatency =
        Metrics.CreateHistogram(
            "bilal_classify_latency_seconds",
            "TrafficClassifier middleware execution time");
}

Middleware'in critical path'inde using (SecurityMetrics.ClassifyLatency.NewTimer()) { ... } ile latency ölçümü, "category" label'ı ile attack kategori dağılımı.

Grafana panel'leri

  • Block rate: rate(bilal_attack_probe_blocks_total[5m]) line chart
  • Category breakdown: sum by (category) (bilal_attack_probe_blocks_total) pie chart
  • Latency p95: histogram_quantile(0.95, rate(bilal_classify_latency_seconds_bucket[5m]))
  • CF push success rate: bilal_cf_ip_list_items_added_total{status="ok"} / sum(bilal_cf_ip_list_items_added_total)

Tier1-critical-security alert grubu

Alerting rule 10 madde (memory'deki project_grafana_alerting_2026_may referansı):

- alert: AttackProbeSurge
  expr: rate(bilal_attack_probe_blocks_total[5m]) > 100/60
  for: 2m
  labels: { severity: critical, tier: 1 }
  annotations:
    summary: "Saldırı atışı normal trafiğin çok üstünde"
    description: "Son 5 dakikada {{ $value }} req/sec attack probe — sahte kullanıcı patlaması veya WAF rule yetersizliği"

Telegram routing tier-1'e — telefonuma anında bildirim.

Cardinality bütçesi

Prometheus label kombinasyonu high-cardinality olursa storage patlar. Path veya IP'yi label yapmak felaket. Bizde sadece 2 label:

  • category (9 unique value)
  • status (3 unique value)

Toplam time-series sayısı: 9 + 3 = 12 yeni series. Memory overhead ~0.1MB.

Grafana güvenlik paneli — 30 günlük: hostname bazında WAF olayları, IP blocklist ekle/çıkar oranı, aktif blocklist zaman serisi ve kaynak kategori dağılımı


Privacy + KVKK: PII'siz Telemetry Tasarımı

Türkiye'de blog çalıştırıyorsan KVKK (6698 sayılı kanun) uyumu zorunlu. Avrupa kullanıcı varsa ek olarak GDPR. PageHits + AttackProbes design'ı baştan PII'siz olacak şekilde kuruldu.

Hangi veri tutulur, hangi tutulmaz

Tutulur (legitimate):

  • VisitorHash — MD5(IP + daily salt), bir gün-içi unique ama gün-aşırı korelasyonsuz
  • ✅ City-level GeoIP — sokak/adres değil, şehir bazlı
  • ✅ ASN (ISP/Organization name)
  • ✅ User-Agent string
  • ✅ Path + StatusCode + HitTime

Tutulmaz (banned):

  • ❌ SessionId, UserId — PageHits için (giriş yapan kullanıcı ayrı tracking)
  • ❌ Email, telefon, isim
  • ❌ Form data, request body
  • ❌ Cookie content (sadece consent flag)

Raw IP saklama gerekçesi (KVKK m.5/2-(f))

AttackProbes.RawIp ve PageHits.RawIp saklanıyor. KVKK madde 5/2-(f):

Veri sorumlusunun meşru menfaatleri için veri işlenmesinin zorunlu olması, ilgili kişinin temel hak ve özgürlüklerine zarar vermemek kaydıyla, açık rıza aranmaksızın kişisel verilerin işlenebileceği halleri kapsar.

Sistem güvenliği (saldırı denemelerini bloklamak, abuse pattern tespit etmek) meşru menfaat. Aydınlatma metni Privacy.tr.resx'te 13. bölümde detaylı; KVKK Aydınlatma Yükümlülüğü doğrudan karşılanıyor.

Cookie banner Consent v2 (Sub-Faz A'da yenilendi). Granted olunca:

var consent = httpContext.Request.Cookies["bk_consent_v1"] == "granted";
// PageHits INSERT'inde ConsentGranted = consent

Granted olmayan kullanıcıların kayıtları da saklanıyor (sistem güvenliği meşru menfaat), ama analytics/marketing kullanımı için ConsentGranted=1 filtresi uygulanıyor. Bu ayrım GDPR Article 6(1)(a) açık rıza vs (f) legitimate interest ayrımına paralel.

90 gün otomatik retention

-- Daily scheduled job
DELETE FROM dbo.PageHits
WHERE HitTime < DATEADD(DAY, -90, SYSUTCDATETIME());

DELETE FROM dbo.AttackProbes
WHERE HitTime < DATEADD(DAY, -90, SYSUTCDATETIME());

90 gün KVKK saklama süresi içinde "makul". Aydınlatma metninde belirtildi.

Backup'lar başka bir konu — DB backup'larında da PII'siz tasarım sayesinde sorun yok; backup encryption + access control ile yeterli.

KVKK başvuru süreci

Veri sahibi (kullanıcı) KVKK m.11 haklarını kullanmak isterse:

  1. [email protected] mail (Privacy.tr.resx'te belirtilen iletişim)
  2. Talep: "veri silinme", "düzeltme", "amaç sorgusu"
  3. 30 gün içinde cevap (KVKK m.13)

PII'siz tasarım sayesinde "spesifik kullanıcının verilerini bul" mümkün olmuyor — bunu KVKK kurumuna proaktif bildiriyoruz ("Kişisel veri silinme talebi: kullanıcıyı tekil olarak tanımlayamadığımız için spesifik kayıt bulunamadı; sistem güvenliği amaçlı tüm IP'ler 90 günde otomatik silindi.")

/tr/Privacy "Güvenlik kayıtları (zorunlu — onaysız)" bölümü: KVKK m.5/2-(f) + m.12 + GDPR 6(1)(f) + Madde 32 + 90 gün + Cloudflare WAF + 5651 sayılı kanun


Sıkça Sorulan Sorular

Cloudflare Analytics zaten visitor sayısı veriyor, ek tablo neden lazım?

Cloudflare Analytics free plan 5 dakika retention veriyor (Pro plan 24 saat). Daha kötüsü: datacenter/VPN trafiğini "real visitor" olarak sayıyor — bizim ölçümümüzde gerçek-vs-şüpheli oranın %33/%67 olduğu ortaya çıkamıyor. Kendi DB'mizde 90 gün retention + IsSuspectNetwork ayrımı + custom dashboard sağlıyoruz. Pillar 1'deki WAF Event Collector ile CF tarafını da topluyoruz, ikisi tamamlayıcı.

PageHits her request'e DB INSERT yapıyor, ölçeklenmez mi?

30 saniyelik IMemoryCache dedup (vh: prefix) yükün yaklaşık %70'ini düşürüyor. Aynı kullanıcı 30 saniyede 10 kez request atsa bile sadece 1 INSERT. Trafik 10K req/sec'e çıkarsa partition'lı async batch INSERT pattern'a geçilebilir (Kafka/RabbitMQ ara katmanı). Şu anki trafik (50-150 req/dakika) için over-engineer.

AttackBurstCounter MERGE neden in-memory dictionary yerine DB'de?

Pod restart direnci + multi-replica tutarlılığı. K8s 2+ replica setup'ta pod-A'nın counter'ı pod-B görmüyor; DB tek source-of-truth. Per-request overhead ~3-5ms (cache hit'te 0.1ms). Trafik küçük blog için optimal trade-off; high-throughput API'de Redis-backed counter (atomic INCR) daha uygun.

KVKK gerçekten Raw IP saklamaya izin veriyor mu?

Evet, madde 5/2-(f) "veri sorumlusunun meşru menfaatleri için veri işlenmesinin zorunlu olması" hükmüne göre. Sistem güvenliği (saldırı denemelerini bloklamak) meşru menfaat. Aydınlatma metni Privacy.tr.resx'te 13. bölümde detaylı. 90 gün retention sonra otomatik silinme + admin override + KVKK başvuru için iletişim kanalı ([email protected]).

Prometheus + Grafana on-prem maliyet ne?

prometheus-net MIT lisans, ücretsiz. Prometheus + Grafana OSS ücretsiz. K8s pod overhead: Prometheus ~500MB RAM (scrape interval 15s, retention 30 gün), Grafana ~200MB. Toplam $0 lisans + ~$5/ay K8s node payı. Alternatif SaaS: Grafana Cloud free 10K series, $19/ay 50K series. On-prem versiyon kontrol açısından üstün; alerting rule'ları, dashboard'lar git-tracked. Bu gözlemlenebilirlik pod'larının node bütçesine ve aylık maliyete etkisini önceden hesaplamak için Kubernetes kaynak bütçesi hesaplayıcısını kullanabilirsiniz.

IsSuspectNetwork flag'ini false-positive yapma riski yok mu?

42 ASN listesi sıkı: sadece datacenter/cloud/VPN provider'ları. ISP'ler (Turkcell 16135, TTNet 9121, Vodafone TR 15897, Bharti Airtel IN 45609) dışlanmış. Google ASN 15169 da dışlanmış (Googlebot zaten LegitBotUaPatterns'da, kalan = Google Cloud-hosted gerçek user). 24 saatlik gözlemde false-positive sayısı 0. Yeni ISP eklemeleri tracking için MaxMind ASN database haftalık güncellenmeli.


İlgili Yazılar

Bu pillar'ın cluster'ları (yayın sırası):

  • C1 PageHits middleware: minimal allocation request sayım pipeline
  • C2 AttackProbes: ThreatScore + 9 attack kategorisi detay
  • C3 AttackBurstCounter: MERGE+OUTPUT SQL deseni derinlik
  • C4 IsSuspectNetwork flag: VPN/datacenter karar matrisi
  • C5 MaxMind GeoIP entegrasyonu: singleton reader thread safety + lookup latency
  • C6 Log → AttackProbes idempotent backfill ETL pattern
  • C7 Origin engelleme + Cloudflare List senkron döngüsü
  • C8 Real Visitor Dashboard: Razor + Chart.js minimal stack
  • C9 Prometheus metric export: counter cardinality bütçesi
  • C10 KVKK uyumlu PageHits: PII'siz tasarım

Cross-pillar bridge (Pillar 1):

🌉 Cloudflare Edge Security + .NET on K8s — Pillar 1 — WAF rule + IP Blocklist + ASN registry + Origin firewall. İki pillar birlikte edge + origin tam güvenlik pipeline'ını kapsıyor.

Linkable assets:

  1. 🔗 ASN Registry CSV (gist) — 42 datacenter ASN, MIT lisans (Pillar 1 ile paylaşılan)
  2. 🔗 24h Anonimleştirilmiş Telemetry Snapshot CSV (gist) — gerçek vs şüpheli oran breakdown
  3. 🔗 PageHits Middleware Starter for ASP.NET Core 10 (GitHub repo) — minimal allocation referans

Sık Sorulan Sorular

Cloudflare Analytics zaten visitor sayısı veriyor, neden ek tablo?

CF Analytics 5 dakika retention (free plan) + datacenter/VPN trafiğini real visitor sayıyor. Kendi DB'mizde 90 gün retention + IsSuspectNetwork ayrımı + custom dashboard yapabiliyoruz. Pillar 1'deki WAF Event Collector ile CF tarafını da topluyoruz, ikisi tamamlayıcı.

PageHits her request'e DB INSERT yapıyor, ölçeklenmez mi?

30 saniyelik dedup cache (IMemoryCache vh: prefix) çoğu yükü düşürüyor. High-traffic page'de aynı kullanıcı 30 saniyede 10 kez request yapsa bile sadece 1 INSERT. Trafik 10K req/sec'e çıkarsa partition'lı async batch INSERT pattern'a geçilir.

AttackBurstCounter MERGE neden in-memory dictionary yerine DB'de?

Pod restart direnci ve multi-replica tutarlılığı. K8s 2+ replica setup'ta pod-A'nın counter'ı pod-B görmüyor; DB tek source-of-truth. Per-request overhead ~3-5ms. Trafik küçük blog için optimal trade-off; high-throughput API'de Redis-backed counter daha uygun.

KVKK gerçekten Raw IP saklamaya izin veriyor mu?

Evet, m.5/2-(f) veri sorumlusunun meşru menfaatleri için veri işlenmesinin zorunlu olması. Sistem güvenliği (saldırı denemelerini bloklamak) meşru menfaat. Aydınlatma metni Privacy.tr.resx 17 bölümde detaylı. 90 gün retention sonra DataRetentionCleanupHostedService ile otomatik silinme + KVKK başvuru iletişim kanalı.

Prometheus + Grafana lisansları K8s on-prem'de nasıl maliyet?

prometheus-net MIT lisans, ücretsiz. Prometheus + Grafana OSS ücretsiz. K8s pod overhead: Prometheus ~500MB RAM (15s scrape interval, 30 gün retention), Grafana ~200MB. Toplam $0 lisans + ~$5/ay K8s node payı. Alternatif Grafana Cloud free tier 10K series, $19/ay 50K series.

IsSuspectNetwork flag false-positive riski yaratmıyor mu?

42-ASN listesi dar tutuldu: sadece datacenter/cloud/VPN provider'lar. ISP'ler (Turkcell 16135, TTNet 9121, Vodafone TR 15897, Bharti Airtel IN 45609) dışlanır. Google ASN 15169 de dışlanır (Googlebot LegitBotUaPatterns'da; kalan = Google Cloud-hosted gerçek kullanıcılar). 24 saatlik gözlemde 0 false positive. Yeni ISP eklemeleri için MaxMind ASN DB haftalık güncellenmeli.

Yorumlar (0)

Yorum ve puan bırakın

Henüz yorum yapılmamış. İlk yorumu sen bırak.