Yanlış Pozitifleri Bitirmek: Allowlist, Skor Eşikleri ve Silmek Yerine Karantina
Spam'i sessizce silen bir sistem, en pahalı hatayı görünmez şekilde yapar. Kaybolan bir fatura, sipariş onayı ya da 2FA kodu geri gelmez. Bu yazı DELETE fiilini mühendislik sözlüğünden çıkarıyor: çok katmanlı allowlist, tek eşik yerine skor bölgeleri ve kullanıcının kendi kurtarabildiği karantina pipeline'ı.
EvilMail Team25 Temmuz 202611 dk okuma
Bir kullanıcı "PayPal ödeme onayım gelmedi" diye yazıyor. Logları açıyorsun: mesaj Rspamd tarafından 6.8 skorla reject edilmiş. Geldiği subdomain p=none politikalı bir DMARC ile imzalı, alignment tutmamış. Asıl sorun senin config'inde — reject eşiğini 6'ya çekmişsin. Yani belirsizlik bandının tam ortasındaki bir mesaj, header alıp Junk'a düşmek yerine 5xx yedi. Gönderici MTA bir bounce üretti, kullanıcı o bounce'ı hiç görmedi. Mesaj artık hiçbir yerde yok.
Bu, spam filtresinin yapabileceği en pahalı hatadır ve tamamen sessizdir. Spam'i inbox'a kaçırmak kullanıcıyı gıcık eder — silmesi bir tık sürer. Meşru bir maili yok etmek ise güven kaybettirir ve kimse fark etmez. Bu iki hatanın maliyeti simetrik değil, o yüzden filtreni simetrik ayarlamak baştan yanlış.
Yanlış pozitifin gerçek maliyeti: neden simetrik düşünmek yanlış
False negative (spam'i geçirmek) geri döndürülebilir bir olaydır. Kullanıcı görür, siler, Bayes'e öğretirsin, bir daha olmaz. False positive (meşru maili engellemek) geri döndürülemez ve genelde görünmezdir. Özellikle transactional mail sınıflarında — fatura, şifre sıfırlama, 2FA kodu, sipariş onayı — tolerans sıfıra yakın olmalı. Bir kullanıcının 2FA kodu Junk'a bile düşse tartışılır; reject edilmesi kabul edilemez.
Yanlış Pozitifleri Azaltma: Allowlist, Skor Eşikleri ve Karantina Stratejisi — EvilMail Blog
Sayıya dökelim. evilmail.pro gibi yüksek hacimli, karışık ve otomatik trafiğin baskın olduğu bir altyapıda günde 100.000 mesaj işlediğini varsay. %0.1'lik bir yanlış pozitif oranı — ki bu "iyi" sayılır — günde
100 kayıp meşru mail
demektir. Bunların onlarcası transactional'dır. Hafta sonu bittiğinde 700 kişi "mailim gelmedi" durumundadır ve sen nedenini loglardan tek tek kazımak zorundasın.
"Ama reject edince gönderici bounce alır, tekrar dener" efsanesini de gömelim. Otomatik gönderici sistemlerinin büyük çoğunluğu bounce'ı okumaz; bir kuyruğa yazar ve unutur. SaaS'ların çoğu "sent" statüsünü SMTP 250'de işaretler, 5xx'i asenkron bir webhook'a bırakır ki kimse izlemez. Yani senin reject kararın, karşı tarafta çoğu zaman hiçbir retry tetiklemez. Mesaj gerçekten ölür.
Buradan çıkan tek mühendislik prensibi şu: hiçbir mesaj yok edilmemeli. Her mesaj ya teslim edilir, ya etiketlenir, ya karantinaya alınır. DELETE fiilini pipeline'dan çıkar.
Eşik mimarisi: tek sayı yerine bölgeler
SpamAssassin'in required_score 5.0'ı ya da "spam eşiği" kavramı, tek bir sayının altını "temiz", üstünü "çöp" sayan ikili bir dünya görüşüdür. Gerçek trafik böyle davranmaz. Skor bir olasılık gradyanıdır ve her bandın kendi aksiyonu olmalı.
Rspamd bunu zaten aksiyon (action) modeliyle destekler. Ama default'ları — greylist 4, add_header 6, reject 15 — genel bir MX içindir. Temp-email ve karışık transactional trafik için reject'i pratikte yukarı iter, yerine geniş bir karantina bölgesi koyarsın.
Fikir şu: 6 ile 15 arasındaki geniş "belirsizlik" bandı, bir mesajın silinmesi gereken değil, kullanıcının gözünden uzak ama erişilebilir bir yere konması gereken bölgedir. reject yalnızca skorun anlamsız yükseğe (>15) çıktığı ya da symbol tabanlı kesin kuralların (bilinen malware imzası, CLAM_VIRUS) tetiklendiği durumlar içindir.
actions.conf yalnızca skor eşiklerini yönetir. "Kesin reject" kararlarını skora bırakma; onları /etc/rspamd/local.d/force_actions.conf içinde tek tek symbol'e bağla — böylece CLAM_VIRUS gibi bir symbol tetiklendiğinde skor ne olursa olsun reject olur, ama sıradan yüksek skor reject'e ulaşamaz.
Skor 8-15 bandındaki rewrite_subject aksiyonunu aşağıdaki karantina pipeline'ı ile birleştirdiğinde, mesaj asla kaybolmaz — sadece kullanıcının inbox'ından uzaklaşır.
Allowlist'i doğru kur: üç ayrı düzlem
"Beyaz liste" kelimesi tehlikeli bir sadeleştirmedir. Bir domaini ya da From adresini kaba bir güven anahtarıyla allowlist'e atmak, tam da o domaini spoof'layan saldırgana kapı açar. Allowlist bir güven anahtarı değil, dar kapsamlı bir istisna mekanizmasıdır ve üç ayrı düzlemde kurulmalıdır.
1. Authentication'a bağlı allowlist (DKIM). Envelope-from ya da header From'a asla güvenme — ikisi de imzasızdır ve spoof edilebilir. Yalnızca DKIM imzası VALID olan ve imzalayan d= domaini map'te yer alan mesajların skorunu düşür.
2. IP/ASN allowlist. Postmark, SES, Mailgun gibi kritik transactional göndericilerin yayınlanmış IP bloklarını ayrı bir düzlemde tut. Bu, DKIM'e ek bir katmandır, onun yerine geçmez.
3. Alıcı-tarafı per-user allowlist. Kullanıcının kendi adres defteri. Bu düzlem globali değil, yalnızca o kullanıcının teslimatını etkiler.
/etc/rspamd/local.d/multimap.conf:
ucl
WHITELIST_DKIM {
type = "dkim";
map = "/etc/rspamd/maps.d/dkim_whitelist.map";
score = -8.0;
# sadece imza VALID + d= domaini map'te ise puan düşürür
}
WHITELIST_IP {
type = "ip";
map = "/etc/rspamd/maps.d/ip_whitelist.map";
score = -5.0;
}
/etc/rspamd/maps.d/dkim_whitelist.map içine yalnızca domainleri yazarsın:
paypal.com
stripe.com
sendgrid.net
Kritik nokta: WHITELIST_DKIM tipi dkim olduğu için Rspamd, map eşleşmesini yalnızca imza doğrulandıktan sonra uygular. paypal.com'u spoof'layan biri geçerli bir DKIM imzası üretemeyeceği için allowlist'ten faydalanamaz.
SpamAssassin tarafında karşılığı nettir ve hangisini kullanmaman gerektiği de bellidir:
# YANLIŞ — imzasız, spoof edilebilir:
# whitelist_from *@paypal.com
# DOĞRU — DKIM doğrulamalı:
whitelist_from_dkim [email protected] paypal.com
# ya da genel auth (SPF/DKIM) tabanlı:
whitelist_auth *@stripe.com
whitelist_from'u aklından çıkar. whitelist_from_dkim ve whitelist_auth doğru araçlardır.
Sil değil, ayır: karantina ve etiketleme pipeline'ı
Şimdi asıl mesele: 6-15 bandındaki mesajları nereye koyacağın. İki uygulanabilir yol var, ikisi de DELETE içermez.
Yol A — Rspamd header + Sieve fileinto. Rspamd milter, add_header bölgesindeki mesajlara X-Spam: Yes, X-Spam-Score ve X-Spamd-Result header'larını ekler. Dovecot Pigeonhole tarafında Sieve bu header'ı görüp mesajı Junk klasörüne yönlendirir. Kullanıcı IMAP üzerinden Junk'ı görür, kurtarabilir.
Yol B — Merkezî karantina deposu. 8-15 bandındaki, daha "kuvvetli spam" gibi görünen ama yine de yok edilmemesi gereken mesajlar için ayrı bir mailbox ya da amavisd quarantine dizini kullan. Bunlar kullanıcının Junk'ında görünmez ama bir admin ya da self-service arayüzünden release edilebilir. Retention politikası standart olarak 30 gün — bu süre boyunca hiçbir mesaj kalıcı silinmez.
reject bölgesi için bile prensip aynı kalır: 5xx dönmek, gönderici MTA'ya "kabul etmedim" demektir, "yok et" demek değil. Karşı taraftaki kopyayı sen yok etmemiş olursun. Kendi tarafında kalıcı bir kopya tutman gereken durumlarda reject yerine yüksek karantina bandını tercih et.
Geri besleme döngüsü: yanlış pozitifi ölçülebilir yap
Bir filtre kör uçamaz. Bayes sınıflandırıcının sürekli beslenmesi ve yanlış pozitif oranının loglardan çıkarılması gerekir.
Kullanıcı Junk'taki bir mesajı "bu spam değil" diyerek inbox'a taşıdığında ne olmalı? Cevap allowlist'e eklemek değildir — o domaini kalıcı olarak beyazlatmak spoofing riskini büyütür. Doğrusu, o mesajı ham olarak öğretmektir:
Bunu elle yapmazsın. Dovecot imapsieve ile Junk klasöründeki taşıma olaylarını yakalayıp otomatik tetiklersin. Junk'a KOPYALANAN mesaj learn_spam, Junk'tan ÇIKARILAN mesaj learn_ham çağırır:
Buradaki kritik ayrım: ikinci kural mailbox2_name = * + mailbox2_from = Junk ile "Junk'tan çıkışı" yakalar. İki kuralı da aynı klasöre bağlarsan ham öğrenmesi hiç tetiklenmez.
FP oranını ölçmek için Rspamd geçmişini kullan:
bash
rspamc stat # genel sınıflandırma istatistikleri, Bayes durumu
rspamc history # son mesajların skor/aksiyon dökümü
Pratik FP metriği: Junk'tan ham'a taşınan mesaj sayısı / toplam işlenen mesaj. Bu oranı haftalık bir audit sorgusuyla çıkar ve eşiklerini veriyle ayarla. Hedef: FP < %0.05 (10.000 mesajda 5'ten az). Bunun üstüne çıkıyorsan add_header eşiğin çok düşük ya da bir kural fazla agresif demektir — rspamc history'den hangi symbol'ün tetiklediğine bak, kör bir eşik oynaması yapma.
DMARC edge-case'lerini ayrıca ele al. p=none yayınlayan ana domainlerden gelen ve sp= politikası tanımsız subdomain'ler, meşru transactional mailde en sık FP kaynağıdır. Bu göndericileri kör bir skor cezasıyla değil, subdomain'e özel DKIM alignment kontrolüyle değerlendir.
Pratik kontrol listesi
reject bölgesini yalnızca çok yüksek skora (>15) ya da kesin symbol kurallarına (bilinen malware imzası) bağla — genel skora değil.
Transactional gönderici IP/ASN'lerini authenticated allowlist'e al; envelope-from ve header From'a asla güvenme.
Her "spam" mesajı IMAP Junk'a ya da karantina deposuna gitsin — pipeline'da DELETE yok.
Junk/karantina retention 30 gün + kullanıcının kendi release erişimi olsun.
Bayes'i günlük besle; Junk↔ham geri beslemesini imapsieve ile otomatikleştir.
Allowlist puan düşürmesini yalnızca DKIM-valid + d= domain eşleşmesiyle sınırla; whitelist_from/imzasız allowlist kullanma.
Haftalık FP audit sorgusu çalıştır (rspamc history + Junk→ham sayacı), eşikleri veriyle oynat, hedef FP < %0.05.
p=none ve subdomain DMARC alignment edge-case'lerini ayrı kural setiyle ele al, kör skor cezası verme.