수신 서버가 실제로 검사하는 것

DKIM 서명은 발신 메일 서버가 붙이는 헤더입니다.

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=s1;
        h=from:to:subject:date:message-id:mime-version:content-type;
        bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
        b=Kx3mP9qa...

수신 서버는 서로 다른 두 가지를 이 순서로 검사합니다.

  1. 본문 해시. 받은 본문을 정규화(canonicalization, 방식은 c= 태그가 정함)한 뒤 해시를 계산해 bh=와 비교합니다. 이 단계에는 키가 필요 없습니다.
  2. 서명. h=에 나열된 헤더와, b=를 비운 DKIM-Signature 헤더 자체를 해시하고, DNS의 s1._domainkey.example.com(셀렉터, _domainkey, d= 도메인 순)에서 가져온 공개키로 그 해시를 b=와 대조합니다.

이 순서가 이 주제에서 가장 쓸모 있는 사실입니다. 본문 해시가 실패했다면 서명 이후 본문이 바뀐 것이고, 키와는 관계가 없습니다. 본문 해시는 맞는데 서명이 실패했다면 서명된 헤더가 바뀌었거나, DNS의 공개키가 서명에 쓴 개인키와 짝이 맞지 않는 것입니다. 문제도 다르고, 연락해야 할 사람도 다릅니다.

DKIM은 메일이 원하는 메일인지, 안전한지 판단하지 않습니다. “이 바이트들은 d= 도메인의 주인이 서명한 그대로다”라는 것만 말합니다.

1단계: 관리 화면이 아니라 결과 헤더를 읽기

받은 메일의 전체 헤더를 엽니다. Gmail은 “원본 보기”(같은 화면에 “원본 다운로드”도 있습니다), Outlook은 메시지 원본 또는 메시지 세부 정보 보기입니다. 수신 시스템이 쓴 Authentication-Results 헤더를 찾으세요.

Gmail은 body hash 불일치를 이렇게 표시합니다.

Authentication-Results: mx.google.com;
       dkim=neutral (body hash did not verify) header.i=@example.com header.s=s1 header.b=Kx3mP9qa;
       spf=pass (google.com: domain of bounce@example.com designates 203.0.113.10 as permitted sender) smtp.mailfrom=bounce@example.com;
       dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=example.com

Gmail은 이것을 fail이 아니라 neutral로 부릅니다. 그리고 DMARC는 통과했습니다. SPF가 통과하고 정렬됐기 때문입니다. 이 메일은 정상적으로 배달됐습니다. 그런데 같은 메일이 한 번 전달(forwarding)되면 SPF도 깨지고, 그때 DMARC가 실패합니다. 깨진 DKIM 서명은 메일이 전달되는 날까지 눈에 띄지 않는 경우가 많습니다.

Microsoft 365와 Outlook.com은 괄호 안에 이유를 적습니다. 대략 이런 모양입니다.

Authentication-Results: spf=pass (sender IP is 203.0.113.10) smtp.mailfrom=example.com;
 dkim=fail (body hash did not verify) header.d=example.com;
 dmarc=pass action=none header.from=example.com;compauth=pass reason=100

그 밖에 signature did not verify, no key for signature, message not signed, OpenDKIM 기반 시스템의 reason="signature verification failed" 같은 표현도 보게 됩니다.

DKIM 결과값과 각각이 가리키는 곳은 다음과 같습니다.

결과뜻볼 곳
pass서명이 검증됨DMARC가 그래도 실패한다면 정렬
fail서명을 검사했지만 검증되지 않음본문 변경, 헤더 변경, 키 불일치
neutral처리할 수 없었음, 또는 (Gmail) body hash 불일치fail과 같음
none메일에 DKIM 서명이 없음그 발신 시스템에서 서명이 꺼져 있음
temperror일시적 문제, 대개 키를 가져오다 DNS 시간 초과DNS 호스팅의 안정성
permerror영구적 문제: 키 없음, 키 레코드 해석 불가, 서명 형식 오류_domainkey 레코드

