현재 도메인 네임 시스템(DNS)을 둘러싼 논의가 활발하며, 36년 된 이 프로토콜에 대대적인 변경을 가하자는 제안이 나오고 있습니다. 인터넷의 이름 서비스는 ARPANET에서 유래했으며, 탄생 이후 지금까지 하위 호환성이 깨진 적은 한 번도 없습니다. 하지만 DNS 전송 메커니즘을 변경하려는 새로운 제안으로 인해 이러한 기록이 깨질 수도 있습니다.
이 게시물에서는 DNS 보안을 위한 두 가지 새로운 기술인 DNS over TLS(DoT)와 DNS over HTTPS(DoH)를 살펴보고, NGINX Open Source와 NGINX Plus를 사용하여 이를 구현하는 방법을 소개합니다.
[편집자 – 이 게시물은 NGINX JavaScript 모듈의 사용 사례를 살펴보는 여러 게시물 중 하나입니다. 전체 목록은 NGINX JavaScript 모듈 사용 사례를 참조하세요.
이 게시물의 코드는 js_import 지시문을 사용하도록 업데이트되었습니다. 이 지시문은 더 이상 사용되지 않는 js_include 지시문을 NGINX Plus R23<.htmla> 이상에서 대체합니다. 자세한 내용은 NGINX JavaScript 모듈 참조 문서를 확인하세요. 구성 예시 섹션에서는 NGINX 구성 및 JavaScript 파일의 올바른 구문을 보여 줍니다.]
DNS의 역사
DNS는 미국 고등연구계획국(ARPA)의 초기 인터넷을 위한 네이밍 서비스를 구축하려는 두 번째 시도였으며, 첫 번째는 1979년 존 포스텔이 IEN-116으로 발표한 인터넷 네임 서버 프로토콜이었습니다. DNS는 계층형으로 설계되었으며, 호스트 이름을 여러 영역으로 분산하여 여러 독립 기관이 관리할 수 있는 구조를 제공했습니다. 최초의 DNS RFC는 1983년에 발표되었으며(RFC 882 및 883), 이후 여러 차례 확장되었지만 당시 정의된 표준에 따라 작성된 클라이언트도 오늘날 여전히 작동합니다.
그렇다면 이제 와서 프로토콜을 변경하려는 이유는 무엇일까요? DNS는 분명 의도한 대로 작동하며, 이는 설계자들의 자신감이 타당했음을 보여 줍니다. 그들은 자신들의 설계가 완벽하다고 확신한 나머지 DNS 패킷에 버전 번호조차 넣지 않았습니다. 이런 주장을 할 수 있는 다른 프로토콜은 거의 떠오르지 않습니다. DNS는 대부분의 프로토콜이 평문으로, 흔히 7비트 ASCII로 통신하던 좀 더 순수한 시대에 고안되었습니다. 하지만 오늘날의 인터넷은 1980년대의 ARPANET보다 훨씬 더 위험한 곳입니다. 오늘날 대부분의 프로토콜은 암호화와 검증을 위해 전송 계층 보안(TLS)을 도입했습니다. DNS 비판론자들은 DNS에도 추가적인 보안 조치가 진작 적용되었어야 한다고 주장합니다.
따라서 DNS가 인터넷에 이름 서비스 프로토콜을 제공하기 위한 두 번째 시도였던 것처럼, DNS over TLS(DoT)와 DNS over HTTPS(DoH)는 DNS 프로토콜을 보호하기 위한 두 번째 시도로 부상하고 있습니다. 첫 번째 시도는 DNSSEC라는 확장 기능이었습니다. 대부분의 최상위 도메인(TLD)이 DNSSEC를 사용하고 있지만, DNSSEC는 애초에 DNS 패킷에 담긴 데이터를 암호화하기 위한 것이 아니라 데이터가 변조되지 않았음을 검증하는 기능만 제공합니다. DoT와 DoH는 DNS를 TLS 터널 안에 캡슐화하는 프로토콜 확장 기능으로, 도입될 경우 36년간 이어져 온 하위 호환성이 끝나게 됩니다.
DoT와 DoH 자세히 알아보기
DoT는 대체로 합리적인 확장 방식으로 여겨진다고 생각합니다. 이미 Internet Assigned Numbers Authority(IANA)로부터 전용 포트 번호(TCP/853)를 할당받았으며, TCP DNS 패킷을 TLS로 암호화된 터널 안에 넣는 방식일 뿐입니다. 이전에도 많은 프로토콜이 이 방식을 사용해 왔습니다. HTTPS는 TLS 터널 안에서 작동하는 HTTP이며, SMTPS, IMAPS, LDAPS는 각각 해당 프로토콜의 보안 버전입니다. DNS는 항상 UDP(또는 특정한 경우 TCP)를 전송 프로토콜로 사용해 왔으므로 TLS 래퍼를 추가하는 것은 큰 변화가 아닙니다.
반면 DoH는 다소 논란의 여지가 있습니다. DoH는 DNS 패킷을 HTTP GET 또는 POST 요청 안에 넣은 다음, HTTPS 연결을 통해 HTTP/2 이상의 프로토콜로 전송합니다. 이는 사실상 다른 HTTPS 연결과 똑같아 보이기 때문에 기업이나 서비스 제공업체는 어떤 요청이 이루어지는지 확인할 수 없습니다. Mozilla를 비롯한 지지자들은 이러한 방식이 사용자가 방문하는 사이트를 엿보는 시선으로부터 숨겨 주어 사용자의 개인정보 보호를 강화한다고 말합니다.
하지만 이는 정확한 설명이 아닙니다. DoH 비판론자들은 DoH가 DNS의 Tor라고 보기는 어렵다고 지적합니다. 브라우저가 DoH를 사용해 조회한 호스트에 최종적으로 연결할 때, 해당 요청은 거의 확실하게 TLS의 서버 이름 표시(SNI) 확장을 사용하기 때문입니다. 여기에는 호스트 이름이 포함되며 평문으로 전송됩니다. 또한 브라우저가 온라인 인증서 상태 프로토콜(OSCP)을 사용해 서버 인증서의 유효성을 확인하려는 경우, 이 과정 역시 평문으로 이루어질 가능성이 큽니다. 따라서 DNS 조회를 모니터링할 수 있는 사람이라면 연결의 SNI나 OCSP 검증에 사용되는 인증서 이름도 읽을 수 있습니다.
많은 사람이 DoH의 가장 큰 문제로 꼽는 것은 브라우저 업체가 사용자의 DoH 요청이 기본적으로 전송될 DNS 서버를 선택한다는 점입니다(예를 들어 미국의 Firefox 사용자는 Cloudflare의 DNS 서버를 사용합니다). DNS 서버 운영자는 사용자의 IP 주소와 사용자가 요청을 보내는 사이트의 도메인 이름을 확인할 수 있습니다. 별것 아닌 것처럼 보일 수 있지만, 일리노이 대학교 연구진은 어떤 웹사이트를 방문했는지 알아낼 수 있다는 사실을 밝혀냈습니다. 웹 페이지 구성 요소에 대한 요청의 목적지 주소, 이른바 페이지 로드 지문(Page Load Fingerprint)만으로도 이를 알아낼 수 있습니다. 이렇게 수집된 정보는 이후 “사용자의 프로필을 작성하고 맞춤형 광고를 노출하는” 데 사용될 수 있습니다.
NGINX가 어떻게 도움이 될까요?
DoT와 DoH가 본질적으로 나쁜 것은 아니며, 사용자의 개인정보 보호를 강화하는 용도로 활용되는 경우도 있습니다. 하지만 공개형 중앙 집중식 DoH 서비스는 사용자의 개인정보 보호에 해롭다는 공감대가 확산되고 있으며 이러한 서비스는 어떤 경우에도 사용하지 않는 것이 좋습니다.
어쨌든 사이트와 앱을 운영한다면 자체 DNS 영역을 관리하고 있을 가능성이 높습니다. 일부는 공개 영역이고, 일부는 비공개 영역이며, 일부는 분할 지평선 방식으로 구성되어 있을 수 있습니다. 언젠가는 자체 DoT 또는 DoH 서비스를 운영하기로 결정할 수도 있습니다. 이때 NGINX가 도움이 될 수 있습니다.
DoT가 제공하는 개인정보 보호 기능은 DNS 보안에 큰 이점을 제공하지만, 현재 사용 중인 DNS 서버가 DoT를 지원하지 않는다면 어떻게 해야 할까요? 이 경우 NGINX를 사용하면 DoT와 표준 DNS 사이에 게이트웨이를 구축할 수 있습니다.
또는 DoT 포트가 차단될 수 있는 상황에서 방화벽을 우회할 수 있는 DoH의 가능성이 마음에 들 수도 있습니다. 이 경우에도 NGINX는 DoH-to-DoT/DNS 게이트웨이를 제공하여 도움을 줄 수 있습니다.
간단한 DoT-DNS 게이트웨이 배포하기
NGINX Stream(TCP/UDP) 모듈은 SSL 종료를 지원하므로 DoT 서비스를 매우 간단하게 설정할 수 있습니다. NGINX 구성 몇 줄만으로 간단한 DoT 게이트웨이를 만들 수 있습니다.
DNS 서버를 위한 upstream 블록과 TLS 종료를 위한 server 블록이 필요합니다:
코드 예제를 로드하는 중…
물론 반대로 수신 DNS 요청을 업스트림 DoT 서버로 전달할 수도 있습니다. 하지만 대부분의 DNS 트래픽은 UDP를 사용하고 NGINX는 DoT와 TCP 기반 DNS 같은 다른 TCP 서비스 간에만 변환할 수 있으므로, 이 방식은 활용도가 떨어집니다.
코드 예제를 로드하는 중…
간단한 DoH-DNS 게이트웨이
DoT 게이트웨이와 비교하면 간단한 DoH 게이트웨이의 구성은 조금 더 복잡합니다. HTTPS 서비스와 Stream 서비스가 모두 필요하며, JavaScript 코드와 NGINX JavaScript module<.htmla>(njs)을 사용하여 두 프로토콜을 상호 변환해야 합니다. 가장 간단한 구성은 다음과 같습니다:
코드 예제를 로드하는 중…
이 구성에서 사용하는 JavaScript 스크립트(nginx_stream.js)에는 다양한 DNS 라이브러리 모듈 파일이 포함되어 있습니다. dns.js 모듈의 dns_decode_level 변수는 DNS 패킷을 어느 정도까지 처리할지 설정합니다. DNS 패킷을 처리하면 성능이 저하될 수밖에 없습니다. 위와 같은 구성을 사용하는 경우 dns_decode_level을 0으로 설정하십시오.
더욱 고급화된 DoH 게이트웨이
NGINX는 HTTP 분야에 상당한 전문성을 갖추고 있으므로, NGINX를 단순한 DoH 게이트웨이로만 사용하는 것은 좋은 기회를 낭비하는 일이라고 생각합니다.
여기에서 사용된 JavaScript 코드는 DNS 패킷을 전체 또는 부분적으로 디코딩하도록 설정할 수 있습니다. 이를 통해 DNS 응답의 최소 TTL을 기준으로 Expires 및 Cache-Control 헤더가 설정되는 DoH 쿼리용 HTTP 콘텐츠 캐시를 구축할 수 있습니다.
다음은 연결 최적화와 콘텐츠 캐싱 및 로깅 지원이 추가된 좀 더 완전한 예제입니다:
코드 예제를 로드하는 중…
NGINX Plus 기능을 활용한 고급 DNS 필터
NGINX Plus 구독을 이용 중이라면 위의 예제를 능동 상태 확인 및 고가용성과 같은 고급 NGINX Plus 기능과 결합하거나, 캐시된 DoH 응답을 관리하기 위해 캐시 제거 API를 사용할 수도 있습니다.
NGINX Plus 키-값 저장소를 사용하여 악성 도메인에 대한 액세스를 효과적으로 차단하는 DNS 응답을 반환함으로써 사용자를 보호하는 DNS 필터링 시스템을 구축할 수도 있습니다. 키-값 저장소의 콘텐츠는 RESTful NGINX Plus API를 통해 동적으로 관리할 수 있습니다.
악성 도메인은 “차단(blocked)”과 “블랙홀(blackhole)”이라는 두 가지 범주로 정의합니다. DNS 필터링 시스템은 도메인의 범주에 따라 해당 도메인에 대한 DNS 쿼리를 다르게 처리합니다.
- For 차단된 도메인의 경우
NXDOMAIN응답을 반환합니다(해당 도메인이 존재하지 않음을 나타냄). - For 블랙홀 도메인의 경우 DNS
A레코드 요청에는 0.0.0.0을 값으로 갖는 단일A레코드를 반환하고,AAAA(IPv6) 레코드 요청에는 ::을 값으로 갖는 단일AAAA레코드를 반환합니다. 그 외 레코드 유형에는 응답 레코드가 없는 응답을 반환합니다.
DNS 필터가 요청을 수신하면 먼저 쿼리된 정확한 FQDN과 일치하는 키가 있는지 확인합니다. 일치하는 키를 찾으면 연결된 값에 지정된 대로 요청을 '스크럽'(차단 또는 블랙홀 처리)합니다. 정확히 일치하는 키가 없으면 차단된 도메인 목록과 블랙홀 처리된 도메인 목록에서 도메인 이름을 검색하고, 일치하는 항목이 있으면 적절한 방식으로 요청을 스크럽합니다.
조회된 도메인이 악성 도메인이 아닌 경우, 시스템은 일반적인 처리를 위해 해당 요청을 Google DNS 서버로 전달합니다.
키-값 저장소 설정
악성으로 간주하는 도메인은 NGINX Plus 키-값 저장소에 다음 두 가지 유형의 항목으로 정의합니다:
- FQDN(정규화된 도메인 이름)별 개별 항목으로, 각 항목의 값은
blocked또는blackhole - 도메인 목록 두 개로, 키 이름은 각각
blocked_domains및blackholed_domains이며, 각 키에는 쉼표로 구분된 값(CSV) 형식의 도메인 목록이 매핑됩니다
다음 키-값 저장소 구성은 stream 컨텍스트에 배치합니다. keyval_zone 지시문은 키-값 저장소에 사용할 메모리 블록을 할당하며, 이 저장소의 이름은 dns_config입니다. 첫 번째 keyval 지시문은 설정된 FQDN과 일치하는 키-값 쌍을 불러오고, 두 번째와 세 번째 지시문은 두 도메인 목록을 정의합니다:
코드 예제를 로드하는 중…
그런 다음 NGINX Plus API를 사용하여 명시적으로 필터링하려는 FQDN 키에 blocked 또는 blackhole 값을 할당하거나, blocked_domains 또는 blackhole_domains 키와 연결된 CSV 형식의 목록을 수정할 수 있습니다. 언제든지 정확한 FQDN을 수정 또는 제거하거나 두 목록 중 하나에 포함된 도메인 집합을 수정할 수 있으며, 변경 사항은 DNS 필터에 즉시 반영됩니다.
요청을 처리할 업스트림 서버 선택
다음 구성은 $dns_response 변수를 로드합니다. 이 변수는 아래의 DNS 쿼리를 수신하는 서버 구성에서 정의된 server 블록의 js_preread 지시문에 의해 값이 설정됩니다. 호출된 JavaScript 코드가 요청을 필터링해야 한다고 판단하면 이 변수를 상황에 따라 blocked 또는 blackhole로 설정합니다.
We use a map 지시어를 사용하여 $dns_response 변수의 값을 $upstream_pool 변수에 할당함으로써 아래의 업스트림 서버 정의에 나오는 업스트림 그룹 중 어느 그룹이 요청을 처리할지 제어합니다. 악성이 아닌 도메인에 대한 쿼리는 기본 Google DNS 서버로 전달되고, 차단되거나 블랙홀 처리된 도메인에 대한 쿼리는 해당 범주의 업스트림 서버에서 처리됩니다.
코드 예제를 로드하는 중…
업스트림 서버 정의
이 구성은 업스트림 서버 그룹인 blocked, blackhole, google을 정의하며, 각 그룹은 각각 차단된 도메인, 블랙홀 도메인, 악성이 아닌 도메인에 대한 요청을 처리합니다.
코드 예제를 로드하는 중…
DNS 쿼리를 수신하는 서버 구성
여기서는 수신 DNS 요청을 처리하도록 stream 컨텍스트에서 수신 대기하는 서버를 정의합니다. js_preread 지시어는 DNS 패킷을 디코딩하고, 패킷의 NAME 필드에서 도메인 이름을 가져온 다음, 키-값 저장소에서 해당 도메인을 조회하는 Javascript 코드를 호출합니다. 도메인 이름이 FQDN 키와 일치하거나 blocked_domains 또는 blackhole_domains 목록에 있는 도메인 중 하나의 영역에 속하면 해당 요청이 제거됩니다. 그렇지 않으면 이름 확인을 위해 Google DNS 서버로 전송됩니다.
코드 예제를 로드하는 중…
이제 거의 다 됐습니다. 마지막으로 server 블록을 추가하기만 하면 됩니다. 이 블록은 쿼리에 대해 블랙홀 처리되거나 차단된 응답을 반환합니다.
코드 예제를 로드하는 중…
이 블록의 서버는 바로 위에 있는 실제 DNS 서버와 거의 동일합니다. 주요 차이점은 요청을 업스트림 DNS 서버 그룹으로 전달하는 대신 클라이언트에 응답 패킷을 보낸다는 것입니다. JavaScript 코드는 포트 9953 또는 9853에서 실행 중인지 감지하며, 패킷을 차단해야 함을 나타내는 플래그를 설정하는 대신 $dns_response에 실제 응답 패킷을 채웁니다. 이것이 전부입니다.
물론 이 필터링을 DoT 및 DoH 서비스에도 적용할 수 있었지만, 여기서는 간단하게 설명하기 위해 표준 DNS를 사용했습니다. DNS 필터링과 DoH 게이트웨이를 통합하는 방법은 독자 여러분의 연습 과제로 남겨 두겠습니다.
필터 테스트
앞서 NGINX Plus API를 사용하여 두 개의 FQDN에 대한 항목과 일부 도메인을 두 도메인 목록에 추가했습니다:
$ curl -s http://localhost:8080/api/5/stream/keyvals/dns_config | jq{
"www.some.bad.host": "blocked",
"www.some.other.host": "blackhole",
"blocked_domains": "bar.com,baz.com",
"blackhole_domains": "foo.com,nginx.com"
}따라서 www.foo.com(블랙홀 처리된 foo.com 도메인 내의 호스트)의 주소 확인을 요청하면 IP 주소가 0.0.0.0인 A 레코드를 받게 됩니다:
$ dig @localhost www.foo.com
; <<>> DiG 9.11.3-1ubuntu1.9-Ubuntu <<>> @localhost www.foo.com
; (1 server found)
;; global options: +mcd
;; Got answer:
;; ->>HEADER,,- opcode: QUERY, status: NOERROR, id: 58558
;; flags: qr aa rd ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available
;; QUESTION SECTION:
; www.foo.com. IN A
;; ANSWER SECTION:
www.foo.com. 300 IN A 0.0.0.0
;; Query time: 0 msec
;; SERVER: 172.0.0.1#53(126.0.0.1)
;; WHEN: Mon Dec 2 14:31:35 UTC 2019
;; MSG SIZEW rcvd: 45코드에 액세스하기
DOH 및 DOT 파일은 제 GitHub 저장소에서 확인할 수 있습니다:
- 다음 폴더에 있는 JavaScript 코드: njs.d/dns folder
- The NGINX configuration in the examples 폴더의 NGINX 구성
NGINX Plus를 사용해 보려면 지금 30일 무료 평가판을 시작하거나 사용 사례에 대해 논의하려면 문의해 주세요.
관련 블로그 게시물
시청하기: KubeCon North America 2023에서의 NGINX Gateway Fabric
이 영상에서는 KubeCon의 NGINX 부스를 찾아가, 저희가 공개한 Kubernetes Gateway API 구현을 살펴봅니다.
F5 NGINX로 대규모 WAF 플릿을 확인하고 관리하는 2가지 방법
앱 보안을 강화하는 두 가지 방법을 소개합니다. NGINX Management Suite를 통해 WAF 운영에 대한 가시성을 높이고, NGINX App Protect WAF를 활용해 코드형 보안을 구현하는 것입니다.
Ingress 컨트롤러 선택 가이드, 4부: NGINX Ingress Controller 옵션
개발 주체, 개발 철학, 프로덕션 환경 준비도, 보안 및 지원을 기준으로 어떤 NGINX Ingress 컨트롤러가 가장 적합한지 알아보세요.
진화하는 공격 환경에 NGINX App Protect Denial of Service가 적응하는 방법
NGINX App Protect Denial of Service(DoS)가 계층 7 DoS 공격을 방어하기 위해 완화 방법을 실시간으로 조정하는 방식을 설명합니다. 정상적인 사용자 행동과 서버 상태에 대한 모델을 생성하여 공격을 나타내는 이상 징후를 감지하고 맞춤형 완화 시그니처를 적용할 수 있습니다.
NGINX Instance Manager를 활용한 대규모 구성 업데이트 및 인증서 관리
NGINX Instance Manager 버전 1.0에는 두 가지 새로운 기능이 도입되었습니다. NGINX 인스턴스에 태그와 사용자 역할을 지정하면 전체 그룹에 구성을 한 번에 적용할 수 있습니다. 인증서 검사는 만료 예정인 인증서를 감지하여 서비스 중단을 방지할 수 있도록 인증서를 교체할 수 있게 해줍니다.
Netflix가 자사 CDN의 핵심으로 NGINX를 선택한 이유
Netflix가 글로벌 콘텐츠 전송 네트워크인 Open Connect의 스트리밍 미디어 기술로 NGINX를 선택한 이유를 NGINX 개발자 Gleb Smirnoff에게서 알아보세요.