어두운 데이터센터를 뒤덮는 에메랄드빛 광선의 급류가 빛나는 업스트림 디플렉터에 부딪혀 갈라지고 휘어지며 흩어지고, 몇 가닥의 깨끗한 선만이 그 뒤편 무광 검은색 서버 블레이드에 가닿는 모습
네트워크 가이드

VPS를 DDoS 공격 속에서도 온라인 상태로 유지하기

상당히 다른 세 가지 공격이 DDoS라는 이름 하나를 나눠 쓰고 있으며, 여러분이 읽게 될 조언 대부분은 이 셋을 하나로 취급하기 때문에 실패합니다: 포화된 업링크에 nginx 지시어로 답하거나, 정교한 L7 공격에 더 큰 서버로 맞서는 식입니다. 쓸모 있는 질문은 결코 “이걸 어떻게 막지”가 아니라 “이것은 물리적으로 어느 계층에서 막을 수 있고, 그 계층은 누구의 소관인가”입니다. 이 페이지는 임대한 리눅스 박스를 기준으로 그 답을 다룹니다 — 여러분이 미처 보기도 전에 위쪽 네트워크가 흡수해 주는 것, 머신 내부에서 진짜로 도움이 되는 것, 그리고 대개 가장 효과적인 한 수는 아예 주소로 찾아지지 않는 것인 이유까지입니다.

이 네트워크에서 대역폭 공격에 대한 완화 조치는 언제나 켜져 있으며, 여러분이 따로 활성화할 것은 없습니다: 10 Gbps 미만의 플러드는 조용히 흡수되고, 10~100 Gbps는 트랜짓 제공업체 단에서 스크러빙되며, 그 이상은 전용 스크러빙 경로로 라우팅을 전환해 처리합니다 — 이 기준치는 마케팅용 형용사로 남겨 두지 않고 문서에 명시해 두었습니다. 이것이 호스팅 업체가 넘겨줄 수 있는 것의 정직한 한계입니다. 그리고 실제로 소규모 서버를 쓰러뜨리는 트래픽에 관한 한, 이는 문제의 절반 중에서도 덜 흥미로운 쪽입니다.

5달러짜리 VPS를 확실하게 죽이는 공격은 뉴스에 나오는 340 Gbps짜리 괴물이 아니기 때문입니다. 그것은 여러분이 한 번도 들여다본 적 없는 상태 테이블을 채워 버리는 초당 4만 개의 작은 SYN 패킷이거나, 데이터베이스 쿼리를 실행하는 사이트 안의 URL 하나에 쏟아지는 초당 300건의 요청입니다 — 이런 트래픽은 멀쩡한 인터페이스에, 전혀 꽉 차지 않은 회선을 타고 도착하며, 어떤 업스트림 스크러버도 이것을 여러분의 실제 사용자와 구별해 내지 못합니다. 이런 공격은 여러분 스스로 처리해야 하며, 셋 중 무엇을 보고 있는지 먼저 가려낸다면 대략 스무 줄의 설정으로 처리할 수 있습니다.

이어지는 내용은 Debian 13 또는 Ubuntu 24.04, nftables, nginx를 사용한다고 가정하며, 여러분이 이미 첫 한 시간 강화 작업을 마쳤다고 가정합니다 — 서비스하지도 않는 포트에 여전히 응답하는 서버는 방어할 준비가 되어 있지 않은 서버입니다.

세 가지 공격, 하나의 이름

“DDoS”는 의도를 가리키는 말이지 메커니즘을 가리키는 말이 아니며, 그 메커니즘들은 서로 공통점이 거의 없습니다. 이를 제대로 구분하는 일은 현학이 아니라 작업의 전부입니다. 각각은 정확히 하나의 계층에서만 막을 수 있고, 나머지 계층에서는 보이지도 않기 때문입니다.

대역폭 공격은 여러분의 대역폭을 노립니다. UDP 증폭 — DNS, NTP, memcached, 그리고 최근에는 작은 질문에 큰 답을 내놓는 거의 모든 것 — 을 이용하면 공격자는 자신의 1 Gbps짜리 용량을 여러분을 향한 50 Gbps로 둔갑시킬 수 있습니다. 표적은 서버가 아니라 회선입니다. 그동안 여러분의 CPU는 심심할 뿐입니다.

프로토콜 및 상태 공격은 커널 안의 유한한 테이블을 노립니다. SYN 플러드는 accept 큐를 고갈시키려 하고, 일반적인 소형 패킷 플러드는 연결 추적(connection tracking)을 고갈시키려 합니다. 둘 다 초당 비트 수가 아니라 초당 패킷 수로 측정되며, 회선이 97% 놀고 있는 상태에서도 머신을 죽일 수 있습니다. 소규모 서버를 쓰러뜨리는 부류가 바로 이것이고, 대부분의 가이드가 건너뛰는 부류도 바로 이것입니다.

애플리케이션 계층 공격은 여러분의 CPU나 데이터베이스를 노리며, 진짜 요청과 구별되지 않는 요청을 사용합니다. 왜냐하면 그것이 실제로 진짜 요청이기 때문입니다. 검색 엔드포인트에 쏟아지는 초당 백 건의 요청은 네트워크에는 아무것도 아니지만 PHP 애플리케이션에는 치명적입니다. 어떤 업스트림 스크러버도 이것을 대신 걸러 줄 수 없습니다: 바깥에서 보면 이는 정확히 성공한 요청처럼 보이기 때문입니다.

이 셋 모두로 위장하고 나타나지만 사실은 공격이 전혀 아닌 네 번째 상황도 있습니다: 어딘가에서 화제가 된 링크, 여러분 자신의 오작동하는 클라이언트, 혹은 예의 없는 크롤러입니다. 이를 먼저 배제하는 데는 비용이 들지 않으며, 놀랍도록 자주 이것이 정답입니다.

박스 안에서는 고칠 수 없는 부분

60 Gbps가 여러분의 주소를 향해 날아오고 여러분의 포트는 1 Gbps짜리라면, 그 패킷들의 운명은 여러분이 관리하는 그 무엇보다도 여러 홉 앞선, 상류의 라우터에서 이미 결정됩니다. 여러분의 방화벽은 그 패킷들을 결코 보지 못합니다. 볼 수가 없습니다: 그 패킷들은 여러분 소유가 아닌 회선을 보호하기 위해 이미 버려졌기 때문입니다. 이것이 대역폭 공격에 관해 가장 중요한 구조적 사실이며, “방화벽을 강화해서 DDoS에 대비하라”는 조언이 대체로 헛소리인 이유입니다.

그러니 의미 있는 질문은 오직 여러분의 호스팅 업체가 자동으로 무엇을 해 주는지, 그 기준치가 어디에 있는지뿐입니다. 저희 기준치는 약속이 아니라 공개된 수치입니다: 10 Gbps 미만은 눈에 띄는 영향 없이 흡수되고, 10~100 Gbps는 트랜짓 제공업체 단에서 스크러빙되며 지연이 약간 늘어난 것을 느낄 수도 있고, 100 Gbps를 넘으면 해당 프리픽스를 전용 스크러빙 경로로 announce합니다 — 지연은 더 눈에 띄게 늘지만 서비스는 계속 응답합니다. 주소를 널 라우팅하는 것은 더 넓은 PoP 전체를 위협하는 지속적인 공격에 대해서만 고려하는 최후의 수단이며, 그런 일이 벌어지면 몇 분 안에 알려 드립니다. 구매할 것도, 켜야 할 것도 없습니다.

