2년 전에 *.example.com을 로드밸런서로 향하게 걸어뒀다. 누가 무슨 서브도메인을 만들든 — app, staging, 이번 주 마케팅이 떠올린 무엇이든 — 그냥 풀린다. 티켓도, DNS 변경도 필요 없이 작동한다. 그러던 어느 오후, 누군가 인증서 챌린지를 통과시키려고 _acme-challenge.api.example.com에 TXT 레코드 하나를 추가하자, dashboard.api.example.com이 풀리지 않기 시작한다. 대시보드는 아무도 안 건드렸다. 와일드카드도 안 건드렸다. 그냥 멈췄다.
DNS 제공업체의 버그가 아니다. 와일드카드가 스펙대로 정확히 동작한 것이고, 그 스펙은 사람들이 생각하는 그것과 다르게 적혀 있다.
머릿속 모델이 틀렸다
거의 모두가 *.example.com을 “example.com 아래 전부 매칭”으로 읽는다. 발등을 찍는 총이 바로 거기, 머릿속에 있다. 와일드카드는 서브트리 catch-all이 아니다. 훨씬 좁고 이상한 것이다 — 아직 존재하지 않는 이름에 대한 답을 서버가 합성할 때 쓰는 템플릿.
원래 와일드카드 규칙은 1987년 RFC 1034 §4.3.3에서 왔고, 구현마다 20년간 해석이 엇갈릴 만큼 모호했다. 2006년에 나온 RFC 4592는 단 한 가지 이유로 존재한다 — 1987년 텍스트가 위험할 만큼 모호했기에, 와일드카드가 실제로 무엇을 하는지 못박기 위해서. 제목이 말 그대로 “The Role of Wildcards in the Domain Name System”이다. 프로토콜이 20년 뒤에 짧은 절 하나를 명확히 하려고 RFC 하나를 통째로 받았다면, 그 절이 얼마나 사고를 쳤는지 짐작이 간다.
피해 대부분은 세 가지 규칙에서 나온다.
규칙 1: 별표는 맨 왼쪽에서만 특별하다
*.example.com은 와일드카드다. api.*.example.com은 아니다 — 거기 별표는 문자 그대로의 라벨, 실제로 * 글자가 들어간 이름이다. 라벨은 소유자 이름의 맨 왼쪽 라벨일 때만 와일드카드로 동작한다. 다른 자리에 두면 DNS는 그걸 그냥 ASCII 바이트로 취급한다.
app.*.example.com이 app.무엇이든.example.com을 매칭하길 바라며 이걸 꺼내는 사람이 있다. 안 된다. 그런 메커니즘은 없다. 와일드카드는 맨 왼쪽 자리만, 오직 맨 왼쪽 자리만 확장한다.
규칙 2: 와일드카드는 이미 존재하는 이름을 절대 덮지 않는다
이게 핵심이다. 와일드카드는 질의한 이름이 존(zone) 안에 더 가까운 매치를 갖지 않을 때만 작동한다. 기술 용어로는 closest encloser(가장 가까운 봉합점) — 당신이 물은 이름의, 실제로 존재하는 가장 깊은 조상이다. 답할 수 있는 와일드카드는 그 closest encloser 바로 아래의 *, 거기뿐이다.
도입부 이야기를 이 규칙으로 따라가 보자. 누가 TXT 레코드를 추가하기 전, 존에는 example.com과 *.example.com, 그리고 별거 없었다. dashboard.api.example.com을 질의하면? 존재하는 가장 깊은 조상은 example.com이다. 그 바로 아래 와일드카드인 *.example.com이 답을 합성한다. 잘 된다. (그리고 그렇다, 이건 dashboard.api라는 두 라벨을 건너뛰어 매칭한다 — 중간에 아무것도 존재하지 않는 한, 와일드카드는 여러 라벨 깊이의 이름도 기꺼이 합성한다. 블로그 글에서 보는 “와일드카드는 한 라벨만 매칭한다”는 주장은 그냥 틀렸다.)
이제 _acme-challenge.api.example.com을 추가한다. 세 단계 아래에 레코드를 만들었지만, 의도하지 않은 일도 같이 했다 — api.example.com을 존재하게 만들었다. 그 자체에는 레코드가 하나도 없다. RFC 4592가 empty non-terminal(빈 비단말)이라 부르는 것, 즉 아래에 뭔가가 살기 때문에 순전히 그 이유로 존재하는 이름이다. 그리고 존재 여부는 이진법이다. DNS 입장에서 api.example.com은 이제 트리의 진짜 노드다.
그래서 dashboard.api.example.com 질의를 다시 돌려보자. 존재하는 가장 깊은 조상은 더 이상 example.com이 아니라 api.example.com이다. 이제 답할 수 있는 와일드카드는 *.api.example.com뿐인데, 그건 만든 적이 없다. *.example.com은 한 단계 너무 높아 닿지 못한다. 서버는 NXDOMAIN을 반환한다. 대시보드는 사라졌고, 그걸 죽인 변경은 전혀 다른 서브도메인에 붙인 인증서 레코드였다.
와일드카드에서 “어제는 됐는데”라는 말이 그렇게 흔한 이유가 이거다. 와일드카드는 건드릴 때 깨지지 않는다. 그 근처를 건드릴 때 깨진다.
규칙 3: 와일드카드는 물어본 타입만 답한다
와일드카드 소유자는 자기만의 레코드 타입을 갖고, 그 타입만 합성한다. *.example.com A 203.0.113.10을 올려두고 클라이언트가 random.example.com의 AAAA(IPv6) 레코드를 물으면, 오류도 안 받고 A 레코드도 안 받는다. NODATA를 받는다 — 와일드카드 덕에 이름은 “존재”하지만 거기 AAAA는 없다. MX든 TXT든 다 마찬가지다. 와일드카드는 두루뭉술한 “예”가 아니다. “정확히 이 레코드 타입들, 그 외엔 없음”이라는 두루뭉술한 선언이다.
그리고 *.example.com은 example.com 자신을 매칭하지 않는다. 정점(apex)은 이미 존재하는 이름이라 규칙 2가 적용된다 — 와일드카드는 자기 부모를 절대 덮지 않는다. 맨도메인이 풀리길 원하면 거기에 자체 레코드를 줘야 한다. 이게 사람들을 끊임없이 넘어뜨린다. 와일드카드는 상상 속의 모든 자식은 덮으면서, 정작 브라우저에 타이핑한 그 이름 하나는 대놓고 무시한다.
실제로 어디서 문다
empty non-terminal 함정이 헤드라인이고, 현대 인프라가 사랑하는 언더스코어 접두 레코드에서 가장 자주 터진다 — 인증서용 _acme-challenge, 메일용 _dmarc와 _domainkey, _<service>._tcp SRV 레코드. 자기 레코드가 없던 서브도메인 아래에 이런 레코드를 하나 두면 — _acme-challenge.api, _dmarc.shop, _sip._tcp.voice — 그 서브도메인에 empty non-terminal이 조용히 생겨난다. 와일드카드가 그 서브도메인의 서브트리를 떠받치고 있었다면 거기서 멈춘다. TLS 챌린지 레코드를 추가하는 사람과 와일드카드를 소유한 사람은 보통 같은 사람이 아니다. 그래서 이게 한 줄짜리 수정이 아니라 몇 시간짜리 장애가 된다.
와일드카드 MX 레코드는 그 자체로 후회의 한 장르다. *.example.com MX 10 mail.example.com은 존재하지 않는 모든 서브도메인이 메일을 받는다는 뜻이다. 스패머가 좋아한다 — 그들이 지어낸 sales@아무거나.example.com으로 보내면 서버가 충실히 받아준다. 편의로 의도한 catch-all이 백스캐터와 스팸 자석이 된다.
DNSSEC가 마지막 반전을 더한다. 서명된 존은 와일드카드 합성이 일어났음을 증명해야 한다. 더 가까운 이름이 존재하지 않았음을 보여주는 NSEC 또는 NSEC3 레코드와, 답이 와일드카드에서 펼쳐졌음을 라벨 수로 드러내는 서명으로. 서명을 잘못하거나, 엉성하게 합성하는 서버를 엄격하게 검증하는 리졸버를 돌리면, 와일드카드 답이 검증에 실패한다. 이제 와일드카드는 검증 안 하는 리졸버에선 되고 검증하는 리졸버에선 깨진다 — 무엇이든 실패할 수 있는 최악의 방식이다. 간헐적으로, 그것도 꼼꼼한 클라이언트에서만.
그래서 뭘 해야 하나
와일드카드가 적은 아니다. 나도 쓴다. 해법은 *.example.com을 “example.com 아래 전부”로 생각하길 멈추고 “다른 식으로는 존재하지 않는 이름을, 가장 가까운 실재 이름 아래에서, 이 특정 레코드 타입들로만 만들어내는 합성기”로 생각하기 시작하는 것이다. 그 모델이 머릿속에 자리 잡으면, 실패가 더는 놀라움이 아니게 된다.
구체적으로: 와일드카드를 운영하면서 그 서브트리 깊은 곳에 레코드도 올린다면, 당신은 empty non-terminal을 만들었고 와일드카드는 더 이상 그 너머에 닿지 못한다는 걸 알아야 한다. 어떤 가지가 구체적 레코드와 와일드카드 폴백을 둘 다 필요로 한다면, 그 가지에도 와일드카드가 필요하다 — api 아래 레코드들과 나란히 *.api.example.com. 그리고 리졸버나 캐시나 제공업체를 탓하기 전에, 누가 지난 화요일에 조용한 언더스코어 레코드를 하나 추가하지 않았는지부터 확인해라.
와일드카드는 안 변했다. 트리가 변했다. 거의 항상 그게 답이다.