헤더를 읽을 때 빠지기 쉬운 함정이 세 가지 있습니다.

  • 내 수신 시스템이 쓴 줄만 믿으세요. 보내는 사람은 누구나 메일에 Authentication-Results 헤더를 넣을 수 있습니다. 의미가 있는 것은 맨 위에 있는, 내 수신 시스템 이름(mx.google.com, 우리 게이트웨이 호스트명 등)이 붙은 줄입니다.
  • 서명이 여러 개일 수 있습니다. 메일 발송 서비스로 보낸 메일에는 업체 서명과 내 서명이 함께 붙어 있는 경우가 많습니다. dkim=pass 옆에 dkim=fail이 하나 있어도, 통과한 쪽의 d=가 내 도메인이라면 문제가 아닙니다.
  • dkim=none은 깨진 서명이 아닙니다. 발신 시스템이 아예 서명하지 않았다는 뜻입니다. 본문 변경을 쫓아도 소용없고, 그 시스템에서 서명을 켜야 합니다.

이메일 헤더 분석은 Authentication-Results에서 SPF·DKIM·DMARC 판정을 뽑아 보여 주고, Received 홉을 홉 사이 지연 시간과 함께 늘어놓습니다. 경로에 리스트 서버나 게이트웨이가 끼어 있었는지 보는 데 도움이 됩니다. 수신 서버의 판정을 보여 주는 것이지, 서명을 직접 다시 검증하지는 않습니다.

2단계: 직접 검증하기

원본 메일을 파일로 내려받습니다(Gmail: “원본 다운로드”). 웹 화면에서 텍스트를 복사하면 공백과 줄바꿈이 바뀌어 멀쩡한 서명도 실패로 나옵니다.

Python dkimpy 패키지를 쓰면 됩니다.

pip install dkimpy
dkimverify < message.eml

내가 가진 바이트 기준으로 서명이 검증되는지 알려 줍니다. 서명이 여러 개 붙은 메일이라면 어느 서명을 검사했는지 확인하세요.

다음은 수신 서버가 가져갈 키를 확인합니다. 셀렉터는 s=에서, 도메인은 d=에서 가져옵니다.

dig +short TXT s1._domainkey.example.com
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvb2y..." "...QIDAQAB"

한 응답에 따옴표로 묶인 문자열이 두 개 보이는 것은 정상입니다. 2048비트 키는 TXT 문자열 하나의 한도인 255자보다 길어서 나뉘고, 수신 서버가 다시 이어 붙입니다. 반면 같은 이름에 TXT 레코드가 두 개 있는 것은 비정상입니다(아래에서 다룹니다).

키 길이를 보려면 p= 값을 디코딩합니다.

dig +short TXT s1._domainkey.example.com | tr -d '" \n' | sed 's/.*p=//; s/;.*//' > key.b64
{ echo '-----BEGIN PUBLIC KEY-----'; fold -w 64 key.b64; echo; echo '-----END PUBLIC KEY-----'; } \
  | openssl pkey -pubin -noout -text | head -1
Public-Key: (2048 bit)

OpenSSL이 읽지 못하면 레코드가 망가진 것입니다. 눈으로 빠르게 보는 방법도 있습니다. 이 형식의 2048비트 RSA 키는 MIIBIjANBgkq로, 1024비트 키는 MIGfMA0GCSq로 시작합니다.

OpenDKIM을 쓴다면 디스크의 개인키와 DNS의 공개키를 대조합니다.

opendkim-testkey -d example.com -s s1 -k /etc/opendkim/keys/example.com/s1.private -vvv

key OK가 나오면 짝이 맞습니다. 중간에 나오는 key not secure는 영역에 DNSSEC 서명이 없다는 뜻일 뿐 오류가 아닙니다.

레코드를 바꾼 뒤에는 리졸버 여러 곳에서 확인하세요(dig @1.1.1.1 …, dig @8.8.8.8 …). DNS 조회는 바깥에서 보이는 TXT 레코드를 있는 그대로 보여 줍니다.

“body hash did not verify”: 누군가 본문을 고쳤다

가장 흔한 DKIM 실패이고, 이때 키는 절대 원인이 아닙니다. 서명과 배달 사이에서 본문을 건드린 것을 찾으세요.

서명 뒤에 붙는 꼬리말이나 면책 문구. 전형적인 구성은 이렇습니다. 메일 서버가 서명하고, 그다음 발신 보안 게이트웨이나 전송 규칙이 법적 고지문, “보낸 곳” 문구, 추적 픽셀을 덧붙입니다. 각각은 멀쩡하게 동작하지만 둘을 합치면 반드시 실패합니다. 해결책은 순서입니다. 서명하는 쪽이 네트워크를 떠나기 직전의 마지막 단계여야 합니다. 꼬리말을 게이트웨이가 붙인다면 서명도 게이트웨이가(또는 게이트웨이 다음의 메일 서비스가) 해야 합니다.