스크러빙에 관해 업체들이 좀처럼 먼저 말해 주지 않는 사실이 두 가지 있습니다. 첫째, 여러분은 자기 자신의 방어만으로 보호받는 것이 아니라 이웃에 의해서도 보호받는다는 점입니다: 여러분과 같은 업스트림 /20을 공유하는 고객을 향한 공격은 어느 프리픽스에도 닿기 전에 흡수됩니다 — 파리의 이웃 대역을 노린 약 340 Gbps짜리 플러드가 저희 쪽에는 측정 가능한 영향을 전혀 남기지 않고 지나간 적이 있는데, 이것이 바로 아무도 눈치채지 못하는 결과입니다. 둘째, 스크러버는 휴리스틱이고 휴리스틱은 가끔 틀린다는 점입니다: 같은 네트워크에서, 스크러버가 이웃을 향한 공격을 걸러내는 동안 정상적인 연결에 TCP 리셋을 9분 동안 쏘아 보낸 적이 있습니다. 두 사건 모두 공개 장애 기록에 남아 있습니다. 다른 누군가가 공격받는 동안 여러분의 세션이 타임아웃이 아니라 능동적인 리셋처럼 죽는다면, 그것은 실재하는 장애 유형이며 한 시간 동안 로컬에서 디버깅하기보다는 신고할 가치가 있습니다.

무엇이든 바꾸기 전에, 90초의 측정

잘못된 DDoS 대응은 하나같이, 무슨 일이 벌어지고 있는지 아무도 파악하기 전에 손을 댄 변경에서 시작됩니다. 먼저 숫자 네 가지를 확보하십시오. 화면 하나에 다 들어오며, 이것만으로 계층이 무엇인지 알 수 있습니다.

# 1. bits vs packets  — which axis is saturated?
sar -n DEV 1 5            # or: ifstat -i eth0 1

# 2. socket states     — SYN-RECV piling up means a SYN flood
ss -s
ss -tan state syn-recv | wc -l

# 3. kernel drop counters (these are the ones that matter)
nstat -az | grep -E 'ListenDrops|ListenOverflows|SyncookiesSent|TCPReqQFullDrop'
dmesg -T | tail -20       # look for: nf_conntrack: table full, dropping packet

# 4. connection tracking headroom
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

이 넷을 함께 읽으십시오. 초당 비트 수는 높고, 초당 패킷 수는 낮고, CPU도 낮다면 대역폭 공격이며, 여러분이 할 일은 이를 확인하고 더 이상 손대지 않는 것입니다. 초당 패킷 수는 높은데 초당 비트 수는 낮다면 — 작은 패킷이 잔뜩 몰려온다는 뜻이므로 — 상태 공격입니다. 즉시 SyncookiesSent와 conntrack 카운트를 살펴보십시오. 양쪽 축 모두 트래픽은 평범한데 CPU만 고정되어 있다면 L7이며, 답은 방화벽이 아니라 웹 서버와 데이터베이스 안에 있습니다.

직관에 반하는 해석 하나는 몸에 새겨 둘 가치가 있습니다: 진짜 대역폭 공격이 벌어지는 동안 여러분의 인터페이스는 오히려 거의 잠잠해 보일 수 있습니다. 여러분이 보고 있는 것은 살아 남은 트래픽입니다 — 이미 꽉 찬 회선을 뚫고 들어온 것, 또는 업스트림 스크러버를 통과한 것입니다. 1 Gbps 포트에서 900 Mbps를 찍고 있는 인터페이스와, 완전히 접속 불가라고 보고하는 사용자들이 동시에 존재한다면, 이는 공격이 아니라는 증거가 아닙니다. 오히려 공격의 전형적인 모습입니다.

그다음에는 그 트래픽이 그냥 진짜 사용자는 아닌지 확인하십시오. 액세스 로그를 tail -f로 10초만 들여다보십시오: 리퍼러도 없이 수천 개의 서로 다른 주소가 같은 경로 하나를 반복해서 두드리고 있다면 공격이고, 정상적인 브라우저들이 다양한 경로에 고르게 퍼져 있다면 그것은 그냥 방문자들입니다 — 이 경우에 속도 제한을 걸면 공격자가 할 일을 여러분이 대신 해 주는 셈이 됩니다.

표적 줄이기는 공격 전에, 공격 중이 아니라

열려 있는 포트 하나하나가 공격자가 채울 수 있는 큐이며, 게스트가 들여다봐야 하는 패킷 하나하나는 설령 여러분이 그것을 버리더라도 CPU와 conntrack 항목 하나의 비용을 치르게 만듭니다. 가장 저렴한 패킷은 여러분의 머신이 아예 받지 않는 패킷입니다. 그래서 게스트 내부에서 걸러내는 것보다 게스트보다 위쪽에서 걸러내는 것이 더 값어치가 있습니다.

여기에는 두 개의 계층이 있으며, 이 둘은 서로 다른 것입니다. 엣지 필터는 선택 사항으로, 서버별로 설정하며, 상태 저장형(stateful)이고 기본 차단이며, 하이퍼바이저 위에서 동작합니다 — 여기서 거부한 트래픽은 여러분의 가상 NIC에는 아예 도달하지 않으므로 CPU도, 메모리도, 상태 테이블 항목도 소모하지 않습니다. 게스트 방화벽 — 여러분의 nftables 규칙셋 — 은 전적으로 여러분의 몫이며 저희는 여기에 절대 손대지 않습니다. 공격을 맞닥뜨려도 살아남는 패턴은, 거칠고 안정적인 L4 원칙(“이 머신은 이 주소들로부터 80, 443, SSH만 서비스하며 그 밖의 것은 존재하지 않는다”)은 엣지에서 표현하고, 애플리케이션에 따라 바뀌는 세밀한 작업은 게스트 규칙셋에 맡겨 두는 것입니다. 둘 다 방화벽 문서에 설명되어 있습니다.

그런 다음 남은 것을 좁혀 나가십시오. 실제로 관리에 쓰는 주소로만 SSH를 제한하는 것은 여기서는 그저 있으면 좋은 부가 조치가 아니라, 여러분의 머신에서 연결 고갈 공격이라는 범주 전체를 통째로 제거해 버리는 조치입니다. 127.0.0.1이나 WireGuard 주소에 바인딩된 데이터베이스는 인터넷에서 아예 플러드 공격을 당할 수가 없습니다. 순수하게 내부용인 제어 플레인을 서비스하고 있다면, 비밀번호를 건 공인 포트 대신 WireGuard 인터페이스 뒤에 두십시오.

SYN 플러드와 accept 큐

SYN 플러드는 핸드셰이크를 악용합니다: 공격자는 연결 요청을 끊임없이 보내면서 결코 완료하지 않고, 각 요청은 타임아웃될 때까지 커널의 SYN 큐 안에서 자리 하나를 차지합니다. 큐가 가득 차면 정상적인 핸드셰이크도 함께 버려집니다 — 서비스는 떠 있고 포트도 열려 있지만 아무도 접속할 수 없습니다.

리눅스에는 90년대부터 깔끔한 해법이 있었고, 이는 기본적으로 켜져 있습니다. SYN 쿠키는 커널이 반쯤 열린 연결을 위해 메모리를 할당하는 일 자체를 완전히 그만두게 해 줍니다: 되돌려 보내는 시퀀스 번호 안에 연결 상태를 인코딩해 두었다가, 클라이언트가 핸드셰이크를 완료하면 이를 다시 복원합니다. 이것이 켜져 있다고 짐작하지 말고 직접 확인하고, 순간적인 폭주를 버텨 낼 수 있도록 큐에 충분한 여유를 주십시오:

