SPF ~all vs -all: 소프트페일 전염병

대부분의 도메인은 SPF 레코드를 ~all로 끝낸다. 실패한 메일을 받아들이라는 뜻이다. SPF 혼자서는 스푸핑을 막을 수 없게 만드는, 안전해 보이는 기본값이다.

거의 모든 도메인의 SPF 레코드는 똑같이 끝난다: ~all. -all이 아니라. 그 한 글자 — 하이픈이 아닌 물결표 — 가 “이 검사에 실패한 메일을 거부하라”와 “이 검사에 실패한 메일을 받아들이되 조금 의심스럽게 보라”의 차이다. 인터넷은 압도적으로 후자를 골랐다. 그리고 ~all을 게시한 사람들 대부분은 자신이 세상의 메일 서버에게 인가되지 않은 발신자를 통과시키라고 말했다는 걸 모른다.

이것이 소프트페일 전염병이고, 이해할 가치가 있다. 사람들을 끊임없이 헷갈리게 하는 한 가지를 설명해 주기 때문이다: 위조 발신자를 막는 게 유일한 임무인 프로토콜 SPF가, 왜 그렇게 좀처럼 아무도 못 막는가.

네 글자, 완전히 다른 네 가지 정책

SPF(RFC 7208)는 당신 도메인의 메일을 보낼 수 있는 IP 주소 목록을 담은 DNS TXT 레코드다. 메커니즘 — ip4:, include:, mx, a — 이 허용 집합을 쌓는다. 레코드는 거의 항상 all 메커니즘으로 끝나고, 실제 정책은 all 앞의 *한정자(qualifier)*에 있다. 네 가지다:

  • +all — Pass. 전부 인가. “누구나 나로서 보낼 수 있다”는 뜻이니, 사실상 정책이 없는 것이다. 제정신이면 아무도 안 쓴다.
  • -all — Fail. 하드페일. 목록에 없는 IP는 명시적으로 인가되지 않음.
  • ~all — SoftFail. IP는 아마 인가되지 않았지만, 도메인이 그렇게 단호히 말할 의사는 없음.
  • ?all — Neutral. 아무 진술도 없음. 정책이 없는 것과 똑같이 취급됨.

all만 덩그러니 쓰면 기본 한정자는 +다. 그게 첫 번째 함정이지만, 흥미로운 건 아니다. 흥미로운 건 -all~all 사이의 틈이다. 그 틈에서 SPF의 약속이 조용히 녹아 없어지기 때문이다.

소프트페일이 수신 서버에게 실제로 시키는 일

여기가 사람들이 놓치는 부분이다. 이 한정자들은 당신 마음 편하자고 붙이는 라벨이 아니라 수신 메일 서버에게 내리는 지시이고, RFC 7208은 각각이 무슨 뜻인지 못 박아 둔다.

하드페일(-all)에 대해 §8.4는 그 결과가 “클라이언트가 주어진 신원으로 도메인을 사용할 권한이 없다는 명시적 진술”이며 수신자가 메시지를 즉시 거부할 수 있다고 한다.

소프트페일(~all)에 대해 §8.5는 놀랄 만큼 솔직하다. 소프트페일은 “‘fail’과 ‘neutral’/‘none’ 사이 어딘가로 취급되어야 한다.” 도메인은 “호스트가 인가되지 않았다고 믿지만 그렇게 강한 진술을 할 의사는 없다.” 그리고 핵심 문장: “수신 소프트웨어는 이 결과만을 근거로 메시지를 거부해서는 안 되며(SHOULD NOT), 평소보다 면밀히 살필 수는 있다(MAY).”

다시 읽어 보라. ~all은 SPF만을 근거로 거부하지 말라는 문서화된 지시다. ~all을 게시할 때 당신은 인터넷의 모든 수신자에게 이렇게 말하는 것이다: 내 SPF 검사에 실패한 메일도 배달하라. 어쩌면 조금 더 걸러라. 아마 안 그러겠지만. 명세 자체가 수신자에게 통과시키라고 말한다.

그래서 “내 SPF가 위조 메일을 막는가?”라는 질문은 대다수 도메인에 대해 정확한 답이 있고, 그 답은 아니오다 — SPF가 고장 나서가 아니라, 그들이 명시적으로 막지 말라고 요청했기 때문이다.

왜 인터넷 전체가 더 약한 쪽을 골랐나

~all이 그렇게 이빨 빠진 거라면 왜 이겼을까? -all이 정상 메일을 깨뜨리고, 그것도 디버깅하기 짜증 나는 방식으로 깨뜨리기 때문이다.

SPF에는 한정자와 무관한 구조적 결함이 있다: 접속하는 IP를 봉투 발신자에 대조해 검증하고, 모든 홉에서 다시 검사한다. 누군가 당신의 메시지를 전달(forward)하는 순간 — .edu 주소를 Gmail로 자동 전달하는 사용자, 메일링 리스트, 메일을 중계하는 “문의하기” 폼 — 전달 서버가 접속 IP가 된다. 그 서버는 당신 SPF 레코드에 없었다. SPF는 실패한다. 뭔가 위조돼서가 아니라, SPF의 좁은 시야에선 전달이 사칭과 구분되지 않기 때문이다.

