확인 도구가 레이트리밋에 걸려 만든 가짜 음성(false negative) — 재현 조건과 회피 명령 실측
도메인에 메일 인증용 DNS 레코드 4건을 새로 등록하고 검증하는 과정에서, 레코드는 정상인데 "없다"고 판독되는 현상을 만났습니다. 원인은 DNS도 전파 지연도 아니었고, 확인하는 쪽 명령의 패턴이었습니다. 재현 조건과 회피 방법을 실측 수치와 함께 정리합니다.
1. 측정 환경
| 항목 | 값 |
|---|---|
| 측정일 | 2026-08-13 |
| 대상 레코드 | 4건 (DKIM TXT / 서브도메인 MX / 서브도메인 SPF TXT / _dmarc TXT) |
| 조회 대상 | 권위 네임서버 2종 + 공용 리졸버 2종 = 4곳 |
| 총 쿼리 수 | 4레코드 × 4곳 = 최대 16회, 1차 시도는 8쿼리 연속 발사 |
| 도구 | dig (기본 UDP) |
| DNS 운영 | 관리형 DNS (권위 NS 2대) |
메일 발신을 위해 루트 도메인은 건드리지 않고 서브도메인에 발신 계열 레코드를 몰아넣는 구성이었습니다. 루트의 기존 MX 3행과 SPF 1건은 수신용이라 무손상 유지가 전제였고, 그래서 "새 레코드 4건이 정확히 들어갔는가"를 조회로 확인해야 했습니다.
2. 1차 측정 — 4건 중 2건이 비어 있었다
권위 NS를 대상으로 레코드를 연속 조회했습니다. 결과가 이랬습니다.
| 레코드 | NS #1 | NS #2 |
|---|---|---|
| DKIM TXT | 존재 | 존재 |
| 서브도메인 SPF TXT | 존재 | 존재 |
| 서브도메인 MX | 간헐 공란 | 존재 |
_dmarc TXT |
존재 | 간헐 공란 |
여기서 나온 1차 결론은 "4건 중 2건 미등록 + 네임서버 간 전파 지연" 이었습니다. 그럴듯했어요. 공란이 한쪽 NS에 몰리지 않고 레코드마다 다른 NS에서 나왔으니, "아직 동기화 중"이라는 그림이 자연스럽게 그려집니다.
이 결론은 틀렸습니다.
가장 큰 힌트는 "간헐"이었습니다. 등록되지 않은 레코드는 간헐적으로 나타나지 않습니다. 없으면 계속 없어야 합니다. 결과가 흔들린다는 건 대상이 흔들리는 게 아니라 경로가 흔들린다는 뜻일 수 있습니다.
3. 재측정 — TCP 단발 조회로는 전건 존재
같은 레코드를, 같은 네임서버에, 이번에는 한 번에 하나씩 TCP로 조회했습니다.
# 1차 (문제 발생): UDP 기본값으로 8쿼리 연타
for r in dkim spf mx dmarc; do
dig @ns1.example-dns.net TXT "$r.example.com" +short
dig @ns2.example-dns.net TXT "$r.example.com" +short
done
# 2차 (정상 판독): TCP + 단발, 조회 사이 간격
dig +tcp @ns1.example-dns.net MX send.example.com +short
dig +tcp @ns2.example-dns.net TXT _dmarc.example.com +short
결과:
| 레코드 | 1차 (UDP 연타) | 2차 (TCP 단발) |
|---|---|---|
| DKIM TXT | 존재 | 존재 |
| 서브도메인 SPF TXT | 존재 | 존재 |
| 서브도메인 MX | 간헐 공란 | 존재 |
_dmarc TXT |
간헐 공란 | 존재 |
4건 전부 양쪽 네임서버에 안정적으로 존재했습니다. 누락도 전파 지연도 없었습니다. 등록은 처음부터 완료된 상태였습니다.
4. 원인 — 확인하는 쪽이 레이트리밋에 걸렸다
UDP로 짧은 시간에 쿼리를 연타하면, 권위 NS 앞단에서 쿼리 단위 레이트리밋이 걸릴 수 있습니다. 그때 돌아오는 것은 "이 레코드는 없습니다"라는 명확한 응답이 아니라 아무것도 아닌 것 — 응답 유실입니다.
문제는 dig +short가 두 상태를 같은 화면으로 출력한다는 점입니다.
| 실제 상태 | dig +short 출력 |
|---|---|
| 레코드 없음 (NXDOMAIN / 빈 응답) | 공란 |
| 응답 유실 (레이트리밋·타임아웃) | 공란 |
공란 두 개는 구분되지 않습니다. 그래서 "없다"로 읽힙니다. 여기에 사람 쪽의 기대가 붙으면 완성됩니다 — 방금 레코드를 등록했으니 "아직 안 퍼졌나 보다"라는 설명이 즉시 준비되어 있고, 그 설명이 데이터와 맞아떨어지는 것처럼 보입니다.
TCP 조회가 이 문제를 피하는 이유는 단순합니다. 연결 기반이라 레이트리밋 계열의 조용한 드롭 대신 실패가 실패로 드러나고, 연타가 구조적으로 어려워집니다.
5. 회피 수칙
같은 함정을 피하려면 조회 자체를 다르게 설계해야 합니다.
# ① 공란과 실패를 구분한다 — +short를 벗기고 status를 본다
dig @ns1.example-dns.net TXT _dmarc.example.com | grep -E "status:|ANSWER:"
# status: NOERROR + ANSWER: 0 → 진짜 없음
# 응답 자체가 없음/타임아웃 → 측정 실패 (레코드 상태 미확정)
# ② 연타하지 않는다
sleep 1 # 쿼리 사이 간격
# ③ 흔들리면 TCP로 재확인한다
dig +tcp @ns1.example-dns.net TXT _dmarc.example.com
# ④ 대조군을 함께 조회한다 — 확실히 존재하는 레코드
dig +tcp @ns1.example-dns.net MX example.com +short
특히 ④번이 중요합니다. 확실히 존재하는 레코드가 같은 조회에서 함께 비어 나온다면, 문제는 대상이 아니라 조회 경로입니다. 대조군 없이 0건을 결론으로 쓰면 이번 같은 오판이 그대로 굳습니다.
6. 검증 결과 (같은 작업의 최종 실측)
레코드가 정상임을 확인한 뒤 실제 메일 1통을 발송해 종단 확인을 했습니다.
| 항목 | 실측 |
|---|---|
| 발송 결과 | 성공 (메시지 ID 반환) |
| 수신함 | 스팸함 아님 — 받은편지함 직행 |
| 발송 → 수신 소요 | 3초 |
| 레코드 4건 최종 상태 | 권위 NS 2종 + 공용 리졸버 2종 전건 일치 |
발송이 성공했다는 사실 자체가 도메인 인증의 실증이기도 합니다. 인증되지 않은 도메인은 발송 단계에서 거부되기 때문입니다.
7. 남는 교훈
이번 건의 실질적 손실은 크지 않았습니다. 오판이 30분 안에 자체 정정됐고, 잘못된 DNS 변경으로 이어지지 않았습니다. 하지만 한 걸음만 더 갔다면 멀쩡한 레코드를 지우고 다시 넣는 작업을 했을 겁니다. 그러면 진짜 장애가 만들어졌겠죠.
정리하면 이렇습니다.
- 간헐적 결과는 대상이 아니라 경로를 의심하라. 없는 것은 꾸준히 없다.
- 빈 출력은 "없음"이 아니다. 빈 출력은 "없음"과 "못 봤음"의 합집합이다.
- 0건·404를 결론으로 쓰기 전에 대조군을 한 번 더 조회하라. 정상인 대상이 같이 0으로 나오면 그건 대상의 문제가 아니다.
- 측정 도구는 측정 대상에 영향을 준다. 폴링 간격, 프로토콜, 재시도 패턴이 전부 관측 결과의 일부다.
특히 4번은 인프라 점검 전반에 걸립니다. 상태를 자주 확인할수록 정확해질 것 같지만, 확인이 잦아지면 확인 행위 자체가 대상의 상태를 바꾸는 구간이 생깁니다. 확인 주기를 정할 때 "얼마나 자주 봐야 하나"만큼 "보는 행위가 무엇을 건드리나" 를 같이 따져야 합니다.
재현 조건 요약
| 조건 | 값 |
|---|---|
| 트리거 | 단시간 다중 UDP DNS 쿼리 (동일 권위 NS 대상 8회 연속) |
| 증상 | 존재하는 레코드가 간헐적으로 공란 반환 |
| 오진 유도 요소 | 신규 등록 직후라는 맥락 + +short의 공란 동일 출력 |
| 회피 | +tcp 단발 조회 / 쿼리 간 간격 / status 필드 확인 / 대조군 병행 |
| 확정 방법 | 동일 대상 TCP 재조회로 전건 존재 확인 |
측정이 흔들릴 때 대상부터 고치려 들면, 고칠 게 없는 곳에 손을 대게 됩니다. 흔들리는 것이 대상인지 자인지부터 확인하는 편이 빠릅니다.