메일링 리스트와 포워딩. 리스트는 수신 거부 꼬리말을 붙이고 본문을 고치기도 합니다. 단순 전달은 보통 본문을 건드리지 않지만, 메시지를 다시 인코딩하는 전달 서비스도 있습니다. 이것은 발신 쪽에서 고칠 수 없습니다. 잘 만든 리스트는 메시지를 손대지 않거나, From:을 리스트 주소로 바꾸고 리스트 도메인으로 다시 서명합니다. 많은 리스트가 ARC 헤더도 붙여서, 최종 수신 서버가 리스트를 거치기 전에는 DKIM이 통과했다는 것을 볼 수 있게 합니다. ARC를 믿을지는 수신 서버가 정합니다.

전송 중 인코딩 변환. 중간 릴레이가 8비트 내용을 받지 않으면 본문을 quoted-printable이나 base64로 바꿀 수 있습니다. 읽어 보면 같은 글이지만 바이트가 달라서 해시도 달라집니다. SMTP의 줄 길이 한도인 998자를 넘는 줄이 다시 감기는 것도 마찬가지입니다. 서명 전에 이미 7비트로 안전한 형태로 만드세요(비ASCII 본문은 서명 전에 quoted-printable이나 base64로 인코딩하도록 메일 프로그램을 설정). 생성하는 HTML의 줄도 짧게 유지하세요. 한 줄짜리 거대한 HTML 대량 메일이 단골 원인입니다.

깨지기 쉬운 정규화. 본문을 c=simple로 서명하면 줄 끝 공백 하나 바뀌어도 실패합니다. relaxed는 줄 안과 줄 끝의 공백 변화를 허용합니다. 특별한 이유가 없다면 c=relaxed/relaxed를 쓰세요. 대부분의 플랫폼이 기본으로 이렇게 합니다.

꼬리말 문제를 “해결”하려고 l= 태그에 손대지는 마세요. 본문의 앞 N바이트만 서명하므로 꼬리말이 붙어도 통과합니다. 그 말은 누구든 서명된 메일 뒤에 내용을 덧붙여도 통과한다는 뜻이기도 합니다. RFC 6376이 바로 이 점을 경고하고, l=가 붙은 서명을 의심스럽게 다루는 수신 서버도 있습니다.

“signature did not verify”: 헤더가 바뀌었거나 키가 맞지 않는다

본문은 그대로 도착했는데 두 번째 검사에서 실패한 경우입니다.

서명된 헤더가 바뀜. 리스트가 제목에 [리스트명]을 붙이거나, 게이트웨이가 [EXTERNAL] 태그를 붙이거나, 무언가가 From:의 표시 이름을 고친 경우입니다. h=에 있는 헤더는 모두 서명 대상입니다. 서명할 때 없던 헤더도 h=에 이름이 있으면 대상입니다. 없는 Reply-To를 서명해 두면 나중에 누가 그 헤더를 추가할 때 서명이 깨집니다. 일부러 하는 보호 장치이지만, 그런 헤더를 붙이는 리스트를 거치면 깨진다는 뜻이기도 합니다.

DNS의 키가 서명한 키가 아님. 주로 키 교체 무렵에 생깁니다.

  • 서버에서 개인키를 새로 만들었는데 DNS에는 예전 공개키가 그대로 있습니다.
  • DNS는 바꿨는데 두 번째 발신 서버(백업 MTA, 웹 서버의 로컬 메일러, 애플리케이션 컨테이너)가 같은 셀렉터로 예전 개인키를 써서 계속 서명합니다.
  • 예전 키로 서명한 메일이 아직 전송 중이거나 재시도 대기열에 있는데 같은 셀렉터 이름을 새 키에 재사용했습니다.

키는 셀렉터 두 개로 교체하세요. 새 키를 새 셀렉터로 게시하고, DNS가 퍼질 때까지 기다린 뒤 서명을 새 셀렉터로 바꾸고, 늦게 도착하는 메일도 검증되도록 예전 셀렉터 레코드는 최소 며칠 남겨 둡니다.