# /etc/sysctl.d/99-flood.conf
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 8192
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_abort_on_overflow = 0

sysctl --system

tcp_synack_retries = 2는 보기보다 훨씬 중요합니다: 기본값인 5는 위조된 핸드셰이크 하나가 커널을 약 3분 동안 붙잡아 둔다는 뜻이고, 2로 낮추면 이를 약 7초로 줄여 줍니다. somaxconn을 올리는 것은 절반의 작업일 뿐입니다 — 큐의 깊이는 이 값과 애플리케이션이 요청한 값 중 더 작은 쪽으로 정해지므로, nginx 쪽에서도 listen 443 ssl backlog=8192;를 설정하고 reload해야 비로소 커널 설정이 의미를 갖습니다. 이는 적용된 것처럼 보이지만 실제로는 아무 효과도 없는 sysctl의 전형적인 사례입니다.

플러드가 애플리케이션에 아예 닿기 전에 막고 싶다면, nftables가 커널을 대신해 핸드셰이크를 완료해 주고 실제로 진짜인 것으로 판명된 연결만 넘겨주게 할 수 있습니다:

table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    iif lo accept
    tcp dport { 80, 443 } ct state new synproxy mss 1460 wscale 7 timestamp sack-perm
    tcp dport { 80, 443 } accept
    ip protocol icmp icmp type echo-request limit rate 5/second accept
  }
}

synproxy에 의존하기 전에, 콘솔로 여전히 접속할 수 있는 머신에서 먼저 테스트해 보십시오. 설정을 잘못하면 이는 자기 서버에서 스스로를 걸어 잠그는 훌륭한 방법이 되어 버립니다. 대부분의 사람에게는 위의 sysctl 블록만으로 충분하며, 정직한 우선순위는 다음과 같습니다: syncookies가 먼저, backlog가 다음, synproxy는 앞의 둘로 부족하다는 것을 직접 측정으로 확인한 뒤에만.

가득 차기 전에는 아무도 보지 않는 테이블

이것이 바로 경험 많은 사람들조차 걸려드는 함정입니다. 상태 저장형 방화벽은 자신이 본 모든 흐름을 기억해야 하며, 그 기억 장치가 바로 nf_conntrack입니다 — 부팅 시점에 크기가 고정되는 해시 테이블입니다. 이것이 가득 차면 커널은 새로운 연결을 떨어뜨립니다 — 공격자든 고객이든 가리지 않고 전부 — 그리고 아무도 보고 있지 않은 dmesg에 딱 한 줄을 남깁니다:

nf_conntrack: table full, dropping packet

이것이 이토록 효과적인 이유는 순전히 산수입니다. 소형 VPS의 기본 테이블은 대략 수만 개 초반대의 항목을 담을 수 있는데, 새로운 발신 주소에서 오는 패킷 하나하나가 항목을 하나씩 만들어 냅니다 — UDP 패킷도, 여러분이 버리는 패킷도, 플러드 자체도 예외가 아닙니다. 위조된 발신지에서 오는 초당 2만 개의 패킷이면 단 몇 메가비트짜리 회선에서도 2초가 채 안 되어 테이블을 가득 채웁니다. 여러분의 대역폭 그래프에는 아무것도 나타나지 않을 것입니다.

# /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 262144
net.netfilter.nf_conntrack_buckets = 65536
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 20
net.netfilter.nf_conntrack_udp_timeout = 20

크기보다 타임아웃 쪽이 더 큰 효과를 냅니다. established 상태인 TCP 흐름의 기본 수명은 5일이며, 이는 일주일째 켜져 있는 서버가 화요일에 이미 끝난 연결의 상태를 여전히 붙들고 있다는 뜻입니다. 유휴 SSH 세션이 아닌 이상 한 시간이면 충분하며, net.ipv4.tcp_keepalive_time = 600은 그런 세션을 우연이 아니라 제대로 살려 둡니다. 크기를 정할 때는 항목당 커널 메모리를 약 300바이트로 잡으십시오: 262,144개면 약 80 MB로, 4 GB 머신에서는 무난하지만, 어느 포럼에서 그렇게 하라고 했다는 이유만으로 천만 개로 설정한다면 무난하지 않습니다.

진짜로 상태가 없고(stateless) 물량이 많은 무언가를 운영하고 있다면 — 권한 있는(authoritative) DNS 서버, 공개 게임 서버, Tor 릴레이 같은 것 — 더 나은 답은 아예 추적을 하지 않는 것입니다. 의미 있는 연결이라는 게 없는 서비스에 연결 추적을 거는 것은 순전한 비용일 뿐입니다:

table ip raw {
  chain prerouting {
    type filter hook prerouting priority raw; policy accept;
    udp dport 51820 notrack
    tcp dport 9001 notrack
  }
}

이렇게 하면 해당 트래픽은 여러분의 ct state established 규칙을 그냥 지나쳐 버리므로, filter 체인에 명시적인 accept 규칙을 따로 두어야 한다는 점을 기억하십시오. 이것이 트레이드오프입니다: 그 포트에 대해서는 상태 저장을 포기하는 대신, 절대 가득 찰 수 없는 테이블을 얻습니다.

L7: 고객과 똑같이 생긴 플러드

소규모 서버를 상대로 가장 효율적인 공격은 사실 플러드가 전혀 아닙니다. 잘 골라낸 초당 수백 건의 HTTP 요청이며, 하나하나가 완벽하게 유효하고, 하나하나가 인덱스 없는 쿼리를 실행하거나 캐시되지 않은 페이지를 렌더링하는 단 하나의 엔드포인트를 두드립니다. 경제성은 잔인할 정도입니다: 그 요청 하나는 공격자에게는 수백 바이트의 비용이지만, 여러분에게는 CPU 200밀리초와 데이터베이스 연결 하나의 비용입니다. 여러분이 감당할 수 있는 어떤 물량에서도 이 경주는 지게 되어 있습니다.

본능적으로는 공격자를 차단하고 싶어집니다. 초당 요청 두 건씩만 보내는, 수천 개의 가정용 주소에 흩어져 있는 봇넷을 상대로는 IP 차단은 그저 보여주기일 뿐입니다 — 실제 사용자를 허용할 만큼 관대한 주소별 속도 제한이라면 애초에 발동조차 하지 않을 것이고, 여러분은 사이트가 다운된 채로 규칙을 추가하는 데 장애 시간을 다 써 버릴 것입니다. 이기는 수는 개수를 바꾸는 것이 아니라 비용을 바꾸는 것입니다.

먼저 캐시하고, 미스도 캐시하십시오. 전체 페이지 캐시는 비용이 큰 요청을 메모리 읽기 한 번으로 바꿔 주며, 공격 상황에서 가장 중요한 지시어는 동시에 발생한 천 건의 캐시 미스가 동시에 천 건의 데이터베이스 쿼리로 번지는 것을 막아 주는 지시어입니다:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app:64m max_size=2g inactive=60m;

server {
  location / {
    proxy_cache app;
    proxy_cache_valid 200 301 302 10m;
    proxy_cache_lock on;              # one origin request per key, not a thousand
    proxy_cache_lock_timeout 5s;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    proxy_cache_background_update on; # serve stale, refresh behind it
    add_header X-Cache $upstream_cache_status always;
    proxy_pass http://127.0.0.1:8080;
  }
}

