NGINX를 DoT 또는 DoH Gateway 로 사용하기
오늘 NGINX를 DoT 또는 DoH Gateway 로 사용에 대하여 이야기 할 것입니다.
현재 DNS (Domain Name System)를 둘러싼 많은 이야기가 있으며 현재 36년 된 프로토콜에 대한 대규모 변경 사항이 제안되었습니다. ARPANET 에 기원을 둔 인터넷의 이름 서비스는 처음부터 이전 버전과 호환되는 중단 현상이 없었습니다. 그러나 DNS 전송 메커니즘을 변경하기 위한 새로운 제안이 이를 변경하려고 할 수도 있습니다.
이 포스트에서는 DNS 보안을 위한 두 가지 새로운 기술인 DNS over TLS(DoT) 및 DNS over HTTPS(DoH)를 살펴보고 NGINX 오픈 소스 및 NGINX Plus를 사용하여 이를 구현하는 방법을 보여줍니다.
[ Editor – 이 포스트는 NGINX JavaScript 모듈의 사용 사례를 탐색하는 여러 포스트 중 하나입니다.
이 포스트의 코드는 NGINX Plus R23 이상에서 더이상 사용되지 않는 js_include 지시문을 대체하는 js_import 지시문을 사용하도록 업데이트되었습니다. 구성 예 섹션은 NGINX 구성 및 JavaScript 파일에 대한 올바른 구문을 보여줍니다.]
목차
1. DNS의 역사
2. DoT 및 DoH 자세히 알아보기
3. NGINX가 Gateway 로 어떻게 도움이 될까요?
4. 간단한 DoT-DNS Gateway 배포
5. 간단한 DoH-DNS Gateway
6. 더 발전된 DoH Gateway
7. NGINX Plus 기능을 사용하는 고급 DNS 필터
8. Key-Value Store 설정
9. 요청을 처리할 Upstream 서버 선택
10. Upstream 서버 정의
11. DNS 쿼리를 수신 대기하는 서버 구성
12. 필터 테스트
13. 코드에 액세스
1. DNS의 역사
DNS는 ARPA(Advanced Research Project Agency)의 초기 인터넷을 위한 명명 서비스를 만들기 위한 두 번째 시도였으며, 첫 번째는 John Postel이 1979년에 IEN-116 으로 게시한 인터넷 이름 서버 프로토콜입니다. DNS는 계층적으로 설계되었으며 호스트명이 영역으로 분산되고 여러 개별 기관에서 관리할 수 있는 구조를 제공합니다. DNS에 대한 첫 번째 RFC는 1983년에 게시되었으며(RFC 882 및 883 ), 수년에 걸쳐 여러 확장이 있었지만, 당시 정의된 표준에 따라 작성된 클라이언트는 오늘날에도 여전히 작동합니다.
그렇다면 지금 프로토콜을 변경해야 하는 이유는 무엇일까? 분명히 의도한 대로 작동하며, DNS 패킷에 버전 번호를 포함하지 않는 것이 옳다고 자신했던 작성자의 확신을 정당화합니다. 이러한 주장을 할 수 있는 다른 프로토콜은 생각할 수 없습니다. DNS는 대부분의 프로토콜이 일반 텍스트와 가끔 7비트 ASCII 인 순진했던 시대에 고안되었지만 오늘날의 인터넷은 1980년대의 ARPANET보다 훨씬 무서운 곳입니다. 오늘날 대부분의 프로토콜은 암호화 및 확인을 위해 TLS(Transport Layer Security)를 추구합니다. DNS 비평가들은 일부 추가 보안에 대한 기한이 지났다고 주장합니다.
따라서 DNS가 인터넷에 이름 서비스 프로토콜을 제공하려는 두 번째 시도인 것과 마찬가지로 DoT(DNS over TLS)와 DoH(DNS over HTTPS)는 DNS 프로토콜 보안을 위한 두 번째 시도로 부상하고 있습니다. 첫 번째 시도는 DNSSEC이었습니다. 대부분의 최상위 도메인(TLD)은 DNSSEC를 사용하지만 DNS 패킷에 포함된 데이터를 암호화하기 위한 것은 아닙니다. 그저 데이터가 변조되지 않았다는 확인만 제공합니다. DoT 및 DoH는 DNS를 TLS 터널 내부로 감싸는 프로토콜 확장이며 채택될 경우 36년 동안의 이전 버전과의 호환이 종료됩니다.
2. DoT 및 DoH 자세히 알아보기
내 생각에 DoT는 대체로 합리적인 확장으로 간주됩니다. IANA(Internet Assigned Numbers Authority)에 의해 자체 포트 번호(TCP/853)가 이미 할당되었으며 TLS 암호화 터널 내에서 TCP DNS 패킷을 간단히 래핑합니다. 많은 프로토콜이 이전에 이 작업을 수행했습니다. HTTPS는 TLS 터널 내부의 HTTP이고 SMTPS, IMAPS 및 LDAPS는 이러한 프로토콜의 보안 버전입니다. DNS는 항상 전송 프로토콜로 UDP(또는 경우에 따라 TCP)를 사용했기 때문에 TLS 래퍼를 추가하는 것은 큰 변화가 아닙니다.
반면에 DoH는 조금 더 논란의 여지가 있습니다. DoH는 DNS 패킷을 받아 HTTP GET 또는 POST 요청 내부에 래핑한 다음 HTTPS 연결을 통해 HTTP/2 이상을 사용하여 전송합니다. 이것은 모든 의도와 목적에 다른 HTTPS 연결과 마찬가지로 나타나며 기업이나 서비스 공급자가 어떤 요청이 이루어지고 있는지 확인하는 것이 불가능합니다. Mozilla 및 기타 지지자들은 이러한 관행이 방문하는 사이트를 비공개로 유지하여 사용자의 개인 정보를 보호한다고 말합니다.
그러나 그것은 사실이 아닙니다. DoH를 비판하는 사람들은 브라우저가 결국 DoH를 사용하여 조회한 호스트에 연결될 때 요청이 TLS SNI( Server Name Indication ) 확장을 거의 확실하게 사용하기 때문에 DNS의 Tor 가 아니라고 지적합니다. 여기에는 호스트명이 포함되며 일반 텍스트로 전송됩니다. 또한 브라우저가 온라인 인증서 상태 프로토콜 (OSC)을 사용하여 서버의 인증서를 검증하려고 하면 해당 프로세스가 일반 텍스트에서도 발생할 수 있습니다. 따라서 DNS 조회를 모니터링할 수 있는 기능이 있는 사람은 연결의 SNI 또는 OCSP 유효성 검사의 인증서 이름을 읽을 수 있습니다.
많은 사람들에게 DoH의 가장 큰 문제는 브라우저 공급업체가 사용자의 DoH 요청이 기본적으로 전송되는 DNS 서버를 선택한다는 것입니다(예를 들어 미국의 Firefox 사용자의 경우 DNS 서버는 Cloudflare에 속함 ) DNS 서버 운영자는 사용자의 IP 주소와 그들이 요청하는 사이트의 도메인 이름을 볼 수 있습니다. 별거 아닌 것 같지만 일리노이 대학의 연구원들은 페이지 로드 핑거프린트라고 하는 웹 페이지 요소에 대한 요청의 대상 주소만으로 사람이 방문한 웹 사이트를 추론 할 수 있음을 발견했습니다. 그런 다음 해당 정보를 사용하여 “광고를 위한 사용자 프로필 및 타겟팅”을 수행할 수 있습니다.
3. NGINX가 Gateway 로 어떻게 도움이 될까요?
DoT와 DoH는 본질적으로 악하지 않으며 사용자 개인 정보 보호를 강화하는 사용 사례가 있습니다. 그러나 공용 중앙 집중식 DoH 서비스가 사용자 개인 정보 보호에 좋지 않다는 공감대가 커지고 있으므로 어떤 경우에도 서비스를 사용하지 않는 것이 좋습니다.
어떤 경우든 사이트와 앱의 경우 일부는 공개, 일부는 비공개, 일부는 분할된 자체 DNS 영역을 관리할 수 있습니다.어느 시점에서 자체 DoT 또는 DoH 서비스를 실행하기로 결정할 수도 있습니다. 이것이 NGINX가 도울 수 있는 곳입니다.
DoT가 제공하는 향상된 개인 정보 보호 기능은 DNS 보안에 몇 가지 큰 이점을 제공하지만 현재 DNS 서버가 DoT를 지원하지 않는다면 어떻게 될까요? NGINX는 DoT와 표준 DNS 사이에 Gateway 를 제공하여 도움을 줄 수 있습니다.
또는 DoT 포트가 차단될 수 있는 경우 DoH의 방화벽 파괴 가능성이 마음에 걸릴 수도 있습니다. 하지만 NGINX는 DoH-to-DoT/DNS Gateway 를 제공함으로써 도움을 줄 수 있습니다.
4. 간단한 DoT-DNS Gateway 배포
NGINX Stram(TCP/UDP) 모듈은 SSL Termination를 지원하므로 실제로 DoT 서비스를 설정하는 것은 정말 간단합니다. NGINX 구성 몇 줄만으로 간단한 DoT Gateway 를 만들 수 있습니다.
Upstream DNS 서버용 블록과 server TLS Termination용 블록이 필요합니다.
stream {
# DNS upstream pool
upstream dns {
zone dns 64k;
server 8.8.8.8:53;
}
# DoT server for decryption
server {
listen 853 ssl;
ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;
ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;
proxy_pass dns;
}
}
물론 다른 방법으로 들어오는 DNS 요청을 Upstream DoT 서버로 전달할 수도 있습니다. 그러나 대부분의 DNS 트래픽은 UDP이고 NGINX는 DoT와 TCP 기반 DNS와 같은 다른 TCP 서비스 간에만 변환할 수 있기 때문에 유용성이 떨어집니다.
stream {
# DoT upstream pool
upstream dot {
zone dot 64k;
server 8.8.8.8:853;
}
# DNS server for upstream encryption
server {
listen 53;
proxy_ssl on;
proxy_pass dot;
}
}
5. 간단한 DoH-DNS Gateway
DoT Gateway 와 비교할 때 간단한 DoH Gateway 의 구성은 조금 더 복잡합니다. HTTPS 서비스와 Stream 서비스가 모두 필요하며 JavaScript 코드와 NGINX JavaScript 모듈 (njs)을 사용하여 두 프로토콜 간 변환합니다. 가장 간단한 구성은 다음과 같습니다.
http {
# This is our upstream connection to the njs translation process
upstream dohloop {
zone dohloop 64k;
server 127.0.0.1:8053;
}
# This virtual server accepts HTTP/2 over HTTPS
server {
listen 443 ssl http2;
ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;
ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;
# Return 404 for non-DoH requests
location / {
return 404 "404 Not Found\n";
}
# Here we downgrade the HTTP/2 request to HTTP/1.1 and forward it to
# the DoH loop service
location /dns-query {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://dohloop;
}
}
}
stream {
# Import the JavaScript file that processes the DoH(?) packets
js_import /etc/nginx/njs.d/dns/dns.js;
# DNS upstream pool (can also be DoT)
upstream dns {
zone dns 64k;
server 8.8.8.8:53;
}
# DNS over HTTPS (gateway) translation process
# Upstream can be either DNS (TCP) or DoT
server {
listen 127.0.0.1:8053;
js_filter dns.filter_doh_request;
proxy_pass dns;
}
}
이 구성은 패킷을 DNS 서비스로 보내는 데 필요한 최소한의 처리를 수행합니다. 이 사용 사례에서는 Upstream DNS 서버가 다른 필터링, 로깅 또는 보안 기능을 수행한다고 가정합니다.
이 구성에 사용된 JavaScript 스크립트( nginx_stream.js )에는 다양한 DNS 라이브러리 모듈 파일이 포함되어 있습니다. dns.js 모듈 내에서 dns_decode_level 변수는 DNS 패킷에서 수행되는 처리량을 설정합니다. DNS 패킷을 처리하면 분명히 성능은 저하됩니다. 위와 같은 구성을 사용하는 경우 로 설정을 dns_decode_level을 0으로 합니다.
6. 더 발전된 DoH Gateway
여기 NGINX에서 우리는 HTTP에 정말 능숙합니다. 따라서 단순한 DoH Gateway 로만 NGINX를 사용하는 것은 낭비된 기회라고 생각합니다.
여기에 사용된 JavaScript 코드는 DNS 패킷의 전체 또는 부분 디코딩을 수행하도록 설정할 수 있습니다. Cash-Control 을 통해 DNS 응답의 최소 TTL을 기반으로 설정된 만료 및 헤더를 사용하여 DoH 쿼리에 대한 HTTP 콘텐츠 캐시를 구축할 수 있습니다.
추가 연결 최적화와 콘텐츠 캐싱 및 로깅 지원이 포함된 보다 완전한 예는 다음과 같습니다.
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format dns '$remote_addr - $remote_user [$time_local] "$request" '
'[ $msec, $request_time, $upstream_response_time $pipe ] '
'$status $body_bytes_sent "-" "-" "$http_x_forwarded_for" '
'$upstream_http_x_dns_question $upstream_http_x_dns_type '
'$upstream_http_x_dns_result '
'$upstream_http_x_dns_ttl $upstream_http_x_dns_answers '
'$upstream_cache_status';
access_log /var/log/nginx/doh-access.log dns;
upstream dohloop {
zone dohloop 64k;
server 127.0.0.1:8053;
keepalive_timeout 60s;
keepalive_requests 100;
keepalive 10;
}
proxy_cache_path /var/cache/nginx/doh_cache levels=1:2 keys_zone=doh_cache:10m;
server {
listen 443 ssl http2;
ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;
ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;
ssl_session_cache shared:ssl_cache:10m;
ssl_session_timeout 10m;
proxy_cache_methods GET POST;
location / {
return 404 "404 Not Found\n";
}
location /dns-query {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_cache doh_cache;
proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body;
proxy_pass http://dohloop;
}
}
}
stream {
js_import /etc/nginx/njs.d/dns/dns.js;
# DNS upstream pool
upstream dns {
zone dns 64k;
server 8.8.8.8:53;
}
# DNS over TLS upstream pool
upstream dot {
zone dot 64k;
server 8.8.8.8:853;
}
# DNS over HTTPS (gateway) service
# This time we’ve used a DoT upstream
server {
listen 127.0.0.1:8053;
js_filter dns.filter_doh_request;
proxy_ssl on;
proxy_pass dot;
}
}
7. NGINX Plus 기능을 사용하는 고급 DNS 필터
NGINX Plus 구독이 있는 경우 위의 예를 활성 상태 확인 및 고가용성과 같은 일부 고급 NGINX Plus 기능과 결합하거나 캐시된 DoH 응답을 관리하기 위해 캐시 제거 API를 사용할 수도 있습니다.
또한 NGINX Plus key-value Store를 사용하여 사용자에 대한 액세스를 효과적으로 차단하는 DNS 응답을 반환하여 악의적인 도메인으로부터 사용자를 보호하는 DNS 필터링 시스템을 구축할 수 있습니다.
RESTful NGINX Plus API를 사용하여 동적으로 key-value Store의 콘텐츠를 관리합니다.
우리는 “blocked”와 “blackhole”이라는 두 가지 범주의 악성 도메인을 정의합니다. DNS 필터링 시스템은 범주에 따라 도메인에 대한 DNS 쿼리를 다르게 처리합니다.
- blocked 도메인 의 경우 NXDOMAIN(도메인이 존재하지 않음을 나타냄) 을 반환합니다.
- blackhole 도메인 의 경우 레코드 요청에 대한 응답으로 단일 DNS A레코드 0.0.0.0을 반환하거나 (IPv6) 레코드 요청에 대한 응답으로 :: A단일 레코드를 반환합니다. 다른 레코드 유형의 경우 응답이 0인 응답을 반환합니다.AAAAAAAA
DNS 필터가 요청을 받으면 먼저 쿼리된 FQDN과 정확히 일치하는 Key를 확인합니다. 하나를 찾으면 연결된 값에 지정된 대로 요청을 “스크럽”( blocked 또는 blackhole ) 합니다. 정확히 일치하는 항목이 없으면 blocked된 도메인과 blackhole 된 도메인 중 하나인 두 목록에서 도메인 이름을 찾고 일치하는 항목이 있으면 적절한 방식으로 요청을 제거합니다.
쿼리된 도메인이 악성이 아닌 경우 시스템은 정기적인 처리를 위해 이를 Google DNS 서버로 전달합니다.
8. Key-Value Store 설정
NGINX Plus key-value Store에 있는 두 종류의 항목에서 악성으로 간주되는 도메인을 정의합니다.
- FQDN(정규화된 도메인 이름)에 대한 개별 항목(각각 값은
blocked또는blackhole) blocked_domains및blackholed_domains라는 Key가 있는 두 개의 도메인 목록은 각각 CSV(쉼표로 구분된 값) 형식의 도메인 목록에 매핑됩니다.
key-value Store에 대한 다음 구성은 stream 컨텍스트에 포함됩니다. 지시문 은 dns_config keyval_zone 이라는 key-value Store에 대한 메모리 블록을 할당합니다. 첫 번째 keyval 지시문은 설정된 일치하는 모든 FQDN key-value 에 로드되고 두 번째 및 세 번째 지시문은 두 도메인 목록을 정의합니다.
# Key-value store for blocking domains (NGINX Plus only)
keyval_zone zone=dns_config:64k state=/etc/nginx/zones/dns_config.zone;
keyval $dns_qname $scrub_action zone=dns_config;
keyval "blocked_domains" $blocked_domains zone=dns_config;
keyval "blackhole_domains" $blackhole_domains zone=dns_config;
그런 다음 NGINX Plus API를 사용하여 명시적으로 스크러빙하려는 값 blocked 또는 FQDN Key에 할당하거나 또는 Key 와 연결된 CSV 형식 목록을 수정할 수 있습니다. 언제든지 정확한 FQDN을 수정 또는 제거하거나 두 목록의 도메인 집합을 수정할 수 있으며 DNS 필터는 변경 사항으로 즉시 업데이트됩니다.
9. 요청을 처리할 Upstream 서버 선택
다음 구성은 아래의 DNS 쿼리를 수신 대기하는 서버 구성에 정의된 블록의 지시문 $dns_response 으로 채워지는 변수를 로드합니다. 호출된 JavaScript 코드가 요청을 스크러빙해야 한다고 판단하면 변수를 또는 적절하게 설정합니다.
지시문을 사용하여 map 변수 값을 변수 $dns_response에 할당함으로써 아래 upstream 서버 정의 에서 요청을 처리하는 $upstream_pool uptstream 그룹을 제어합니다. 비악성 도메인에 대한 쿼리는 기본 Google DNS 서버로 전달되는 반면, 차단되거나 blackhole된 도메인에 대한 쿼리는 해당 카테고리의 upstream 서버에서 처리됩니다.
# The DNS response packet; if we're scrubbing the domain, this gets set
js_set $dns_response dns.get_response;
# Set upstream to the Google DNS server if $dns_response is empty, otherwise
# to 'blocked' or 'blackhole'
map $dns_response $upstream_pool {
"blocked" blocked;
"blackhole" blackhole;
default google;
}
10. Upstream 서버 정의
이 구성은 blocked, blackhole 및 비악성 도메인에 대한 요청을 각각 처리하는 upstream 서버의 blocked, blackhole 및 구글 그룹을 정의 합니다.
# Upstream pool for blocked requests
upstream blocked {
zone blocked 64k;
server 127.0.0.1:9953;
}
# Upstream pool for blackholed requests
upstream blackhole {
zone blackhole 64k;
server 127.0.0.1:9853;
}
# Upstream pool for standard (Google) DNS
upstream google {
zone dns 64k;
server 8.8.8.8:53;
}
11. DNS 쿼리를 수신 대기하는 서버 구성
stream에서 들어오는 DNS 요청을 수신 대기하는 컨텍스트 에서 서버를 정의합니다. 지시문 js_preread은 DNS 패킷을 디코딩하고 패킷의 NAME 필드에서 도메인 이름을 검색하고 Key-value Store에서 도메인을 조회하는 Javascript 코드를 호출합니다. blocked_domains 도메인 이름이 FQDN Key와 일치하거나 또는 목록에 있는 도메인 중 하나의 영역 내에 있으면 blackhole_domains 스크러빙됩니다. 그렇지 않으면 확인을 위해 Google DNS 서버로 전송됩니다.
# DNS (TCP) server
server {
listen 53;
js_preread dns.preread_dns_request;
proxy_pass $upstream_pool;
}
# DNS (UDP) server
server {
listen 53 udp;
js_preread dns.preread_dns_request;
proxy_responses 1;
proxy_pass $upstream_pool;
}
server 대부분 blackhole 또는 blocked 응답으로 쿼리에 응답하는 최종 블록을 추가하기만 하면 됩니다.
# Server for responding to blocked/blackholed responses
server {
listen 127.0.0.1:9953;
listen 127.0.0.1:9853;
listen 127.0.0.1:9953 udp;
listen 127.0.0.1:9853 udp;
js_preread dns.preread_dns_request;
return $dns_response;
}
이 블록의 서버는 바로 위에 있는 실제 DNS 서버와 거의 동일하지만 주요 차이점은 이러한 서버가 DNS 서버의 upstream 그룹에 요청을 전달하는 대신 클라이언트에 응답 패킷을 보낸다는 것입니다. JavaScript 코드는 포트 9953 또는 9853에서 실행될 때를 감지하고 패킷을 차단해야 함을 나타내는 플래그를 설정하는 대신 $dns_response 실제 응답 패킷으로 채웁니다.
물론 이 필터링을 DoT 및 DoH 서비스에도 적용할 수 있었지만 작업을 단순하게 유지하기 위해 표준 DNS를 사용했습니다. DNS 필터링과 DoH Gateway 의 병합은 독자의 연습 문제로 남겨둡니다.
12. 필터 테스트
우리는 이전에 NGINX Plus API를 사용하여 2개의 FQDN에 대한 항목과 일부 도메인을 2개의 도메인 목록에 추가했습니다.
$ 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"
}
따라서 http://www.foo.com (blackhole된 foo.com 도메인 내의 호스트 ) 의 해상도를 요청하면 AIP 주소가 0.0.0.0인 레코드를 얻습니다.
$ 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
13. 코드에 액세스
DOH 및 DOT용 파일은 내 GitHub 저장소 에서 사용할 수 있습니다.
- njs.d/dns 폴더 의 JavaScript 코드
- 예제 폴더 의 NGINX 구성
NGINX 를 사용해 보시려면 지금 30일 무료 평가판을 시작하거나 당사에 연락하여 사용 사례에 대해 논의하십시오.
NGINX에 대해 최신 소식을 빠르게 전달받고 싶으시면 아래 뉴스레터를 구독하세요.