8장 : DNS
DNS 심화와 실무
DNS의 기본 동작과 레코드를 이해했으니, 이제 실무에서 DNS가 어떻게 활용되고 어떤 보안 위협이 존재하는지 살펴보겠습니다.
DNS 라운드 로빈
하나의 도메인에 여러 개의 A 레코드를 설정하면, DNS 서버는 질의마다 IP 주소의 순서를 바꿔서 응답합니다. 이것이 DNS 라운드 로빈입니다.
A 레코드 설정
example.com → 10.0.0.1 (서버 A)
example.com → 10.0.0.2 (서버 B)
example.com → 10.0.0.3 (서버 C)
질의 1 응답 순서: [10.0.0.1, 10.0.0.2, 10.0.0.3]
질의 2 응답 순서: [10.0.0.2, 10.0.0.3, 10.0.0.1]
질의 3 응답 순서: [10.0.0.3, 10.0.0.1, 10.0.0.2]
클라이언트는 보통 첫 번째 IP를 사용
→ 트래픽이 세 서버에 분산
문제점
서버 B 다운! → DNS는 모른다 (헬스 체크 없음)
질의 2 → 10.0.0.2 (다운된 서버) → 접속 실패!
TTL 동안 캐시 → 분산이 불균등
실무: DNS 라운드 로빈 단독 사용 X
→ L4/L7 로드 밸런서와 함께 사용GeoDNS와 글로벌 서비스 라우팅
GeoDNS는 클라이언트의 위치에 따라 다른 IP 주소를 응답하는 DNS 서비스입니다.
API 서버
example.com
│
┌─────────────┼─────────────┐
한국에서 미국에서 유럽에서
질의 시 질의 시 질의 시
│ │ │
10.0.1.1 10.0.2.1 10.0.3.1
(도쿄 서버) (버지니아 서버) (프랑크푸르트 서버)
위치 판단 기준
1. ECS (EDNS Client Subnet): 리졸버가 클라이언트 서브넷 전달
2. 리졸버 IP: 한국 ISP 리졸버 → 한국 사용자로 추정
3. GeoIP 데이터베이스: IP → 위치 매핑 DB
한계
8.8.8.8 같은 글로벌 리졸버 사용 시 위치 추정 부정확
→ ECS 확장이 이 문제를 상당 부분 해결| DNS 라우팅 정책 | 설명 | 사용 사례 |
|---|---|---|
| 단순 (Simple) | 하나의 레코드 응답 | 단일 서버 |
| 라운드 로빈 | 여러 레코드 순환 | 기본 부하 분산 |
| 가중치 (Weighted) | 비율에 따라 분배 (70:30) | A/B 테스트, 점진적 이전 |
| 지리적 (Geolocation) | 클라이언트 위치 기반 | 지역별 콘텐츠 제공 |
| 지연 시간 (Latency) | 가장 빠른 리전으로 | 글로벌 서비스 |
| 장애 조치 (Failover) | 주 서버 다운 시 백업 | 고가용성 |
| 다중 값 (Multi-value) | 헬스 체크된 여러 IP | 향상된 라운드 로빈 |
평상시:
example.com → 10.0.1.1 (Primary, 서울)
헬스 체크: ✓ 정상
장애 발생:
example.com → 10.0.1.1 (Primary) 헬스 체크: ✗ 실패!
→ 10.0.2.1 (Secondary, 도쿄)로 자동 전환
복구 후:
example.com → 10.0.1.1 (Primary) 헬스 체크: ✓ 복구
→ 다시 Primary로 자동 복귀
Route 53, Cloudflare 등이 이 기능 제공
TTL을 짧게 설정해야 빠른 전환 가능DNS over HTTPS와 DNS over TLS
전통적인 DNS 질의는 평문 UDP로 전송됩니다. 이것은 누구든 네트워크 트래픽을 감청하면 이 사용자가 어떤 사이트에 접속하려는지 알 수 있다는 의미입니다.
기존 DNS (평문 UDP):
사용자 ──UDP 53──→ DNS 리졸버
"www.비밀사이트.com의 IP?"
↑
네트워크 관찰자 (ISP, 공유기, 공격자)
→ 사용자의 모든 방문 사이트 목록 노출!
HTTPS로 암호화해도:
DNS 질의: 평문 → 방문지 노출 ✗
HTTP 내용: 암호화 → 내용 보호 ✓
SNI 필드: 평문 → 도메인 노출 ✗ (ECH로 해결 중)| 프로토콜 | 포트 | 암호화 | 식별 가능 | 장점 | 단점 |
|---|---|---|---|---|---|
| DNS (전통) | 53/UDP | 없음 | DNS 트래픽 | 빠름, 단순 | 프라이버시 없음 |
| DoT | 853/TCP | TLS | DNS 전용 포트 | 표준 암호화 | 차단 쉬움 |
| DoH | 443/TCP | HTTPS | HTTPS와 구분 불가 | 차단 어려움 | 디버깅 어려움 |
DNS over TLS(DoT)는 DNS 질의를 TLS로 암호화합니다. 853번 포트를 사용합니다. DNS 트래픽을 식별하기가 비교적 쉬워 네트워크 관리자가 차단할 수 있습니다.
DNS over HTTPS(DoH)는 DNS 질의를 HTTPS 프로토콜로 캡슐화합니다. 443번 포트를 사용하므로, 일반 HTTPS 트래픽과 구분되지 않습니다.
# 현재 시스템의 DNS 서버 확인
# Windows
ipconfig /all | findstr "DNS Servers"
# Linux
cat /etc/resolv.conf
# DoH 지원 DNS 서버로 직접 질의
curl -s "https://cloudflare-dns.com/dns-query?name=example.com&type=A" \
-H "Accept: application/dns-json" | python3 -m json.tool
# DoH 응답 예시:
# {
# "Status": 0,
# "Answer": [
# {
# "name": "example.com",
# "type": 1,
# "TTL": 86400,
# "data": "93.184.216.34"
# }
# ]
# }DNS 캐시 포이즈닝과 DNSSEC
DNS는 인터넷의 핵심 인프라이기 때문에, 공격 대상이 되기도 합니다.
정상 흐름
클라이언트 → 리졸버 → 권한 서버
"bank.com = 10.0.0.1" (정상)
공격
1. 공격자가 리졸버에 bank.com 질의를 발생시킴
2. 권한 서버 응답보다 먼저 위조 응답을 전송
"bank.com = 200.0.0.1" (공격자 서버)
3. 리졸버가 위조 응답을 캐시에 저장
4. TTL 동안 모든 클라이언트가 공격자 서버로 연결!
클라이언트 → 리졸버 (캐시: bank.com = 200.0.0.1)
↓
200.0.0.1 (피싱 사이트)
→ 로그인 정보 탈취!
→ URL은 정상처럼 보임!
Kaminsky Attack (2008)
Transaction ID(16비트)를 추측하여 대규모 위조 가능
→ DNS 보안의 근본적 취약점 노출DNSSEC(DNS Security Extensions)는 이 문제를 해결하기 위한 보안 확장입니다.
. (Root)
│ KSK: 루트 키 (Trust Anchor)
│ 서명: .com의 DS 레코드에 서명
▼
.com (TLD)
│ ZSK: .com 영역 서명 키
│ 서명: example.com의 DS 레코드에 서명
▼
example.com
│ ZSK: example.com 영역 서명 키
│ 서명: A 레코드에 서명
▼
A: 93.184.216.34 + RRSIG (디지털 서명)
리졸버 검증 과정
1. 루트 키(Trust Anchor)는 미리 알고 있음
2. 루트의 서명으로 .com 키 검증
3. .com의 서명으로 example.com 키 검증
4. example.com의 서명으로 A 레코드 검증
→ 체인이 끊기지 않으면 "이 레코드는 진짜다"
DNSSEC 관련 레코드
RRSIG: 레코드의 디지털 서명
DNSKEY: 영역의 공개 키
DS: 하위 영역의 키 해시 (위임 서명)
NSEC/NSEC3: "이 이름은 존재하지 않음" 증명# DNSSEC 서명 확인
dig +dnssec example.com
# DNSSEC 검증 (AD 플래그 확인)
dig +dnssec +adflag example.com | grep "flags"
# ;; flags: qr rd ra ad; → ad = Authenticated Data (검증 성공)
# DNSKEY 레코드 조회
dig DNSKEY example.com +short
# DS 레코드 확인 (상위 영역에 저장)
dig DS example.com +shortDNSSEC은 데이터의 무결성과 인증을 보장하지만, 기밀성은 보장하지 않습니다. 기밀성까지 보호하려면 DoT나 DoH를 함께 사용해야 합니다.
DNS 실무 문제 해결
사이트 접속 불가?
│
├── ping IP주소 → 성공? → DNS 문제!
│ │
│ nslookup domain │
│ ├── 응답 없음 → DNS 서버 장애 / 방화벽
│ ├── NXDOMAIN → 도메인 미등록 / 만료
│ ├── SERVFAIL → 권한 서버 장애 / DNSSEC 실패
│ └── 잘못된 IP → 캐시 포이즈닝 / 레코드 오류
│
└── ping IP주소 → 실패? → 네트워크 문제 (DNS 아님)| 문제 | 증상 | 원인 | 해결 |
|---|---|---|---|
| DNS 캐시 오래됨 | 서버 이전 후 옛 IP 접속 | TTL 내 캐시 유지 | 캐시 플러시, TTL 미리 낮추기 |
| NXDOMAIN | 사이트를 찾을 수 없음 | 도메인 미등록/만료 | 도메인 등록/갱신 확인 |
| SERVFAIL | DNS 해석 실패 | 권한 서버 장애, DNSSEC 오류 | 권한 서버 상태 확인 |
| 느린 DNS | 페이지 로딩 지연 | ISP DNS 느림 | 1.1.1.1 또는 8.8.8.8 사용 |
| DNS 하이재킹 | 엉뚱한 사이트 접속 | 악성 DNS, 라우터 해킹 | DNS 서버 직접 지정, DoH 사용 |
import socket
import time
def dns_health_check(domains, dns_servers=None):
"""여러 도메인의 DNS 해석 상태 확인"""
results = []
for domain in domains:
start = time.time()
try:
ip = socket.gethostbyname(domain)
elapsed = (time.time() - start) * 1000
status = "OK"
results.append((domain, ip, f"{elapsed:.1f}ms", status))
except socket.gaierror as e:
elapsed = (time.time() - start) * 1000
results.append((domain, "-", f"{elapsed:.1f}ms", str(e)))
# 결과 출력
print(f"{'도메인':<25} {'IP':<18} {'시간':<10} {'상태'}")
print("-" * 70)
for domain, ip, time_str, status in results:
print(f"{domain:<25} {ip:<18} {time_str:<10} {status}")
domains = [
"google.com",
"github.com",
"naver.com",
"example.com",
"nonexistent-domain-test.xyz",
]
dns_health_check(domains)다음 장에서는 웹 개발자가 매일 사용하지만 깊이 이해하는 경우가 드문 프로토콜, HTTP를 자세히 살펴보겠습니다.