2008년 여름, 댄 카민스키는 많은 사람의 휴가를 취소시켰다. 그가 찾아낸 공격은 오프패스 공격자 — 당신의 트래픽을 볼 수는 없고 그저 추측만 하는 자 — 가 재귀 리졸버의 캐시를 오염시켜 도메인 하나를 통째로 딴 데로 돌릴 수 있게 했다. 뒤이은 소동이 낳은 해결책은 모두가 기억하는 그것이다: 소스 포트 무작위화. DNS 질의는 이미 공격자가 맞혀야 할 16비트 트랜잭션 ID를 담고 있었는데, 이제 무작위 소스 포트에서도 나가게 되어 위조자는 ID와 포트를 함께 맞혀야 했고, 65,536가지였던 경우의 수가 수십억 가지로 불어났다. RFC 5452가 2009년에 이를 명문화했고, 리졸버들이 배포했고, 당장의 위기는 지나갔다.

하지만 소스 포트를 무작위화하는 건 언제나 해결책으로 분장한 임시방편이었다. 아무것도 인증하지 않는다. 그저 복권을 맞히기 어렵게 만들 뿐이고, 충분히 빠른 공격자가 충분히 굵은 회선을 쥐고 있으면 어렵다는 건 불가능하다는 것과 같지 않다. 더 나은 아이디어가 바로 거기 있었다 — 값싸고, 상태 없고, 범용적인 — 그리고 2016년 마침내 표준이 됐다. 거의 모든 곳에서 켜졌고, 거의 아무 데서도 제대로 쓰이지 않았다.

DNS 쿠키란 실제로 무엇인가

DNS 쿠키는 브라우저 쿠키가 아니고, 추적과도 아무 상관이 없다. 질의와 응답에 얹혀 다니는 EDNS 옵션 — 옵션 코드 10 — 이다. 2016년에 나온 RFC 7873은 이를 담백하게 “오프패스 공격자에 의한, 점점 흔해지는 여러 서비스 거부·증폭/위조·캐시 포이즈닝 공격에 대해 DNS 서버와 클라이언트에 제한적 보호를 제공하는 경량 DNS 트랜잭션 보안 메커니즘”이라고 설명한다.

그 문장에서 일하는 단어는 경량이다. 메커니즘 전체가 작은 불투명 바이트열 두 개다:

  • 클라이언트 쿠키 — 클라이언트가 골라 보내는 8바이트. 서버가 그대로 되돌려준다.
  • 서버 쿠키 — 서버가 생성해 클라이언트에게 건네는 8~32바이트. 클라이언트가 저장했다가 다음 질의에 실어 보낸다.

그게 전부다. 배포할 공유 키도, 인증서도, 맞춰야 할 시계도 없다. 그리고 이 미니멀함이 존재 이유의 전부다. 우리에겐 이미 DNS 트랜잭션을 암호학적으로 인증하는 방법 — TSIG — 이 있었지만, 바로 그것이 양쪽에 공유 비밀 키를 설정해야 하기 때문에 모르는 상대 사이에서는 거의 아무도 돌리지 않는다. 쿠키는 키 관리 프로젝트 없이 기본값으로 켜둘 수 있는 물건이 되도록 설계됐다. 금고가 아니라 과속방지턱. 하지만 정확히 옳은 자리에 놓인 과속방지턱이다.

값싼 바이트열 두 개가 서로 다른 두 공격을 막는 법

영리한 대목은 클라이언트 쿠키와 서버 쿠키가 서로 다른 것을 방어한다는 점이다.

클라이언트 쿠키는 질문하는 쪽을 보호한다. 리졸버가 클라이언트 쿠키를 담아 질의를 보내면, 정상 서버는 응답에 그걸 그대로 되돌려준다. 응답을 위조하는 오프패스 공격자는 그 쿠키를 볼 수 없다 — 그들이 추측하고 있는 회선 위에 있지 않다 — 그래서 위조본은 틀린 쿠키를 달고 오거나 아예 없이 오고, 리졸버는 그걸 버린다. 무작위 소스 포트와 같은 발상인데, 이제 오프패스 공격자는 한 번도 본 적 없는 64비트를 추가로 맞혀야 한다. 그건 “더 어려워졌다”가 아니다. 끝난 것이다 — 상대 서버도 쿠키를 말하는 한.

서버 쿠키는 서버를 보호하고, 이쪽이 인터넷의 더 큰 문제와 맞닿은 절반이다. UDP 위의 DNS는 인터넷이 가장 아끼는 증폭기다: 작은 질의에 위조된 소스 주소를 달아 보내면, 서버는 당신이 지목한 피해자에게 훨씬 큰 답을 쏜다. 서버 쿠키는 서버가 실제로 대화해 본 클라이언트와 위조된 주소를 구별하게 해 준다. 클라이언트 쿠키만 들고 온 클라이언트에게 서버는 작은 응답 — BADCOOKIE, 확장 RCODE 23 — 을 줄 수 있는데, 여기엔 새 서버 쿠키와 그걸 가지고 다시 오라는 지시가 담겨 있다. 진짜 클라이언트는 그렇게 한다. 위조된 소스 주소는 그럴 수 없다. 응답이 공격자가 아니라 위조된 피해자에게 가기 때문이다. 문제는 RFC 자체에 적혀 있다. 쿠키가 아예 없는 질의에는 서버가 쿠키를 구현하지 않은 것처럼 답한다(RFC 7873 5.2.1절). 공격자는 그냥 옵션을 빼면 된다. 그러니 반사 방어는 핸드셰이크 그 자체가 아니라, 핸드셰이크 덕분에 나머지 트래픽에 할 수 있게 되는 일에 있다. 진짜 클라이언트들이 유효한 서버 쿠키를 들고 오면, 쿠키 없는 트래픽에는 속도 제한을 세게 걸고 UDP 응답을 작게 줄일 수 있다 — 중요한 리졸버들은 면제된다는 걸 알고서.

