이 오류가 뜻하는 것
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 안에서 평가하는 항목도 같은 10회 한도를 공유합니다. 최상위 DNS 유발 항목 5개에 하위 include 3개가 추가되면 참조 구조 합계는 9가 아니라 8입니다. 이 합계가 모든 발신자의 실제 평가 횟수는 아닙니다. SPF는 메커니즘을 순서대로 평가하고 일치하면 멈춥니다(RFC 7208 §§4.6.2–4.6.4).
같은 절에 덜 알려진 두 번째 제한도 있습니다. void lookup입니다. 레코드가 하나도 없는 응답(NODATA)이나 존재하지 않는 이름(NXDOMAIN)을 받은 조회를 말하고, RFC는 수신 서버가 이것을 두 번까지만 허용하고 그 이상이면 permerror를 내도록 권고합니다. 더 이상 SPF를 게시하지 않는 옛 발송 서비스 도메인을 가리키는 include:, A 레코드를 지운 a:old-mail.example.com 같은 것이 이 한도를 갉아먹습니다. 또 include한 대상에 SPF 레코드가 아예 없으면 횟수와 상관없이 그것만으로 permerror입니다.
참조 구조와 발신 IP 평가를 구분하기
아래는 교육용 가상 고정 DNS 트리입니다. 2026년 10월 제공자의 실제 DNS 관측값이 아닙니다. 순환 include에서 끝나지 않거나 DNS 실패를 조회 0회로 숨길 수 있는 재귀 셸 함수 대신 이 예제로 계산을 설명합니다.
example.com: v=spf1 ip4:203.0.113.10 mx a include:_spf.a.example include:_spf.b.example include:_spf.c.example ~all
_spf.a.example: v=spf1 include:_spf.a1.example include:_spf.a2.example include:_spf.a3.example ~all
_spf.a1.example: v=spf1 ip4:192.0.2.1 ~all
_spf.a2.example: v=spf1 ip4:192.0.2.2 ~all
_spf.a3.example: v=spf1 ip4:192.0.2.3 ~all
_spf.b.example: v=spf1 ip4:198.51.100.1 ~all
_spf.c.example: v=spf1 ip4:198.51.100.2 ~all
최상위 DNS 유발 항목은 mx, a, include 3개로 5개입니다. _spf.a.example에서 include 3개가 추가되고 나머지 하위 레코드는 ip4와 all뿐이므로 참조 구조 합계는 5 + 3 = 8입니다. 이는 항목 수이며, 개별 DNS 패킷 수나 캐시 미스 횟수가 아닙니다.
발신 IP가 203.0.113.10이면 첫 ip4에 일치해 Pass로 종료하므로 실제로 평가한 DNS 유발 항목은 0개입니다. SPF 정책을 얻는 최초 TXT 조회는 이 항목 수에 포함하지 않습니다. 다른 발신 IP는 다른 경로를 거쳐 일찍 일치하거나 DNS 오류를 만날 수 있습니다. redirect=는 일치하는 메커니즘이 없을 때만 적용하며, all이 있으면 무시합니다(§6.1).
구조 합계만으로 Pass·Fail·TempError·PermError를 판정할 수 없습니다. 실제 평가에는 발신 IP와 평가 대상 신원, DNS 응답, 매크로, 10항목 한도와 별도의 MX/주소·void lookup 제한이 필요합니다. DNS 실패·SPF 없음·다중 SPF·순환·시간초과는 각각 확인해야 하며 정상 조회 0회로 취급하면 안 됩니다. 실제 발송 경로별 평가는 유지보수되는 SPF 평가기를 사용하세요. 이 글은 SPF 구현체가 아닙니다.
게시된 정책을 확인할 때는 제거한 재귀 함수 대신 다음처럼 조회합니다.
dig +time=2 +tries=1 TXT example.com
명령 종료코드와 DNS 응답 상태를 먼저 확인하고 TXT 응답을 읽으세요. 빈 응답은 조회 수 0을 뜻하지 않습니다. 한 SPF TXT 레코드 안의 문자열 여러 개는 공백을 새로 넣지 않고 이어 붙입니다. 별개의 SPF 레코드 두 개를 게시하는 것은 다르며 PermError의 원인입니다.
이메일 점검은 최상위 레코드의 DNS 유발 항목을 보여 줍니다. 발신 IP를 평가하거나 include를 재귀적으로 조회하지 않습니다. 해당 레코드를 확인하는 데 사용하고, 참조하는 정책은 DNS 조회로 확인하세요. 구조상 숫자만으로 실제 SPF 판정을 보장하지 않습니다.
어떤 메일은 통과하고 어떤 메일은 실패하는 이유
발신 IP마다 평가를 멈추는 위치가 다릅니다. 첫 IP 범위에 일치하면 뒤의 include를 평가하지 않고, 나중에 일치하는 발신자는 그 전에 조회 한도나 DNS 오류를 만날 수 있습니다. From:을 위조한 메일도 봉투 발신자의 SPF에서 허용한 IP로 보낼 수 있으므로 위조라는 이유만으로 트리 전체를 돈다고 단정하지 않습니다.
DMARC의 SPF 경로를 통과하려면 SPF Pass와 From: 도메인 정렬이 필요합니다. PermError는 그 조건을 충족하지 못하지만, 정렬된 유효한 DKIM 서명이 있으면 DMARC를 통과할 수 있습니다. 수신 서버는 별도의 수신 정책도 적용합니다. 기준은 RFC 7208과 RFC 7489 §3.1입니다.
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는 이 메일에 아무 역할도 하지 않고 조회만 씁니다. 이 메일 한 통만으로 include를 삭제하지는 마세요. 루트 MAIL FROM을 쓰는 다른 활성 경로와 계절성 발송이 없는지 확인한 뒤 정리하고 남은 경로를 테스트하세요. 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로 바꿀 뿐입니다.
고친 뒤 확인
dig +time=2 +tries=1 TXT example.com
DNS 응답 상태와 SPF 정책이 하나임을 확인한 뒤, 유지보수되는 SPF 평가기로 실제 발신 IP·MAIL FROM별 판정을 확인하세요. 그다음 Gmail 주소로 메일을 보내고 “원본 보기”를 여세요. SPF 줄이 보낸 서버에 대해 PASS여야 합니다. 남겨 둔 발송 서비스마다 반복하세요. 실패하던 것은 레코드 끝쪽에 있던 발송처입니다. 이전 레코드의 TTL이 지나면 이메일 점검에서도 새 레코드와 조회 항목을 볼 수 있습니다.
SPF는 괜찮은데 여전히 DMARC에서 실패한다면 SPF보다는 정렬이나 DKIM 문제인 경우가 대부분입니다. DKIM이 실패하는 이유와 DMARC none·quarantine·reject 차이를 보세요. SPF가 나머지와 어떻게 맞물리는지는 이메일 전달률 가이드에 정리돼 있습니다.
내 도메인에서 바로 확인
무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.