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 레코드. 이들 하나하나가 부모에 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. 그리고 리졸버나 캐시나 제공업체를 탓하기 전에, 누가 지난 화요일에 조용한 언더스코어 레코드를 하나 추가하지 않았는지부터 확인해라.
와일드카드는 안 변했다. 트리가 변했다. 거의 항상 그게 답이다.