DNS 레코드가 망가짐. 반복해서 나오는 것들입니다.

  • 도메인이 두 번 붙음. 많은 DNS 관리 화면이 영역 이름을 자동으로 붙입니다. s1._domainkey.example.com을 입력하면 s1._domainkey.example.com.example.com이 만들어집니다. s1._domainkey만 입력하세요.
  • 문자열 두 개가 아니라 레코드 두 개로 나뉨. 키가 같은 이름의 TXT 레코드 두 개에 반씩 들어갑니다. 수신 서버는 반쪽짜리 키 두 개를 받습니다. 레코드 하나 안에 문자열 두 개로 들어가야 합니다.
  • 복사·붙여넣기 손상. p= 중간에 줄바꿈, 따옴표, 공백이 끼어들었거나, 긴 레코드를 제대로 다루지 못하는 관리 화면이 255자에서 잘라 버렸습니다.
  • CNAME 누락이나 오타. Microsoft 365와 대부분의 메일 발송 서비스는 업체가 보관하는 키를 가리키는 CNAME(Microsoft는 selector1._domainkey, selector2._domainkey)을 넣게 합니다. CNAME이 없거나 오타가 있으면 키도 없습니다.

키가 아예 없을 때, 그 밖의 영구 오류

no key for signature, permerror, 키 조회 결과 NXDOMAIN은 모두 수신 서버가 셀렉터._domainkey.도메인에서 아무것도 찾지 못했다는 뜻입니다. 설정했다고 생각하는 셀렉터가 아니라, 실패한 메일의 s=에 적힌 셀렉터를 그대로 조회하세요. 업체가 대신 서명한다면 셀렉터도 업체 것입니다.

p=가 비어 있는 레코드(v=DKIM1; p=)는 키를 일부러 폐기했다는 표시입니다. 이것이 보인다면 어떤 시스템이 아직 그 셀렉터를 쓰는데 누군가 폐기한 것입니다.

서명에는 만료 시각(x=)을 넣을 수 있습니다. 재시도 대기열에서 그 시각을 넘긴 메일은 만료된 서명을 달고 도착해 실패합니다. x=를 쓴다면 몇 분이 아니라 며칠로 잡으세요.

temperror는 대개 수신 서버가 키를 가져오다 시간 초과가 난 것입니다. 가끔 한 번은 있을 수 있지만, 반복된다면 DNS 호스팅을 의심하세요.

Ed25519 서명(RFC 8463)도 유효한 DKIM이지만 수신 서버의 지원이 고르지 않습니다. Ed25519로 서명한다면 RSA 서명도 함께 붙이세요.

DKIM은 통과하는데 DMARC가 실패할 때

헤더에 dkim=pass와 dmarc=fail이 함께 보인다면 서명 자체는 멀쩡합니다. 다른 도메인으로 서명했을 뿐입니다. DMARC는 d=가 From: 도메인과 맞을 때만 DKIM pass를 인정합니다(strict 정렬이면 정확히 같아야 하고, 기본값인 relaxed 정렬이면 조직 도메인이 같으면 됩니다).

흔한 이유는 이렇습니다.

  • Google Workspace는 관리 콘솔에서 키를 만들어 게시하고 “인증 시작”을 누르기 전까지 gappssmtp.com 도메인으로 서명합니다.
  • Microsoft 365는 셀렉터 CNAME 두 개를 게시하고 내 도메인의 DKIM을 켜기 전까지 onmicrosoft.com 도메인으로 서명합니다.
  • 메일 발송 서비스는 대시보드에서 도메인 인증을 설정하지 않으면 업체 도메인으로 서명합니다.

이것을 고치는 방법과 DMARC를 none에서 reject로 언제 올릴지는 DMARC p=none·quarantine·reject 차이에서 다룹니다.

바깥에서 도메인의 DKIM 확인하기

DKIM 키는 목록으로 조회할 수 없습니다. 셀렉터를 알아야 합니다. 이메일 점검은 흔한 셀렉터 18개(google, default, k1, selector1, selector2, s1, s2, Proton Mail·Fastmail·Zoho용 셀렉터, 20230601 같은 Google의 날짜형 이름)를 시도합니다. 여기서 “감지 안 됨”은 그 이름들에 키가 없다는 뜻이지, 도메인이 서명하지 않는다는 뜻이 아닙니다. 쓰는 업체가 다른 셀렉터를 쓸 수 있으니, 실제 메일의 s=를 읽어 직접 조회하세요.

실패를 진단하는 것이 아니라 처음부터 설정해야 한다면 이메일 전달률 가이드부터 보세요. 도메인의 MX·SPF·DMARC를 DKIM과 함께 빠르게 확인하는 방법은 MX·SPF·DKIM·DMARC 레코드 확인법에 있습니다.

내 도메인에서 바로 확인

무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.