‘지원’과 ‘요구’ 사이의 틈

바로 여기서 조용히 어긋난다.

BIND — 가장 널리 배포된 권한·재귀 서버 — 는 2016년 9.11 버전에서 DNS 쿠키를 기본값으로 켰다. 기본 상태에서 자기 질의에 쿠키를 실어 보내고, 받은 질의에 쿠키로 답한다. 현대적인 리졸버를 돌리고 있다면 당신은 지금 이 순간 거의 확실히 이 프로토콜을 말하고 있다.

하지만 쿠키를 보내고 답하는 것은 쿠키를 활용하는 것과 같지 않고, 보호는 활용하는 쪽에 있다. BIND의 require-server-cookie — 유효한 서버 쿠키 없이 온 쿠키 지원 클라이언트에게 완전한 UDP 답 대신 BADCOOKIE를 보내는 스위치 — 는 기본값이 no이고, 켜더라도 애초에 쿠키를 보낸 클라이언트에게만 적용된다. 반사 공격에 실제로 중요한 지렛대는 응답 속도 제한(RRL)이다. BIND의 RRL은 유효한 서버 쿠키를 가진 클라이언트를 면제하는데, 운영자가 직접 설정해야 하고 기본 상태에선 아예 없다. 기본 자세는 쿠키를 주고받은 뒤 쿠키 있는 트래픽과 없는 트래픽을 똑같이 대하는 것이다. 주고받기만 하고 아무 데도 쓰지 않는 쿠키는 대체로 연극이다: 회선 위 바이트 비용은 치렀는데 방어는 거의 사지 못했다. 공격자는 그냥 쿠키를 빼고 보내고, 당신은 어차피 답하기 때문이다.

이 대목이 정말로 답답하다. 이 메커니즘은 초안에 멈춰 있지 않다. 브라우저 벤더나 플래그 데이를 기다리지도 않는다. 배포됐고, 켜져 있고, 상당수의 리졸버와 권한 서버 사이 회선 위에 올라 있다 — 그런데 비용은 치르면서 보안은 가장 적게 내주는 모드로 공회전하고 있다.

왜 공회전했는가

솔직한 답은 순전한 태만이 아니다. 여러 해 동안 쿠키에는 운영자가 강제를 꺼둘 정당한 이유가 되어 준 진짜 상호운용 문제가 있었다.

서버 쿠키는 비밀에서 계산된다. 단일 서버에서는 괜찮다. 하지만 진지한 DNS는 대부분 애니캐스트나 로드밸런서 뒤 — 하나의 IP 주소를 위해 답하는 수십 대의 머신 — 에 산다. 각 머신이 자기만의 비밀로 서버 쿠키를 계산하면, 머신 A에서 쿠키를 받아 머신 B로 돌아온 클라이언트가 거부당하고, 강제는 당신이 막으려던 그 공격자로 당신의 인프라 자신을 바꿔 놓는다. 2016년 원 규격은 서버 쿠키의 구성 방식을 구현마다 정하도록 남겨 두었고(예시만 제시했다), 그래서 구현들이 갈라졌고 서로 다른 구현이 섞인 클러스터는 합의할 수 없었다.

RFC 9018이 2021년에 이를 고쳤다. 상호운용 가능한 서버 쿠키를 못박은 것이다: 클라이언트 쿠키·타임스탬프·클라이언트 주소에 공유 비밀을 키로 SipHash-2-4를 적용하는 정해진 구성으로, 클러스터의 모든 머신이 같은 쿠키를 계산하고 클라이언트가 그 사이를 자유로이 오갈 수 있게 했다. 그것으로 마지막 남은 좋은 핑계가 사라졌다. 그러나 그것은 원본보다 5년 늦게, DNS 쿠키를 이미 “지원됨, 무시”로 분류해 둔 인터넷에 도착했고, 기본값은 끈질기다. 기술적으로 이미 켜져 있는 기능을 켜자고 아무도 점검 창을 잡지 않는다.

배포됐고, 존재하고, 공회전 중

나는 DNS 보안 기능을 실제로 어떤 운명을 맞았는가로 머릿속에 분류해 둔다. DNSSEC은 배포됐지만 대체로 중요해지지 못했다 — 20여 년이 지나도록 대부분의 존은 여전히 서명되지 않았는데, 아무도 원하지 않는 키 관리 약속을 요구했기 때문이다. QNAME 최소화는 배포됐고 실제로 중요해졌다 — 남에게 아무 비용도 지우지 않는 리졸버 쪽 변경이라, 주요 리졸버들이 기본값으로 켤 때까지 조용히 퍼졌다. DNS 쿠키는 세 번째, 더 이상한 범주다: 배포됐고, 존재하고, 상당수의 DNS 서버에서 돌아가는데, “존재”를 “보호”로 바꾸는 설정이 운영자 몫으로 남아 있어 제 일의 일부만 하고 있다.

해결책은 이미 쿠키를 말하는 권한 서버에서 설정 몇 줄이다 — 응답 속도 제한을 켜서 쿠키 없는 UDP는 조이고 쿠키를 든 리졸버는 통과시키고, 쿠키 지원 클라이언트에는 require-server-cookie도 고려하고, 이를 못 견디는 레거시 클라이언트를 지켜보면, 당신의 서버는 훨씬 나쁜 증폭기가 된다. DNS에서 가용한 보호 대비 노력 비율이 가장 높은 축에 드는데, 2016년부터 켜졌지만-장전되지 않은 채 선반에 놓여 있었다. 장전하라.