"DKIM anahtarını yeniledim, DNS'i üç kez kontrol ettim, hâlâ dkim=fail." Bu cümleyi kuran herkes neredeyse her zaman yanlış yeri kazıyor. Çünkü DKIM tek bir doğrulama değil, iki bağımsız doğrulamadır ve bunların hangisinin koptuğunu bilmeden anahtar döndürmek, motoru çalışmayan arabanın lastiklerini değiştirmeye benzer.
Vakaların büyük çoğunluğunda anahtar gayet iyidir. Kopan şey body hash'tir: imzayı atan sunucunun gördüğü gövde ile alıcının aldığı gövde artık byte-byte aynı değildir. Aradaki bir sistem mesajı değiştirmiştir. Bu yazıda tahmini bırakıp deterministik teşhise geçiyoruz: hatayı doğru okumak, ham .eml üzerinden kanıt çıkarmak, bh='yi openssl ile elle yeniden hesaplamak ve suçluyu Received zincirinde yakalamak.
Önce hatayı doğru oku: bh= mi, b= mi koptu?
RFC 6376'ya göre DKIM doğrulaması iki kapıdan geçer. Birinci kapı gövdeyi canonicalize edip SHA-256 ile hash'ler ve sonucu imzadaki bh= değeriyle karşılaştırır. İkinci kapı h= listesindeki başlıkları artı DKIM-Signature başlığının kendisini canonicalize edip, DNS'ten çekilen public key ile b= imzasını doğrular. İkisi de geçerse dkim=pass. Kritik nokta: bu iki kapı bağımsız fail eder ve ikisinin fail nedeni tamamen farklı yerlere işaret eder.
Bu yüzden Authentication-Results başlığında sadece dkim=fail görmek yetmez. Asıl bilgi reason alanındadır:
Authentication-Results: mx.example.com;
dkim=fail reason="body hash did not verify"
header.d=gonderen.com header.s=s1 header.b=Ab3dEf==reason="body hash did not verify" → Kapı 1 koptu, gövde değişti. Anahtara dokunma. reason="signature did not verify" → Kapı 2 koptu, sorun anahtar, selector ya da imzalanan başlıklardadır. Gmail'de "Show original", Outlook'ta ham başlıklar, ya da kendi MTA'nızda rspamd/OpenDKIM logları bu satırı verir. Reason'ı okumadan atılan her adım kördür.
Kanıtı ham mesajdan çıkar: .eml'i alıcıdan iste
En sık yapılan hata: imzayı atan sunucunun outbound kopyasıyla debug etmek. O kopya her zaman geçerli görünür, çünkü bozulma transit'te olur. Alıcının aldığı ham mesajı isteyin — Gmail'de "Show original → Download original", Outlook'ta mesajı .eml olarak kaydet.
Elinizde ham mesaj varken DKIM-Signature başlığının her alanını okuyun:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=gonderen.com; s=s1; t=1719900000;
h=from:to:subject:date:message-id;
bh=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=;
b=Ab3dEf4Gh...==- v= sürüm, her zaman 1.
- a= algoritma:
rsa-sha256ya daed25519-sha256. - c= canonicalization çifti:
header/body. - d= ve s= imzalayan domain ve selector — DNS sorgusunda bunları kullanacağız.
- h= imzalanan başlıklar. Buradaki bir başlık transit'te değişirse Kapı 2 kırılır.
fromburada zorunludur (RFC 6376 §5.4). - bh= gövdenin base64 SHA-256'sı. Teşhisin merkezi bu.
- b= asıl imza.
- l=, t=
Vakayı burada donduruyoruz: bh= imzalayanın iddiasıdır. Şimdi gerçeği hesaplayacağız.
Canonicalization: simple mi relaxed mi, ve neden her şeyi değiştirir
Canonicalization, hash'lemeden önce mesajı normalize etme kuralıdır. c=simple/simple ile c=relaxed/relaxed arasındaki fark, imzanın transit'te hayatta kalıp kalmayacağını belirler.
Simple body neredeyse sıfır tolerans tanır. Sadece gövdenin sonundaki fazla boş satırları kırpar. Satır içinde tek bir boşluğun sekmeye dönmesi, iki boşluğun bire inmesi — hepsi bh='yi anında kırar. Gerçek dünyada hiçbir mail geçidi bu kadar temiz davranmaz.
Relaxed body çok daha dayanıklıdır: satır içindeki ardışık boşlukları (WSP) tek boşluğa indirir, satır sonundaki boşlukları siler, gövdenin sonundaki boş satırları atar. Yani boşluk düzeyinde oynayan ama satır yapısını koruyan sistemlere karşı biraz nefes payı bırakır.
Relaxed header ise başlık adını küçük harfe çevirir, katlanmış (folded) başlıkları tek satıra düzleştirir ve iki nokta etrafındaki boşlukları normalize eder.
Pratik tavsiye tek cümle: her zaman `relaxed/relaxed` imzalayın. simple, footer ekleyen, encoding değiştiren, başlık katlayan gerçek dünya transit'inin hiçbirinde hayatta kalmaz. OpenDKIM'de Canonicalization relaxed/relaxed, rspamd'de varsayılan zaten budur.
bh='yi elle yeniden hesapla — asıl teşhis adımı
İşin özü burada. bh='yi kendiniz üretip imzadakiyle karşılaştırdığınızda teşhis tahminden çıkıp deterministik olur.
Ham .eml'den gövdeyi ayıklayın — gövde, ilk boş satırdan sonraki her şeydir. Kritik detay: gövde CRLF (`\r\n`) satır sonlarıyla olmalı, aksi halde hash asla tutmaz. relaxed gövdeyi hazırladıktan sonra:
# 1) Gövdeyi çıkar (ilk boş satırdan sonrası), satır sonlarını CRLF'e zorla
sed '1,/^\r\{0,1\}$/d' alici.eml | sed 's/\r*$/\r/' > body.raw
# 2) relaxed kuralı: satır-içi ardışık WSP -> tek boşluk, satır sonu WSP sil
# (basit hâli; tam kural için opendkim-testmsg / rspamadm tercih edin)
# 3) SHA-256 + base64 = hesaplanan bh
openssl dgst -sha256 -binary body.raw | openssl base64Elle canonicalization hataya açık olduğu için üretimde opendkim-testmsg < alici.eml ya da rspamd'nin rspamadm aracını kullanmak daha güvenli. Ama mantık aynı:
- Hesaplanan değer imzadaki
bh=ile aynıysa → gövde el değmemiş. Sorun Kapı 2'de: anahtar, selector ya dah=başlıklarından biri değişmiş. - Farklıysa → gövde transit'te değişti. Suçlu aradaki bir sunucu. Received zincirine geçin.
Suçlu genelde aradaki sunucu — "the man in the middle"
Gövde değiştiyse, mesajın gönderen MTA ile alıcı arasında dokunduğu her hop şüphelidir. En sık görülen senaryolar:
- 1.Mailing list yöneticileri (Mailman, Google Groups):
Subject'e[liste-adı]etiketi ve gövdeye unsubscribe footer'ı ekler. Hemh='dekiSubject(Kapı 2) hem gövde (Kapı 1) kırılır. Çözüm liste tarafında From rewrite + ARC. - 2.Kurumsal güvenlik geçitleri (Proofpoint, Mimecast, Barracuda): "EXTERNAL SENDER" banner'ı ya da tarama footer'ı enjekte eder. Gövde büyür,
bh=gider. - 3.Exchange transport rule / disclaimer: yasal footer ekler — klasik
bh=katili. - 4.Encoding yeniden kodlaması:
Content-Transfer-Encoding: 8bit→quoted-printabledönüşümü gövdenin her byte'ını değiştirir. Görünüşte aynı metin, tamamen farklı hash. - 5.
Suçluyu bulma yöntemi: Received başlıklarını en üstten (en son hop) aşağı okuyun. Boyutun ya da Content-Transfer-Encoding'in hangi hop'tan sonra değiştiğini izleyin. O hop sizin adamınızdır.
l= tuzağı ve klasik başlık hataları
l= etiketi gövdenin yalnızca ilk N byte'ını imzalar. Footer ekleyen sistemlere karşı bir kaçış yolu gibi görünür — "footer imzasız kalır, bh= tutar" mantığı. Yapmayın. l= bir güvenlik açığıdır: saldırgan imzasız bölgeye içerik ekleyebilir ve modern doğrulayıcıların birçoğu l= içeren imzalara giderek daha az güvenir. Footer sorununun doğru çözümü l= değil, ya imzayı footer eklendikten sonra atmak ya da ARC'dir.
Diğer sık hatalar: From'un h= listesinde olmaması (RFC ihlali, çoğu doğrulayıcı otomatik fail verir); saat kayması yüzünden t= gelecekte ya da x= geçmişte kalması. Oversigning tekniği doğrudur — bir başlığı h='de iki kez listelemek (subject:subject), sonradan eklenen sahte kopya başlığa karşı korur — ama yanlış uygulanınca meşru mesajlarda fail üretebilir.
Kendi tarafında doğrula: selector, DNS ve anahtar
Kapı 2 koptuysa (yani bh= tutuyor ama imza tutmuyor) sıra kendi altyapınızda. Yayınlanan public key'i çekin:
dig +short s1._domainkey.gonderen.com TXT
# v=DKIM1; k=rsa; p=MIIBIjANBgkqh...QAB- `p=` boşsa anahtar iptal edilmiş (revoked) demektir — doğrulama başarısız olur.
- Selector uyuşmazlığı:
DKIM-Signature'dakis=ile DNS'te yayınlanan selector aynı mı? Rotasyon sonrası eski selector'la imzalayıp yeniyi yayınlamak klasik hatadır. - 255-byte tuzağı: 2048-bit anahtar tek TXT string'ine sığmaz, birden fazla parçaya bölünür (
"...abc" "def..."). DNS bunları birleştirir; parçaları yanlış tırnaklamak ya da araya boşluk koymak"signature did not verify"üretir.
Anahtarın kendisi sağlam mı diye bakmak için:
P="MIIBIjANBgkqh...QAB"
echo -n "$P" | base64 -d | openssl rsa -pubin -inform DER -text -nooutOpenDKIM tarafında kontrol edilecek anahtarlar: Canonicalization relaxed/relaxed, SignHeaders (oversigning listesi), Selector, KeyFile. Postfix'e milter olarak bağlanır. Uçtan uca test için mesajı [email protected] benzeri bir yansıtıcıya, dkimvalidator.com'a ya da mail-tester.com'a gönderin; learndmarc.com ise akışı interaktif gösterir.
DMARC bağı: pass yetmez, alignment şart
DKIM'in pass olması DMARC için tek başına yeterli değildir. d= domaini From: başlığındaki domain ile alignment içinde olmalı. Relaxed alignment organizational domain eşleşmesi ister (mail.gonderen.com ≈ gonderen.com), strict tam eşleşme. Bu yüzden liste ya da geçit From'u yeniden yazdığında DKIM teknik olarak pass olsa bile DMARC fail olabilir — ve tam bu noktada ARC (RFC 8617) devreye girer: meşru değiştirici, orijinal doğrulama sonucunu zincirleyerek taşır, böylece alıcı DKIM kırılmış olsa da geçmişteki "pass"i güvenilir biçimde görebilir.
Teşhis checklist'i
- 1.
Authentication-Resultsreason'ını oku:body hash did not verify(Kapı 1) mı,signature did not verify(Kapı 2) mi. - 2.Debug'ı alıcının ham
.eml'i üzerinden yap, kendi outbound kopyanla değil. - 3.
bh='yiopenssl dgst -sha256 -binary | openssl base64ile elle hesapla; gövdeyi CRLF'e zorla. - 4.Farklıysa
Receivedzincirini takip et;Content-Transfer-Encoding/boyut hangi hop'ta değişti, suçlu o hop. - 5.
c=relaxed/relaxed'e geç,l='yi tamamen kaldır,From
Bu sırayı takip ederseniz, "anahtarı yeniledim hâlâ fail" döngüsünden çıkar ve her vakada tam olarak hangi kapının, hangi hop'ta koptuğunu byte düzeyinde kanıtlarsınız.