-all이면 그 전달된-하지만-정상인 메일이 거부된다. ~all이면 살아남는다. “진짜 사용자의 진짜 메일을 가끔 거부”와 “스푸핑을 가끔 통과” 중에서, 엄청난 수의 운영자가 — 그리고 더 결정적으로, 그들을 위해 SPF 레코드를 생성해 주는 플랫폼과 마법사가 — 물결표를 골랐다. 공개 측정도 이를 뒷받침한다: SPF를 게시한 인터넷을 스캔해 보면 소프트페일이 일관되게 다수이고, 수백만 도메인에서 ~all-all보다 많으며, 주요 발송 플랫폼 대부분이 설정 문서에서 ~all을 기본값으로 둔다. 기본값이 표준이 되었고, 표준이 인터넷의 실제 이메일 태세가 되었다.

비극은, 모두가 이걸 SPF 튜닝 결정 — 하드페일은 용감하고 소프트페일은 조심스럽다 — 으로 취급했다는 것이다. 사실 그것은 SPF를 혼자서는 안전하게 강제할 수 없다는 자백이었는데도. 그리고 그 자백은 이를 고치기로 되어 있던 바로 그것을 곧장 가리킨다.

SPF는 애초에 당신이 생각한 주소를 검사한 적이 없다

~all-all 논쟁이 사람들이 싸우는 것만큼 중요하지 않은 가장 깊은 이유: SPF는 사용자가 보는 발신자를 검사하지 않는다.

SPF는 MAIL FROM — SMTP 대화 중에 쓰이는 봉투 발신자, 리턴 패스 — 과 HELO 이름을 검증한다. 메일 클라이언트에 보이는 사람이 읽는 주소, 즉 From: 헤더는 보지 않는다. RFC 7208은 자신이 검사하는 신원이 이것들이며 헤더 From은 그중에 없다고 명시한다. 스패머는 From: 헤더에 무엇이든 넣고, 봉투에는 SPF가 깔끔히 통과하는 자기 도메인을 쓸 수 있으며, 당신의 SPF 레코드는 — 하드페일이든 소프트페일이든 아무 차이 없이 — 피해자가 실제로 읽는 그 주소에 대해 애초에 관할권이 없었다.

그래서 “우리는 -all을 쓰니 스푸핑에서 안전하다”는 용감한 경우에조차 틀렸다. SPF 하드페일은 당신의 봉투 도메인이 리턴 패스로 쓰이는 걸 막는다. 누군가 자기 SPF는 통과시키면서 보이는 From:에 당신 도메인을 위조하는 것에 대해서는 아무것도 하지 못한다. 검사와 기만이 서로 다른 필드를 보고 있다.

From: 헤더를 마침내 인증에 연결하는 프로토콜은 DMARC다. DMARC는 통과한 SPF(또는 DKIM) 결과가 From: 도메인과 *정렬(align)*할 것을 요구하고 — 그리고 이 부분이 소프트페일 논쟁을 거의 무의미하게 만든다 — SPF의 ~all이 거부했던 그 reject 정책을 도메인 소유자가 게시할 수 있게 한다. DMARC 아래에서는 SPF가 ~all로 끝나든 -all로 끝나든 거의 상관없다. DMARC가 정렬로부터 통과/실패를 다시 도출하고 당신의 p=reject를 그와 무관하게 적용하기 때문이다. 강제가 한 계층 위로, 실제로 올바른 주소를 보고 있는 계층으로 옮겨 갔다.

그래서 실제로 뭘 해야 하나

당신이 어디에 있느냐에 따라, 정직한 입장은 둘이다.

강제 정책(p=quarantine 또는 p=reject)이 걸린 DMARC가 있다면, ~all이냐 -all이냐로 고민하지 마라. DMARC가 올바른 신원에 대해, 정렬과 함께 강제를 하고 있다. ~all을 유지하는 건 괜찮고 오히려 더 안전하다. 어차피 DMARC가 DKIM으로 잡을 전달된 메일을 이중으로 벌하지 않기 때문이다. 물결표는 당신의 약한 고리가 아니다.

DMARC가 없다면, 당신의 ~all이 실제로 무엇인지 이해하라: 세상의 메일 서버에게 내 SPF를 강제하지 말아 달라고 정중히 부탁하는 메모다. 스푸핑 방어가 아니다. 애초에 그랬던 적이 없다. -all로 옮기면 조금 얻는다 — 수신자가 위조된 봉투 발신자를 거부할 수 있다 — 전달 메일을 깨뜨리는 대가로, 그러고도 당신의 보이는 From:은 활짝 열려 있다. 진짜 해법은 더 나은 한정자를 고르는 게 아니다. DMARC를 게시해, 사용자가 읽는 그 주소를 마침내 무언가가 검사하게 하는 것이다.

물결표 전염병은 수백만 관리자가 저지른 실수가 아니다. 혼자서는 “거부”라고 안전하게 말할 수 없는 프로토콜에 대한 올바른 해석이다. 실수는 거기서 멈추고 일이 끝났다고 생각하는 것이다.

토론 참여

← 블로그로 돌아가기