평문 DNS가 남기는 흔적
전통적인 DNS 질의는 UDP/53 위를 평문으로 흐른다. 사용자가 접속하려는 도메인이 그대로 노출되기 때문에 같은 네트워크의 관찰자나 경로상의 ISP는 어떤 서비스에 접속했는지 손쉽게 수집할 수 있다. HTTPS로 본문을 암호화해도 SNI와 DNS가 평문이면 목적지 도메인은 사실상 공개된다. 사내망이나 VPN 게이트웨이에서 자체 리졸버를 운영할 때, 상위 리졸버로 나가는 구간이 평문이면 이 노출 지점을 그대로 넘겨받는 셈이다.
DNS over TLS(DoT, RFC 7858)는 이 구간을 TLS로 감싸 TCP/853에서 질의·응답을 암호화한다. 상위 리졸버와의 트래픽을 도청·변조로부터 보호하고, 인증서 검증으로 상대 리졸버가 정말 의도한 서버인지 확인한다.
DoT와 DoH 중 무엇을 쓸까
암호화 목적은 같지만 운영 관점의 차이가 크다.
| 항목 | DoT | DoH |
|---|---|---|
| 포트 | 853 (전용) | 443 (HTTPS와 혼재) |
| 차단 난이도 | 포트 기반으로 쉬움 | 일반 HTTPS와 구분 어려움 |
| 운영 가시성 | DNS 트래픽 식별 쉬움 | 애플리케이션이 우회 가능 |
| 인프라 적합성 | 중앙 리졸버 강제에 유리 | 브라우저 개별 설정에 유리 |
사내 정책으로 리졸버를 강제하고 로그·감사를 남겨야 하는 인프라라면 포트가 분리된 DoT가 관리하기 쉽다. 반대로 개별 단말이 검열망을 우회해야 하는 상황이라면 DoH가 유리하다.
아키텍처 개요
구성은 단순하다. 사내 클라이언트는 내부 리졸버(예: 10.0.0.53)만 바라보고, 내부 리졸버는 자신을 재귀 캐시로 두면서 상위 공용 리졸버로 나가는 구간에만 DoT를 적용한다. 즉 클라이언트 → (평문, 내부망) → Unbound → (DoT, 인터넷) → 상위 리졸버 형태다. 내부망 구간은 신뢰 경계 안이므로 평문을 유지해 성능을 확보하고, 외부로 나가는 노출 구간만 암호화한다.
Unbound으로 DoT 포워딩 구성
Unbound은 재귀 캐시와 DoT 포워딩을 함께 처리할 수 있다. 아래 설정은 로컬 캐시로 동작하면서 상위 리졸버로는 TLS로만 질의한다.
# /etc/unbound/unbound.conf.d/dot.conf
server:
interface: 10.0.0.53
interface: 127.0.0.1
access-control: 10.0.0.0/8 allow
access-control: 127.0.0.0/8 allow
# 캐시 최소 TTL로 반복 질의 절감
cache-min-ttl: 60
prefetch: yes
# DoT 인증서 신뢰 앵커
tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"
forward-zone:
name: "."
forward-tls-upstream: yes
# 반드시 인증서 검증용 이름(@853#hostname)을 명시
forward-addr: 9.9.9.9@853#dns.quad9.net
forward-addr: 1.1.1.1@853#cloudflare-dns.com
핵심은 forward-tls-upstream: yes와 주소 뒤의 #hostname이다. 호스트명을 빼면 TLS는 맺어지지만 인증서의 이름 검증을 건너뛰어 중간자 공격에 노출된다. 반드시 명시한다.
클라이언트를 내부 리졸버로 강제
단말이 임의의 공용 리졸버를 직접 쓰지 못하도록 리졸버를 고정한다. systemd-resolved를 쓰는 리눅스라면 다음과 같이 지정한다.
# /etc/systemd/resolved.conf
[Resolve]
DNS=10.0.0.53
Domains=~.
# 내부 리졸버가 이미 DoT를 처리하므로 여기선 opportunistic로 충분
DNSOverTLS=opportunistic
# 평문 폴백 차단
FallbackDNS=
여기에 더해 방화벽에서 내부 리졸버 IP를 제외한 목적지의 853/53 아웃바운드를 차단하면 우회를 막을 수 있다.
# 내부 리졸버 외 외부 DNS 차단 (nftables 예시)
nft add rule inet filter output ip daddr != 10.0.0.53 \
udp dport 53 drop
nft add rule inet filter output ip daddr != 10.0.0.53 \
tcp dport { 53, 853 } drop
동작 검증
구성 후에는 실제로 암호화 경로로 나가는지 확인한다. kdig로 DoT 질의를 직접 던져보고, 상위 구간을 패킷으로 확인한다.
# DoT로 직접 질의 (TLS 핸드셰이크·인증서까지 확인)
kdig -d @9.9.9.9 +tls-ca +tls-host=dns.quad9.net example.com
# 내부 리졸버가 상위로 나갈 때 853만 쓰는지 확인
sudo tcpdump -ni eth0 'tcp port 853 or udp port 53'
tcpdump 결과에서 외부로 향하는 udp/53이 보이지 않고 tcp/853만 나타나야 정상이다. 평문 53이 섞여 나온다면 폴백 경로가 열려 있는 것이다.
운영 시 주의점
첫째, DoT는 TCP 기반이라 커넥션 재사용이 중요하다. Unbound이 상위와 연결을 유지하도록 캐시·prefetch를 켜 핸드셰이크 비용을 줄인다. 둘째, 상위 리졸버가 853을 막거나 장애가 나면 전면 장애로 번진다. 서로 다른 사업자의 리졸버를 최소 둘 이상 등록해 이중화한다. 셋째, 인증서 신뢰 앵커(tls-cert-bundle) 경로와 시스템 시계를 점검한다. 시각이 틀어지면 인증서 검증이 실패해 해석이 통째로 멈춘다. 마지막으로 암호화는 도청을 막을 뿐 질의 내용 자체는 상위 리졸버가 본다. 로그 최소화나 자체 재귀 해석 여부는 별도 정책으로 다뤄야 한다.