이 오류가 뜻하는 것
permerror는 SPF가 “당신의 레코드를 해석할 수 없으니 판정을 내리지 않겠다”고 말하는 결과입니다. 그중 “too many DNS lookups”는 가장 많이 겪는 원인이고, 전적으로 스스로 만든 문제입니다. 나 또는 나보다 먼저 발송 서비스를 추가한 사람들이, 평가하는 데 DNS 조회가 10번 넘게 필요한 레코드를 만들어 놓은 것입니다.
규칙은 RFC 7208 §4.6.4에 있습니다. 아래 항목들은 DNS 조회를 일으키고, SPF 평가 한 번에 합계 10회로 제한됩니다.
| 항목 | 세어지나 | 비고 |
|---|---|---|
include: | 예 | 불러온 레코드 안의 조회도 모두 합산 |
redirect= | 예 | 대상 레코드 안의 조회도 모두 합산 |
a, a:host | 예 | 하나당 1회 |
mx, mx:host | 예 | 하나당 1회. MX 호스트가 10개를 넘으면 그 자체로 실패 |
ptr | 예 | RFC가 아예 쓰지 말라고 합니다 |
exists: | 예 | 하나당 1회 |
ip4:, ip6: | 아니요 | DNS 조회가 필요 없음 |
all | 아니요 | |
exp= | 아니요 | 실패한 뒤 설명 문구를 만들 때만 조회 |
많이 놓치는 부분은 이 숫자가 재귀적으로 합산된다는 점입니다. 내 레코드에는 항목이 다섯 개뿐이어도, 그중 include 하나가 안에서 include를 세 개 더 부르면 조회는 다섯 번이 아니라 아홉 번입니다. 제한은 트리 전체에 걸립니다.
같은 절에 덜 알려진 두 번째 제한도 있습니다. void lookup입니다. 레코드가 하나도 없는 응답(NODATA)이나 존재하지 않는 이름(NXDOMAIN)을 받은 조회를 말하고, RFC는 수신 서버가 이것을 두 번까지만 허용하고 그 이상이면 permerror를 내도록 권고합니다. 더 이상 SPF를 게시하지 않는 옛 발송 서비스 도메인을 가리키는 include:, A 레코드를 지운 a:old-mail.example.com 같은 것이 이 한도를 갉아먹습니다. 또 include한 대상에 SPF 레코드가 아예 없으면 횟수와 상관없이 그것만으로 permerror입니다.
조회 수를 제대로 세기
먼저 게시된 레코드를 봅니다.
$ dig +short TXT example.com | grep -i spf1
"v=spf1 mx a include:_spf.google.com include:mailgun.org include:sendgrid.net include:servers.mcsv.net ip4:203.0.113.10 ~all"
조회 항목이 여섯 개로 보입니다. 실제 숫자를 알려면 모든 include와 redirect를 따라가야 합니다. 아래 셸 함수는 dig로 이를 따라가며 지나간 경로를 stderr에 찍습니다.
spfcount() {
local rec t n=0
rec=$(dig +short TXT "$1" | sed 's/" "//g; s/"//g' | grep -i '^v=spf1 ')
for t in $rec; do
t=$(printf '%s' "${t#[+~?-]}" | tr 'A-Z' 'a-z')
case "$t" in
include:*) echo "$1 -> $t" >&2; n=$((n + 1 + $(spfcount "${t#include:}"))) ;;
redirect=*) echo "$1 -> $t" >&2; n=$((n + 1 + $(spfcount "${t#redirect=}"))) ;;
a|a:*|a/*|mx|mx:*|mx/*|ptr|ptr:*|exists:*) echo "$1 -> $t" >&2; n=$((n + 1)) ;;
esac
done
echo "$n"
}
sed 부분이 중요합니다. 긴 TXT 레코드는 따옴표로 묶인 문자열 여러 개로 게시되는데, SPF는 이것을 공백 없이 이어 붙입니다. 위 예시에 돌려 보면 이렇습니다.
$ spfcount example.com
example.com -> mx
example.com -> a
example.com -> include:_spf.google.com
example.com -> include:mailgun.org
mailgun.org -> include:_spf.mailgun.org
_spf.mailgun.org -> include:_spf1.mailgun.org
_spf.mailgun.org -> include:_spf2.mailgun.org
mailgun.org -> include:_spf.eu.mailgun.org
example.com -> include:sendgrid.net
sendgrid.net -> include:ab.sendgrid.net
example.com -> include:servers.mcsv.net
11
11회입니다. 위 경로에 나온 발송 서비스 레코드는 2026년 10월 기준 실제 값입니다. include:mailgun.org 하나가 5회, include:sendgrid.net이 2회, Google과 Mailchimp의 include는 각각 1회입니다. 서비스 회사들은 이 레코드 구조를 예고 없이 바꿉니다. 작년에는 괜찮던 레코드가 아무도 손대지 않았는데 한도를 넘는 이유가 이것입니다. 포럼 글의 숫자(이 글 포함)를 믿지 말고 직접 다시 세세요.
이 함수는 일부러 단순하게 만들었습니다. 매크로(%{i} 등)를 펼치지 않고, 레코드에 all이 있으면 redirect=를 무시한다는 규칙도 반영하지 않습니다. 일반적인 레코드에서는 정식 SPF 라이브러리와 같은 답을 냅니다.
이메일 점검은 레코드를 읽어, 레코드 자체에 있는 조회 항목만으로 10을 넘으면 경고합니다. include 안까지 따라 들어가지는 않으므로, 7로 나온 레코드도 include를 펼치면 한도를 넘을 수 있습니다. 레코드 확인은 이메일 점검으로, 전체 합산은 위 함수로 하세요. 발송 서비스의 include 안에 무엇이 들었는지 보고 싶으면 DNS 조회로 해당 레코드를 게시된 그대로 볼 수 있습니다.
어떤 메일은 통과하고 어떤 메일은 실패하는 이유
대부분의 수신 서버는 SPF를 왼쪽부터 평가하다가 처음으로 일치하는 메커니즘에서 멈춥니다. 위 예시에서 203.0.113.10이 보낸 메일은 맨 끝의 ip4:에서야 일치하므로, 수신 서버는 그 앞의 include를 전부 거치다 도중에 한도에 걸립니다. 반면 Google 서버가 보낸 메일은 조회 3회 만에 _spf.google.com 안에서 일치해 통과할 수 있습니다.
그래서 “SPF permerror”는 일부 발송 경로에서만 나타나는 경우가 많습니다. 레코드 끝쪽에 있는 발송처, 그리고 모든 위조 메일입니다. 위조 메일은 어디에도 일치하지 않으니 항상 트리 전체를 돕니다. 트리 전체를 세는 점검 도구가 보여 주는 것이 이 최악의 경우이고, 중요한 것도 이 경우입니다.
그다음은 수신 서버에 달렸습니다. DMARC에서 SPF permerror는 SPF 실패로 취급되므로, From: 도메인과 정렬된 유효한 DKIM 서명이 있는 메일은 여전히 DMARC를 통과합니다. DKIM이 없거나 정렬되지 않은 메일은 이제 통과할 근거가 없습니다. 깨진 SPF 레코드 자체를 나쁜 신호로 보는 수신 서버도 있습니다.
10회 아래로 줄이는 방법
순서대로 진행하세요. 앞의 세 가지는 비용이 들지 않고, 대개 이것으로 충분합니다.
1. 더 이상 쓰지 않는 발송 서비스 제거
레코드에는 누군가 한 번 써 본 도구의 include가 계속 쌓입니다. 하나씩 보면서 누가 담당하는지, 지난달에 실제로 메일을 보냈는지 확인하세요. DMARC 집계 보고서가 바로 답을 줍니다. 한 달 치 보고서에 발송처로 한 번도 나오지 않는 서비스는 내 도메인으로 메일을 보내고 있지 않습니다. 그 include를 지우세요.
죽은 include는 쓸모없는 정도가 아니라 해롭습니다. 서비스가 그 호스트명을 폐기했다면 그 include는 이제 void lookup이거나 SPF 레코드가 없는 include이고, 둘 다 그것만으로 permerror를 낼 수 있습니다.
2. 실제로 발송하지 않는 a, mx, ptr 제거
a는 도메인의 A 레코드가 가리키는 서버, 보통 웹 서버를 허용합니다. 웹사이트가 메일을 직접 보내지 않는다면(문의 폼은 대개 메일 서비스나 API를 통해 보냅니다) a는 조회 1회를 낭비합니다.
mx는 수신 메일 서버를 허용합니다. Google Workspace나 Microsoft 365라면 그 서버는 제공자의 수신 서버이고, 발신은 이미 제공자 include가 허용하고 있습니다. mx가 의미 있는 경우는 직접 운영하는 서버가 같은 IP로 메일을 받고 보낼 때뿐입니다.
ptr은 느리고 믿을 수 없으며, RFC 7208도 쓰지 말라고 합니다. 지우세요.
3. 각 서비스가 이 레코드에 있어야 하는지부터 확인
대부분이 끝내 모르고 지나가는 해결책입니다. SPF는 수신자가 보는 From: 주소가 아니라 봉투 발신자(Return-Path, MAIL FROM이라고도 함)를 기준으로 검사합니다. 많은 발송 플랫폼이 자기 반송 도메인을 쓰거나, bounce.example.com 같은 서브도메인을 CNAME으로 자기 쪽에 연결하게 합니다. 이런 플랫폼이 보낸 메일은 그 반송 도메인으로 SPF를 검사하고, 루트 도메인의 레코드는 아예 조회하지 않습니다.
그 서비스가 실제로 보낸 메일의 헤더를 보세요.
Return-Path: <bounces+81234-ab12@em5521.example.com>
Authentication-Results: mx.google.com;
spf=pass (google.com: domain of bounces+81234-ab12@em5521.example.com designates 198.51.100.40 as permitted sender) smtp.mailfrom=bounces+81234-ab12@em5521.example.com;
Return-Path 도메인이 example.com이 아니라 em5521.example.com입니다. 루트 레코드에 넣은 이 서비스의 include는 이 메일에 아무 역할도 하지 않고 조회만 씁니다. 지운 뒤 다음 DMARC 보고서에서 결과를 확인하세요. DMARC 기본값인 relaxed 정렬에서는 example.com 아래의 반송 도메인도 정렬된 것으로 인정됩니다.
4. 발송 흐름을 서브도메인으로 분리
같은 원리를 의도적으로 쓰는 방법입니다. 마케팅, 청구, 고객지원 메일을 news.example.com, billing.example.com처럼 나눌 수 있다면, 서브도메인마다 자기 SPF 레코드와 자기 몫의 10회 한도를 갖습니다. 수신자는 차이를 거의 느끼지 못하고, 한 발송 흐름의 평판 문제가 그 서브도메인에 머무는 부수 효과도 있습니다.
5. 평탄화는 자동화와 함께만
평탄화(flattening)는 include를 그것이 지금 펼쳐지는 IP 범위로 바꿔 넣는 것입니다.
v=spf1 include:_spf.google.com ip4:198.2.128.0/18 ip4:148.105.0.0/16 ip4:205.201.128.0/20 ~all
평탄화한 include마다 조회가 최소 1회 줄어듭니다. 대가는 남의 IP 목록 사본을 내가 관리하게 된다는 것입니다. 서비스가 범위를 추가해도 내 레코드는 모르고, 새 범위에서 나간 메일은 SPF 실패가 됩니다. 아무도 알려 주지 않습니다. DMARC 보고서나 반송 메일로 알게 됩니다.
평탄화를 한다면 서비스 include를 주기적으로 다시 조회해 DNS를 갱신하는 장치와 함께 하세요. DNS 제공자 API를 쓰는 스크립트든 상용 평탄화 서비스든 상관없습니다. 범위를 손으로 한 번 복사하고 잊어버리는 것만은 하지 마세요. 범위가 자주 바뀌는 대형 서비스는 가능하면 include로 두고, 작고 안정적인 것만 평탄화하세요.
하는 김에 실무 제한 두 가지. 따옴표로 묶인 TXT 문자열 하나는 255자를 넘을 수 없으므로 긴 레코드는 여러 문자열로 나눠 게시합니다. SPF가 이어 붙이므로 문제없습니다. 그리고 레코드 전체를 적당히 짧게 유지하세요. 아주 큰 DNS 응답은 UDP에 다 들어가지 않아 TCP로 넘어갈 수 있고, 그만큼 문제가 생길 곳이 늘어납니다.
효과가 있는 “나누기”는 이것의 변형뿐입니다. 평탄화한 범위를 include:_spf1.example.com 같은 내 하위 레코드에 넣되, 그 안에는 ip4:와 ip6:만 두는 방식입니다. 하위 레코드 하나가 조회 1회로 많은 범위를 담습니다. 여전히 include가 들어 있는 하위 레코드로 쪼개는 것은 아무것도 줄이지 못하고, 조각마다 조회만 1회씩 늘립니다.
6. 매크로 기반 SPF
한 단계 더 나간 기법으로 exists:%{i}._spf.example.com이 있습니다. %{i} 매크로가 접속한 IP로 펼쳐지고, 직접 운영하는 DNS 서버가 허용된 IP면 A 레코드로 답합니다. 발송처가 몇 개든 조회 1회입니다. 여러 호스팅형 SPF 서비스가 이 방식으로 동작합니다. 잘 작동하지만, 이제 SPF가 그 DNS 서비스의 가용성과 정확성에 달리게 되고 사람이 레코드를 읽어서는 무엇이 허용되는지 알 수 없습니다. 발송처가 아주 많고 결과를 책임질 사람이 있는 조직에 맞는 방법입니다.
하지 말아야 할 것
- “부하를 나누려고” SPF 레코드를 두 개 게시하지 마세요. 같은 이름에
v=spf1로 시작하는 TXT 레코드가 두 개면 그 자체로 permerror입니다(RFC 7208 §4.5). - 오류를 없애려고
+all로 바꾸거나-all/~all을 빼지 마세요.+all은 인터넷 전체에 내 도메인으로 메일을 보낼 권한을 줍니다. - 주 메일 제공자의 include를 지워서 해결하지 마세요. 가장 중요한 메일에서 permerror를 일반 fail로 바꿀 뿐입니다.
고친 뒤 확인
spfcount example.com # 10 이하가 나와야 함
dig +short TXT example.com | grep -ci spf1 # 1이 나와야 함
그다음 Gmail 주소로 메일을 보내고 “원본 보기”를 여세요. SPF 줄이 보낸 서버에 대해 PASS여야 합니다. 남겨 둔 발송 서비스마다 반복하세요. 실패하던 것은 레코드 끝쪽에 있던 발송처입니다. 이전 레코드의 TTL이 지나면 이메일 점검에서도 새 레코드와 조회 항목을 볼 수 있습니다.
SPF는 괜찮은데 여전히 DMARC에서 실패한다면 SPF보다는 정렬이나 DKIM 문제인 경우가 대부분입니다. DKIM이 실패하는 이유와 DMARC none·quarantine·reject 차이를 보세요. SPF가 나머지와 어떻게 맞물리는지는 이메일 전달률 가이드에 정리돼 있습니다.
내 도메인에서 바로 확인
무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.