proxy_cache_lockuse_stale는 트래픽 급증을 버텨 낼 수 있느냐 없느냐를 가르는 두 줄입니다. 이것이 없으면, 인기 있는 키 하나가 만료되는 순간 그 시점에 진행 중이던 모든 요청이 오리진 요청으로 둔갑합니다 — 감당할 만한 공격을 장애로 바꿔 버리는 스탬피드입니다. 이것이 있으면, 백엔드는 구간마다 키 하나당 요청 하나만 처리하고 나머지 모두는 약간 오래된 페이지를 받게 되는데, 그 대안이 아예 페이지가 없는 것뿐인 모든 상황에서 이는 올바른 트레이드오프입니다.

그다음에는 비용이 큰 경로를 구체적으로 속도 제한하십시오, 넉넉하게, 그리고 실패했을 때 눈에 보이는 방식으로:

limit_req_zone $binary_remote_addr zone=general:16m rate=20r/s;
limit_req_zone $binary_remote_addr zone=costly:16m  rate=2r/s;
limit_conn_zone $binary_remote_addr zone=conn:16m;
limit_req_status 429;
limit_conn_status 429;

server {
  limit_req  zone=general burst=40 nodelay;
  limit_conn conn 24;

  location /search { limit_req zone=costly burst=5 nodelay; proxy_pass http://127.0.0.1:8080; }
  location /login  { limit_req zone=costly burst=3 nodelay; proxy_pass http://127.0.0.1:8080; }
}

nodelay 없이 쓰는 burst는 초과 요청을 거부하는 대신 큐에 쌓아 두는데, 공격 상황에서 이는 nginx가 여러분을 대신해 수천 개의 연결을 공손하게 열어 둔 채 붙들고 있다는 뜻입니다 — 요청 플러드를 연결 플러드로 바꿔 버린 셈입니다. nodelay를 쓰고, 즉시 429를 반환하고, 나머지는 클라이언트가 알아서 하게 두십시오. 그리고 어떤 프록시 뒤에 있든, set_real_ip_fromreal_ip_header를 제대로 설정해 두지 않는 한 $binary_remote_addr는 프록시 자신의 주소라는 점을 기억하십시오. 여러분 자신의 프런트엔드 주소를 키로 삼은 속도 제한은 처음 발동하는 순간 모든 사용자를 한꺼번에 걸어 잠글 것입니다.

Slowloris 방식의 공격 — 수많은 연결을 열어 놓고 각각 요청을 한 바이트씩 찔끔찔끔 보내는 방식 — 은 nginx의 이벤트 모델을 거의 괴롭히지 못하지만, 그래도 타임아웃은 조여 둘 가치가 있습니다: client_header_timeout 8s; client_body_timeout 8s; send_timeout 10s; reset_timedout_connection on;. limit_conn과 합쳐지면, 이것이 방어의 전부입니다.

fail2ban이 DDoS 도구가 아닌 이유

fail2ban은 정말로 유용한 도구이지만, 다른 문제를 겨냥한 도구입니다. 로그 파일을 읽고, M분 안에 N번 실패했는지 판단한 다음, 방화벽 규칙을 삽입합니다. 이 과정 하나하나가 플러드에는 맞지 않는 형태입니다.

일단 너무 느립니다: 40초짜리 폭주는 차단 창이 닫히기도 전에 이미 끝나 있습니다. 로그를 읽는 방식이므로, 공격은 이미 모든 요청의 정가를 여러분에게서 다 받아 간 뒤입니다 — 파싱은 피해가 발생한 뒤에야 일어납니다. 개별 주소를 차단하는 방식이므로, 만 개의 발신지를 상대로는 아무것도 못 하거나 만 개의 선형 규칙을 삽입하게 되는데, 이 시점에는 방화벽 자체가 병목이 되어 여러분이 공격자를 대신해 공격을 완성해 주는 꼴이 됩니다. 그리고 이는 여러분의 로그에 의존하므로, 디스크를 로그 줄로 가득 채울 만큼 큰 플러드는 여러분이 미처 생각지 못한 경로로 서버를 쓰러뜨릴 수 있습니다.

동적 차단을 원한다면, 조회가 해시로 이루어지고 만료가 자동으로 처리되는 데이터 플레인에서 하십시오. nftables의 동적 집합(dynamic set)은 크기와 무관하게 O(1)이며, 스스로 알아서 잊어버립니다:

table inet filter {
  set flooders {
    type ipv4_addr
    flags dynamic, timeout
    timeout 10m
    size 65535
  }

  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    iif lo accept
    ip saddr @flooders drop
    tcp dport { 80, 443 } ct state new add @flooders { ip saddr limit rate over 50/second burst 100 packets } drop
    tcp dport { 80, 443 } accept
  }
}

이 규칙은 신중하게 읽으십시오. 부정문 때문에 헷갈리기 쉽습니다: 주소가 집합에 추가되고 버려지는 것은 오직 속도가 한도를 넘어설 때뿐입니다. fail2ban은 자신이 잘하는 일에만 쓰십시오 — SSH와 애플리케이션 로그인을 상대로 한 느린 자격 증명 추측처럼, 특정 주소를 사람 시간 단위로 차단하는 것이 정확히 맞아떨어지는 일 말입니다.

무기가 되지 마십시오: 여러분이 호스팅하고 있는 증폭

대형 대역폭 공격은 하나같이, 자신이 거기에 가담하고 있다는 사실조차 몰랐던 운영자들의 서버가 동력이 되어 굴러갑니다. 그 메커니즘은 작게 위조된 요청에 큰 응답으로 답하는 UDP 서비스입니다: 오픈 DNS 리졸버는 요청받은 것의 50배를 돌려주고, 잘못 설정된 NTP와 memcached는 그보다 더 심하며, 요청자가 누구인지 확인하기도 전에 응답부터 해 버리는 프로토콜이라면 무엇이든 후보가 됩니다.

그 결과는 피해자에게 닿기도 전에 먼저 여러분에게 떨어집니다. 여러분의 업링크는 여러분 자신이 내보내는 응답으로 가득 차고, 호스팅 업체는 여러분의 포트에서 지속적인 악성 트래픽이 빠져나가는 것을 보게 되며, 그 트래픽을 받는 트랜짓 공급자들은 그 발신 대역을 널 라우팅하기 시작합니다 — 이용 약관(acceptable-use policy)이 그 밖의 거의 모든 것은 허용하면서도 공격 발신과 증폭에 대해서만은 확실하게 선을 긋는 이유가 바로 이것입니다. 본의 아니게 리플렉터가 되는 것은, 여러분이 운영하려던 것과는 아무 상관도 없는 주소를 가장 빨리 잃는 방법입니다.

# bind a recursive resolver to localhost, never 0.0.0.0
# unbound: interface: 127.0.0.1  +  access-control: 0.0.0.0/0 refuse

# what is actually listening on a public address?
ss -ulpn
ss -tlpn

# from another machine, confirm you do not answer strangers
dig @your.server.ip example.com +short          # must time out
ntpq -c rv your.server.ip                        # must fail

게임 서버, 권한 있는 네임서버, WireGuard 엔드포인트처럼 공개 UDP 서비스를 의도적으로 운영하고 있다면, 규칙은 다른 모든 곳과 똑같습니다: 속도를 제한할 것은 요청이 아니라 언제나 응답입니다. bindknot 모두 응답 속도 제한(response rate limiting)을 지원합니다. 이를 사용하십시오. 이것이 바로 여러분의 사용자를 서비스하는 것과 다른 누군가의 공격을 실어 나르는 것의 차이입니다.

가장 효과적인 방어는 찾아지지 않는 것

지금까지의 모든 내용은 이미 여러분의 주소를 찾아낸 공격에 대한 사후 대응입니다. 해 둘 가치는 있지만, 차선책입니다. 아무도 이름 붙일 수 없는 오리진은 L3나 L4 수준에서는 애초에 공격할 방법이 없으며, 트래픽을 서비스하는 주소와 세상이 알고 있는 주소를 분리하는 것이야말로 이 페이지에서 가장 레버리지가 큰 조치입니다.

오리진 주소는 정교한 공격을 통해 새어 나가는 경우가 거의 없습니다. 이력과 부주의를 통해 새어 나가며, 그 목록은 짧고 잘 알려져 있습니다. 오래된 DNS 레코드가 흔한 범인입니다: 패시브 DNS 아카이브는 여러분이 프런트를 세우기 전에 갖고 있던 A 레코드를 영원히 기억합니다. 인증서 투명성(certificate transparency) 로그는 공개적이고 영구적이므로, 오리진을 곧바로 가리키던 호스트네임으로 발급된 인증서는 여러분이 예전에 어디에 살았는지를 알려 주는 서명되고 타임스탬프 찍힌 기록으로 남습니다. 발신 메일은 모든 메시지의 헤더 안에 발신 서버의 주소를 새겨 넣습니다. 그리고 조용한 범인 하나: 맨 IP로도 응답해 버리는 웹 서버입니다. 이 때문에 여러분 사이트의 HTML을 노려 주소 공간 전체를 스캔하는 사람이라면 누구든 오후 한나절이면 콘텐츠만으로 여러분을 찾아낼 수 있습니다.

마지막 것은 두 줄이면 고칠 수 있는데도 거의 아무도 손대지 않습니다. 기본 서버가 여러분이 서비스하는 호스트네임 없이 도착한 모든 요청을 거부하도록 만드십시오:

server {
  listen 80 default_server;
  listen 443 ssl default_server;
  ssl_reject_handshake on;      # nginx 1.19.4+: no certificate, no fingerprint
  return 444;                   # close without a response
}

그런 다음 프런트를 고르십시오. 상업용 CDN이 가장 뻔한 답이며, 여기서 진짜 문제가 되는 진짜 비용을 함께 데려옵니다: 여러분은 TLS를 종단하고, 평문을 들여다보고, 오리진을 알고 있으며, 여러분이 고르지 않은 관할권에서 법적 절차를 송달받을 수 있는 회사 하나를 새로 추가하는 셈입니다 — 이는 이름을 남기지 않고 암호화폐로 서버 비용을 치른 이유의 상당 부분을 무너뜨립니다. 그럼에도 이 방법을 쓰겠다면, 오리진이 단 한 번도 공개된 적이 없어야 한다는 것과, 오래된 기록 하나만 발견되어도 이 구조 전체가 무너진다는 것을 이해해 두십시오.

여러분이 애초에 지불한 그 속성을 그대로 지키는 대안은 스스로 프런트가 되는 것입니다. nginx를 리버스 프록시로 돌리는, 두 번째 리전의 5달러짜리 인스턴스 하나를 두고, 오리진의 엣지 필터가 오직 그 주소 하나로부터의 트래픽만 받아들이게 하면, 5분이면 재발급할 수 있는 희생용 주소 하나와 여러분에게만 응답하는 제어 플레인을 얻게 됩니다. 이는 제3자만 빠진, 똑같은 아키텍처입니다. 그리고 이용자층이 받아들일 수 있는 경우라면 Tor 어니언 서비스는 방정식에서 IP를 통째로 없애 버립니다 — 플러드를 걸 주소 자체가 없습니다. 다만 어니언 서비스에도 introduction point라는 나름의 공격 표면은 있으며, 바로 이 때문에 최신 Tor는 정확히 이 문제를 위한 작업 증명(proof-of-work) 방어책을 탑재하고 있습니다.

국가, ASN, 그리고 트래픽의 모양을 차단하기

조만간 누군가는 국가 단위 차단을 제안하게 됩니다. 이는 무딘 방법이고, 가끔은 맞기도 하지만, 대개는 뭔가 하고 있다는 기분을 내는 방법에 지나지 않습니다.

지역 게임 서버, 내부 도구, 관리 패널처럼 서비스 대상이 정말로 제한되어 있고, 여행 중인 자신의 사용자나 이상하게 라우팅된 사용자를 포함한 오탐을 감수할 각오가 되어 있다면 이는 옹호할 만합니다. 하지만 여러분의 나라를 포함해 전 세계 가정용 회선에 흩어져 있는 현대의 봇넷을 상대로는 거의 쓸모가 없으며, 공개 서비스라면 오히려 적극적으로 해롭습니다: 실제 사용자를 조용히 잃게 될 뿐이고, 떠나 버린 사람들은 두 번 다시 볼 수 없습니다.

ASN 단위 차단이 더 날카롭습니다. 소수의 호스팅 업체 — 시간 단위로 대여되는 값싼 VPS 대역 — 에서 발원하는 공격 트래픽은, 일반 사용자가 데이터센터에서 웹서핑을 하지 않는다는 사실 덕분에, 국가 차단보다 훨씬 적은 피해로 프리픽스 단위로 떨어뜨릴 수 있습니다. 데이터센터 대역은 편리하게도 스크래핑과 크리덴셜 스터핑이 흘러나오는 곳이기도 합니다.

하지만 이 발상의 진짜 오래가는 버전은 지리와는 전혀 상관이 없습니다: 발원지가 아니라 트래픽의 모양으로 걸러내는 것입니다. 공격은 대개 구조적으로 공통된 무언가를 지닙니다 — 같은 User-Agent, 같은 헤더 누락, 같은 쿼리 파라미터를 가진 같은 URL, 같은 TLS 핑거프린트, 진짜 브라우저라면 항상 보내는 두 번째 요청의 완전한 부재 같은 것들입니다. 이 공통된 속성을 찾아내면, 비용이 전혀 들지 않으면서 공격자가 주소를 바꿔도 살아남는 규칙 하나를 쓸 수 있습니다 — 그리고 공격자는 여러분이 그 주소들을 나열하는 속도보다 더 빠르게 주소를 바꿀 것입니다.

# in an attack: what do these requests have in common?
awk '{print $1}'  /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20   # addresses
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20   # user agents
awk '{print $7}'  /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20   # paths

만약 하나의 User-Agent가 요청의 90%를 차지한다면, 해결책까지는 90초밖에 남지 않은 것입니다. 만약 상위 스무 개 주소가 각각 트래픽의 2%씩만 차지한다면, 주소를 찾는 일은 그만두십시오 — 그것은 분산 공격이며, 답은 캐싱과 속도 제한입니다.

주소가 노출되어 버렸을 때

어떤 때는 공격이 무작위 주소가 아니라 여러분 개인을 노리고, 표적화되고, 끈질기게 이어집니다. 이때 올바른 대응은 그 주소를 계속 방어하는 것을 그만두고 아예 버리는 것입니다. 이는 패배가 아닙니다. 소규모 서비스에서는 대개 가장 저렴하게 끝낼 수 있는 결말이며, 미리 연습해 두지 않았을 때만 고통스러울 뿐입니다.

이를 빠르게 만드는 요소는 모두 사전에 결정해 두는 것들입니다. DNS TTL은 평소에도 300초로 유지해 두십시오 — 비용은 무시할 만한 수준이고, 이득은 하루가 아니라 5분 만에 옮길 수 있다는 것입니다. 배포는 재현 가능하게 유지하십시오. 다시 세울 수 있는 서버라야 옮길 수 있는 서버이기 때문입니다. 재구축한다는 것이 3월에 무엇을 했는지 기억해 내는 일을 뜻한다면, 여러분에게 있는 것은 계획이 아니라 인질입니다. 다른 리전에 복원 가능한 백업을 두고, 실제 훈련을 통해 복원에 걸리는 시간을 파악해 두십시오.

그러면 실제로 옮기는 과정은 짧습니다: 다른 리전에 배포하고, 복원하고, DNS를 건드리기 전에 호스트네임 오버라이드로 새 주소에서 먼저 검증하고, 레코드를 전환한 다음, 가장 늦게 반영되는 리졸버들이 따라잡을 때까지 이전 서버를 충분히 오래 켜 두십시오. 마이그레이션 가이드가 이 순서를 제대로 다루고 있으며, 성능 때문에 옮기든 누군가 화가 나서 옮기든 순서는 동일합니다.

요금 체계 덕분에 리허설은 사실상 공짜나 다름없습니다: 요금은 잔액에서 일 단위로 비례 청구되므로, 오후 한나절 사이에 배포하고 테스트하고 없애 버리는 두 번째 머신의 비용은 몇 센트에 불과하며, 계약도, 카드도, 취소해야 할 갱신도 없습니다. 여러분의 첫 주소 이전이 공격을 받는 도중에 벌어져야 할 이유는 없습니다.

이것들로도 사지 못하는 것

정직한 마무리 두 가지입니다. 이 페이지의 나머지 부분은 구성상 낙관적이기 때문입니다.

첫째, 모든 것을 다 흡수하는 호스팅 업체는 없으며, 그렇다고 주장하는 업체는 그저 뭔가를 팔고 있는 것입니다. 용량은 유한하고, 스크러빙 용량은 더욱 유한하며, 충분히 큰 규모에서는 수천 명의 고객을 보호하는 네트워크에게 경제적으로 합리적인 행동은 결국 주소 하나의 announce를 중단하는 것입니다. 저희는 이를 더 넓은 PoP 전체를 위협하는 지속적인 공격에 대한 최후의 수단으로만 다루며, 여러분이 스스로 알아내게 두지 않고 먼저 알려 드립니다 — 하지만 그런 일이 절대 일어나지 않는다는 약속은 거짓말일 것이고, 그런 약속을 하는 호스팅 업체라면 어디든 에누리해서 들으셔야 합니다.

둘째, 보복은 선택지에 없습니다. booter·stresser 서비스는 테스트 도구로 위장한 유료 공격 대행 서비스이며, 중요한 관할권 대부분에서 불법이고, 광범위하게 감시받고 있으며, 그 밖의 거의 모든 것을 허용하는 이곳의 정책 아래에서도 이것만큼은 여러분의 서버를 종료시킬 것입니다. 공격자에게 DDoS를 매력적으로 만드는 그 비대칭성은 방어자에게는 그것을 무가치하게 만듭니다: 잃을 것이 없는 상대를 상대로는 맞플러드로 이길 수 없습니다. 흡수하고, 캐시하고, 옮기고, 지루해지게 내버려 두십시오 — 자신이 보내는 트래픽에 비용을 치러야 하는 쪽에게는, 결국 그렇게 되기 마련입니다.

  1. 90초 만에 계층 특정하기

    설정을 바꾸기 전에 먼저 두 축과 두 카운터를 확인하십시오. 비트는 높고 패킷은 낮으면 대역폭 공격이고, 패킷은 높고 비트는 낮으면 상태 공격이며, 둘 다 평범한데 CPU만 고정되어 있으면 L7입니다.

    sar -n DEV 1 5                       # bits/s and packets/s per interface
    ss -s                                # socket summary; watch synrecv
    nstat -az | grep -E 'ListenDrops|ListenOverflows|SyncookiesSent'
    cat /proc/sys/net/netfilter/nf_conntrack_count
    dmesg -T | grep -i conntrack | tail

    그다음에는 성공한 트래픽과 자해성 트래픽을 배제하십시오: 액세스 로그를 10초만 훑어보십시오. 경로들이 실제로 사이트가 쓰이고 있는 모습이라면, 여러분에게 있는 것은 공격자가 아니라 방문자이거나 버그입니다.

  2. 커널 공격을 저렴하게 넘기기

    파일 두 개, reload 한 번. 이는 범용 서버라면 어디에나 안전하게 적용할 수 있으며, 소형 머신이 쓰러지는 가장 흔한 두 가지 경로를 제거해 줍니다.

    cat > /etc/sysctl.d/99-flood.conf <<'EOF'
    net.ipv4.tcp_syncookies = 1
    net.ipv4.tcp_max_syn_backlog = 8192
    net.core.somaxconn = 8192
    net.ipv4.tcp_synack_retries = 2
    net.ipv4.tcp_keepalive_time = 600
    net.netfilter.nf_conntrack_max = 262144
    net.netfilter.nf_conntrack_buckets = 65536
    net.netfilter.nf_conntrack_tcp_timeout_established = 3600
    net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 20
    EOF
    
    sysctl --system
    sysctl net.ipv4.tcp_syncookies net.netfilter.nf_conntrack_max   # verify, do not assume

    그다음에는 애플리케이션도 맞추십시오: nginx 쪽에도 listen 443 ssl backlog=8192;가 필요합니다. 그렇지 않으면 방금 키운 커널 큐가, 프로세스가 요청한 더 작은 값에 의해 그대로 제한되어 버립니다.

  3. 서비스하지 않는 모든 것을 게스트보다 위에서 닫기

    실제로 무엇이 대기하고 있는지 나열하고, 공인 주소에 있어야 할 것이 무엇인지 정한 다음, 거친 규칙은 하이퍼바이저 엣지 필터로 올려서 트래픽이 여러분의 가상 NIC에 아예 도달하지 않게 하십시오.

    ss -tlpn; ss -ulpn        # every public listener, with the process that owns it

    서버 → 네트워크 → 방화벽에서 엣지 필터의 기본 정책을 drop으로 설정하고, 여러분이 서비스하는 포트만 허용하며, SSH는 관리에 쓰는 주소로만 제한하십시오. 게스트의 nftables 규칙셋은 두 번째 계층으로 그대로 유지하십시오 — 이것 아니면 저것이 아니라 이중 안전장치입니다 — 그리고 데이터베이스와 관리자 인터페이스는 공인 포트를 방화벽으로 막는 대신 127.0.0.1이나 WireGuard 주소에 바인딩하십시오.

  4. 비용이 큰 경로 앞에 캐시 두기

    페이지를 렌더링하는 무엇에게나 가장 값어치가 큰 단일 변경입니다. proxy_cache_lock onproxy_cache_use_stale를 갖춘 proxy_cache를 추가하십시오. 그러면 동시에 발생한 천 건의 캐시 미스가 오리진 요청 단 한 건으로 줄어들고, 나머지 모두는 약간 오래된 페이지를 받게 됩니다.

    nginx -t && systemctl reload nginx
    curl -sI https://example.com/ | grep -i x-cache     # MISS, then HIT

    믿기 전에 히트율부터 확인하십시오. 모든 응답에 Set-Cookie가 붙어 있어서 절대 히트하지 않는 캐시는, 이 작업 전체에서 가장 흔한 가짜 안도감입니다 — curl -sI를 두 번 실행해 X-Cache를 읽어 보며 확인하십시오.

  5. 방문자가 아니라 비용을 만드는 경로에 속도 제한 걸기

    전역에는 넉넉한 제한을, 데이터베이스를 건드리는 엔드포인트에는 빡빡한 제한을 적용하십시오. 언제나 nodelay를, 언제나 상태 코드와 함께, 조용한 큐잉은 절대 쓰지 마십시오.

    limit_req_zone $binary_remote_addr zone=general:16m rate=20r/s;
    limit_req_zone $binary_remote_addr zone=costly:16m  rate=2r/s;
    limit_req_status 429;

    nginx가 어떤 프록시 뒤에 있다면 set_real_ip_fromreal_ip_header부터 먼저 설정하십시오. 프런트엔드 자신의 주소를 키로 삼은 제한은 공격자를 조절하지 못합니다 — 대신 처음 발동하는 순간 모든 사람을 동시에 조절해 버립니다.

  6. 오리진 주소를 공개된 시야에서 치우기

    맨 IP를 쓸모없게 만든 다음, 무엇을 그 앞에 세울지 정하십시오. 거부 설정은 두 줄이면 되고, 인터넷 전체를 스캔해 여러분의 HTML을 찾아내는 경로를 영구히 막아 줍니다.

    server {
      listen 80 default_server;
      listen 443 ssl default_server;
      ssl_reject_handshake on;
      return 444;
    }

    그런 다음 누출이 흔히 일어나는 순서대로 점검하십시오: 패시브 DNS 아카이브에 남은 과거 A 레코드, 한때 오리진을 가리켰던 호스트네임이 등장하는 인증서 투명성 로그, 그리고 발신 메일 헤더입니다. 이 중 하나라도 여전히 그 주소를 지목하고 있다면 프런트엔드로는 여러분을 구하지 못합니다 — 먼저 주소를 바꾸고, 그다음에 프런트를 세우십시오.

  7. 숫자를 갖춰서 네트워크 운영팀에 알리기

    대역폭 공격 완화는 자동으로 이루어지며 티켓이 필요 없지만, 증거를 담은 보고서가 있으면 사람이 나서서 자동화가 실제로 무엇을 했는지 확인하고, 자동화가 볼 수 없는 장애 유형 — 이를테면 스크러버가 정상 세션에 리셋을 쏘는 것 같은 — 을 잡아낼 수 있습니다.

    목적지 주소, UTC 기준 시작 시각, 프로토콜과 목적지 포트, 여러분이 직접 측정한 양쪽 축의 속도, 그리고 여러분 자신의 카운터에 패킷이 도착하는 것으로 보이는지 아니면 업스트림에서 사라지는 것으로 보이는지를 함께 보내십시오. tcpdump -ni eth0 -c 200의 결과 50줄이 설명 한 문단보다 더 값어치가 있습니다. “사이트가 느려요”는 조치할 수 없지만, “185.x.x.x, 14:02 UTC, UDP to 443, 약 1.2 Mpps, 인터페이스에는 40 Kpps 도착으로 표시됨”은 조치할 수 있습니다.

  8. 아무 문제 없을 때 주소 이전 연습해 두기

    오늘 DNS TTL을 300초로 설정하고 그대로 유지하십시오. 그런 다음 한 시간 뒤에 없앨 머신 하나로, 처음부터 끝까지 훈련을 한 번 해 보십시오.

    # deploy a second instance in another region, restore, verify by hostname override
    curl --resolve example.com:443:<new-ip> https://example.com/ -sI

    그 과정이 몇 분 걸렸는지 적어 두십시오. 그 숫자가 여러분의 진짜 DDoS 대응 계획입니다 — 이 가이드의 어떤 규칙보다도 더 그렇습니다 — 표적 공격이 여러분을 얼마나 오래 오프라인 상태로 묶어 둘 수 있는지를 말해 주는 숫자이기 때문입니다. 요금은 잔액에서 일 단위로 비례 청구되므로, 이 리허설 전체의 비용은 몇 센트에 불과합니다.

비교

각 공격을 실제로 막을 수 있는 지점

모든 계층은 무언가는 막아 내지만 나머지에는 눈이 멀어 있습니다. 사람들의 주말을 날려 버리는 실수는, 애초에 공격이 보이지도 않았던 계층에서 방어하는 것입니다.
계층막아 주는 것막지 못하는 것여러분이 치르는 비용누가 통제하는가
업스트림 스크러빙대역폭 공격 — 10 Gbps 미만은 흡수, 100 Gbps까지는 스크러빙, 그 이상은 BGP로 우회정상적인 트래픽 물량 안에 들어오는 모든 것: 상태 공격, L7아무것도 없음. 항상 켜져 있고, 티켓도 필요 없으며, 우회 중 가끔 지연이 생길 뿐저희 쪽, 자동으로
하이퍼바이저 엣지 필터닫힌 포트로 향하는 모든 것을, 가상 NIC에 닿기도 전에 — CPU도, 상태 항목도 쓰지 않음계속 열어 둬야 하는 포트를 노리는 공격여러분이 작성한 규칙 말고는 아무 비용도 없음; 상태 저장형, 기본 차단여러분, 서버별로
게스트 방화벽 (nftables)synproxy를 쓴 SYN 플러드, 발신지별 패킷 속도, 원치 않는 프로토콜이미 위쪽 회선을 채워 버린 트래픽 — 애초에 도착하지 않음패킷 하나하나에 대한 CPU와 conntrack 항목 — 버려지는 패킷까지 포함여러분, 전적으로
커널 튜닝 (sysctl)accept 큐와 conntrack 고갈 — 소형 서버를 죽이는 전형적인 원인유효하고 완료된 연결이라면 무엇이든합리적인 conntrack 크기 기준 약 80 MB의 RAM. 파일 두 개여러분, 전적으로
애플리케이션 (캐시 + 속도 제한)L7 플러드, 요청 스탬피드, 비용이 큰 엔드포인트패킷 수준의 공격 — 웹 서버까지 도달하지 않음약간 오래된 페이지, 그리고 잘못 판단한 사용자들에게 나가는 429여러분, 전적으로
리버스 프록시 프런트엔드오리진을 직접 노리는 공격 — 겨냥할 공인 주소 자체가 없음프런트 자체를 노리는 공격, 그리고 오리진 IP가 유출된 이후의 모든 것월 5달러와 패치를 계속 관리해야 할 머신 한 대직접 운영한다면 여러분
상업용 CDN대규모 L3/L4 및 L7 공격을, 규모 있게, 지원 창구와 함께한 번이라도 공개된 적 있는 오리진, 또는 맨 IP로 응답하는 오리진여러분이 누구인지 알고 법적 송달도 받을 수 있는 제3자에 의한 TLS 종단CDN 업체
Tor 어니언 서비스IP 기반 공격 전부 — 프로토콜 안에 주소 자체가 없음introduction point 플러드 — 그래서 최신 Tor는 작업 증명 방어책을 탑재하고 있음지연, 그리고 Tor를 기꺼이 쓸 이용자층여러분과 네트워크
FAQ

자주 묻는 질문

제 서버에서 DDoS 방어 기능을 따로 켜야 하나요?

아닙니다. 대역폭 공격 완화는 PoP보다 상류에 있고 항상 켜져 있습니다 — 구매할 상품도, 켜야 할 스위치도 없습니다. 10 Gbps 미만은 눈에 띄는 영향 없이 흡수되고, 10~100 Gbps는 트랜짓 제공업체 단에서 스크러빙되며 이때 짧은 지연 증가를 느낄 수도 있고, 100 Gbps를 넘으면 해당 프리픽스를 전용 스크러빙 경로로 announce하여 지연은 더 눈에 띄게 늘지만 서비스는 계속 응답합니다. 이 기준치는 문서에 공개되어 있습니다. 자동이 아닌 것은 L4 위쪽의 모든 것입니다: 상태 테이블 고갈과 애플리케이션 계층 공격은 업스트림에서 볼 때 평범한 트래픽처럼 보이며, 여러분의 머신에서 직접 처리해야 합니다.

공격을 받으면 제 IP를 널 라우팅하시나요?

최후의 수단으로만, 그리고 단순히 여러분의 서버만이 아니라 더 넓은 PoP 전체를 위협하는 지속적인 공격에 대해서만 그렇게 합니다 — 그리고 그런 일이 벌어지면 여러분이 스스로 알아차리게 두지 않고 몇 분 안에 알려 드립니다. 이런 일이 절대 일어나지 않는다고 약속하는 호스팅 업체가 있다면, 그것은 네트워크가 아니라 마케팅을 설명하고 있는 것입니다: 충분히 큰 규모에서는, 수천 명의 고객을 보호한다는 것이 결국 주소 하나를 거두어들이는 일을 뜻하게 됩니다. 현실적인 보호책은 스스로를 매력 없는 표적으로 만드는 것입니다 — 오리진 주소를 비공개로 유지하고, 연습해 둔 주소 이전 계획을 갖추고, 복구에 걸리는 시간을 파악해 두십시오.

사이트는 다운됐는데 대역폭 그래프는 정상으로 보입니다. 무슨 일인가요?

거의 확실히 상태 고갈입니다 — 전형적인 경우가 nf_conntrack 테이블이 가득 차는 것입니다. 위조된 발신지에서 오는 초당 2만 개의 작은 패킷은 단 몇 메가비트만 쓰면서도 기본 테이블을 몇 초 만에 채워 버리므로, 그래프에는 아무것도 나타나지 않는 채로 모든 새 연결이 버려집니다. dmesg -T | grep conntrack으로 “table full, dropping packet”을 확인하고, nf_conntrack_countnf_conntrack_max와 비교해 보십시오. 다른 가능성은 정반대의 해석입니다: 위쪽 회선이 이미 꽉 차서, 여러분의 인터페이스가 공격이 아니라 그 공격에서 살아남은 것만 보여 주고 있는 경우입니다.

더 큰 플랜이면 작은 플랜은 견디지 못하는 공격도 버틸 수 있나요?

가끔은 그렇지만, 사람들이 기대하는 이유 때문은 아닙니다. 더 많은 vCPU와 RAM은 L7 플러드와 상태 고갈에는 실제로 도움이 됩니다. 이런 공격은 CPU와 메모리와 테이블 공간을 고갈시키기 때문입니다. 대역폭 공격을 상대로는 플랜이 거의 무관합니다 — 그 뒤에 무엇이 있든 패킷은 여러분의 포트보다 상류에서 이미 버려지며, 2.5 Gbps짜리 포트라고 해서 40 Gbps로부터 여러분을 구해 주지 않습니다. 업그레이드하기 전에 캐싱과 conntrack 크기부터 고치십시오. 더 저렴하고, 대개는 그것이 진짜 문제였던 것으로 드러납니다.

no-KYC VPS 앞에 Cloudflare 같은 CDN을 둬도 되나요?

기술적으로는 가능하고, 실제로 잘 작동합니다. 다만 그렇게 하기 전에 어떤 트레이드오프를 감수하는지 이해해 두십시오: CDN은 여러분의 TLS를 종단하고 평문을 들여다보며, 오리진 주소를 알고, 이메일과 흔히 결제 수단으로도 여러분을 특정할 수 있는 계정을 보유하며, 여러분이 고르지 않은 관할권에서 법적 절차를 송달받을 수 있습니다. 이름을 남기지 않고 구매한 서버 입장에서는, 이것이 애초에 그 구조로 피하려 했던 바로 그 당사자를 다시 끌어들이는 셈입니다. 위협 모델이 노출이 아니라 다운타임이라면 합리적인 선택입니다 — 다만 오리진이 단 한 번도 공개된 적이 없어야 하며, 그렇지 않으면 오래된 DNS 레코드 하나가 이 모든 것을 무너뜨립니다. 위협 모델에 “누가 내 위치를 아는가”까지 포함된다면, 대신 두 번째 인스턴스에서 여러분 자신의 리버스 프록시를 직접 운영하고, 오리진은 오직 그곳으로부터의 트래픽만 받아들이게 하십시오.

fail2ban만으로 DDoS를 막기에 충분한가요?

아닙니다. 이는 약한 도구라기보다 애초에 맞지 않는 도구입니다. 로그를 파싱한 뒤 사람 시간 단위로 반응하므로, 요청들은 이미 치를 비용을 여러분에게서 다 받아 간 뒤입니다. 한 번에 주소 하나씩만 차단하므로 만 개의 발신지 앞에서는 아무 소용이 없고, 시도하게 내버려 두면 여러분의 방화벽을 선형 스캔으로 만들어 버립니다. 그리고 공격자가 얼마든지 플러드로 채울 수 있는 로그에 의존합니다. fail2ban은 자신이 잘하는 일 — SSH와 로그인 폼을 상대로 한 느린 자격 증명 추측 — 에만 쓰고, 플러드 속도로 벌어지는 일에는 타임아웃을 둔 nftables 동적 집합을 사용하십시오.

국가나 ASN을 통째로 차단해야 하나요?

국가 차단은 이용자층이 정말로 한정되어 있고, 여행 중인 사용자와 이상하게 라우팅된 사용자를 잃는 것을 감수할 수 있을 때만 옹호할 만합니다. 여러분의 나라를 포함해 전 세계 가정용 회선에 퍼져 있는 현대의 봇넷을 상대로는 거의 쓸모가 없습니다. 데이터센터 ASN을 차단하는 쪽이 더 날카롭습니다 — 일반 사람들은 호스팅 대역에서 웹서핑을 하지 않으므로 — 게다가 덤으로 스크래핑과 크리덴셜 스터핑까지 잡아냅니다. 더 나은 본능은 트래픽의 모양으로 걸러내는 것입니다: 공통된 User-Agent, 누락된 헤더, 반복되는 경로 하나. 이런 규칙은 공격자가 주소를 바꾼 뒤에도 계속 작동합니다 — 그리고 공격자는 몇 분 안에 주소를 바꿀 것입니다.

Tor 어니언 서비스를 운영하면 안전해지나요?

IP 기반 공격에 한해서는 그렇습니다 — 프로토콜 안에 플러드를 겨냥할 주소 자체가 없으며, 이는 점진적인 개선이 아니라 근본적으로 다른 보안 태세입니다. 다만 전반적으로 안전해지는 것은 아닙니다: 어니언 서비스도 introduction point에서 공격받을 수 있으며, 그래서 최신 Tor는 클라이언트 쪽에서 플러드 비용을 비싸게 만드는 작업 증명 방어책을 탑재하고 있습니다. 또한 지연이라는 비용이 들고, Tor를 기꺼이 쓸 이용자층으로 대상이 제한됩니다. 많은 사람이 둘 다 운영합니다 — 도달 범위를 위한 클리어넷 프런트 하나와, 클리어넷 쪽이 공격받는 동안에도 계속 작동하는 어니언 주소 하나입니다.

Deploy your offshore server.

지역을 선택하세요. 플랜을 선택하세요. 키를 붙여넣으세요. 결제하세요. 다음 47초는 저희가 책임집니다.