tl;dr

VCN 설정중 보안목록 부분에 각 rule의 stateful, stateless 여부를 선택하는 칸이 있다. 커널을 아무리 튜닝해도 SYN Flood로 인해 VNIC의 conn track이 꽉 차버리면 패킷자체가 넘어오질 않는다. 서버 운영중에 SYN Flood를 당하는것 같으면 이 부분을 꼭 확인해 보자. 해당되는 경우, Observation -> Service Matrix에서 “Ingress Packets Dropped by Full Connection Tracking Table” 그래프가 꽉 차는것을 볼 수 있을것이다.

VCN의 Security List Details 메뉴에 있는 Rule 생성·편집기의 모습. stateless 체크박스가 있다.
Observation -> Service Matrix의 그래프로 conntrack 상태를 확인할 수 있다.

시작하기 전…

아래의 글은 필자가 직접 운영하고 있는 서비스의 이야기는 아니다. 지인의 “원인을 알 수 없는 공격발생”을 도와주며 접하게 되었다. 필자는 옆에서 분석과 대응법 제시만 거들어 줬다. 실제 대응은 지인분이 직접 하셨다. 이 글은 그 때 있었던 일을 정리한 것이다.

문제의 시작 – 접속이 되지 않음

처음에 나에게 온 연락은 “서비스가 접속이 되지 않고, 네트워크 사용률 그래프를 보니 공격으로 의심된다”는 내용이었다. 기존에 연결되어 있는 TCP 연결은 아무 문제없이 작동하나, 새로운 연결은 전혀 되지 않았다 . 서버의 자원이 부족한 것도 아니었다. 기존 연결은 문제없으나 새 연결에서 문제가 보이는것은 전형적인 SYN Flood의 증상이기에, SYN Flood 공격이 발생중이고 그걸 방어하는것으로 방향을 잡았다.

SYN Flood 공격은 TCP의 3-way handshake 구조로 인해 발생한다. TCP는 새로운 연결을 만들때 SYN 상태가 적혀있는 패킷을 서버로 보낸다. 서버는 이것을 읽고 SYN과 ACK 상태가 적힌 패킷을 클라이언트로 보낸다. 클라이언트가 마지막으로 ACK 상태가 적힌 패킷을 서버로 보냄으로서 연결이 수립된다.

이 과정속에서 서버는 처음 수신된 패킷과 자신이 보낸 패킷의 정보를 기억하고 있어야 한다. 해당 정보들은 백로그(SYN Queue)에 잠시 저장된다. 하지만 백로그가 무한하지 않다는게 문제이다. 1개의 SYN 패킷은 1개의 큐 공간을 차지한다 (SYN Cookie는 문맥을 위해 제외). 초당 2만개의 SYN 패킷을 보내면 10초만에 20만개의 공간이 사라진다.

SYN 패킷을 무지막지하게 찍어내는 공격이니 만큼, 이 공격을 당하는 중에 패킷을 캡쳐하면 무수한 SYN 패킷이 찍힌다. 그러므로 위의 예상이 맞는지 확인하려면 tcpdump로 수 초~수 분간 패킷을 다 찍어보면 된다. 아쉽게도 첫날에는 시간이 부족해서 하지 못했다. 하루 단위로 딱 30분씩만 공격이 이루어졌기 때문이다. 첫날은 SYN Flood임을 의심만 하고 넘어갔다. 이 공격으로 인해 SSH까지 접속이 되지 않아서 시간을 더 끌기도 했다. (시간이 충분했으면 Private IP를 붙여서 접속했을것이다)

공격 기간중의 인바운드 패킷 그래프이다. 순간 치솟다가 패킷 수신이 멈춘다.
그리고 갈 수록 수신되는 패킷의 량도 적어진다.

물론 그냥 가만히 있지는 않았다. 웹VNC 콘솔로 접속해서 최소한의 정보를 모으려고 시도했다. netstat을 찍어봤을때 SYN_RECV가 80번 포트에 가득한 것을 보고 NGinx의 backlog를 긴급히 키웠다. NGinx의 SYN 관리 그래프가 512에서 5k로 높아졌지만 접속장애는 멈추지 않았다.

분석의 시작과 1차 시도

다음 기회가 찾아왔다. 즉시 tcpdump를 하고 데이터를 까 보았다. 예상한대로 SYN 패킷이 가득했다. SYN Flood 공격임을 확인했다. 각 패킷의 발신 IP는 모두 랜덤하며 겹치는것이 없었다. IP가 Spoofing 된 것으로 판단했다. SYN Flood를 막는 가장 쉬운 방법은 패킷을 DROP 하는 것이다. 커널의 TCP 처리단에 패킷이 못들어가게 하면 끝이다. 덤프된 패킷을 확인해 보니 특정한 규칙을 가지고 있었다. iptables의 u32 모듈을 이용해서 특정 시그니처의 SYN 패킷을 모두 DROP 하는것으로 대응 방향을 설정했다.

패킷의 시그니처를 이용해서 전부 DROP 시키는것은 사실 처음해보는 것이었다. 제대로 설정하니까 그 날의 공격이 거의 끝났다. 필자는 u32 모듈이 Ethernet 부분은 처리하지 못한다는 부분을 헤매었다. Wireshark 상에서는 TCP 패킷이 [Ethernet Header + IP Header + TCP Header + Payload] 로 표시된다. 하지만 u32 모듈에서는 Ethernet Header 영역이 짤린채로 들어온다. (iptable이기 때문에 IP Header 영역부터 들어온다고 생각하면 될 듯하다.) 그러므로 Wireshark 상의 위치에서 14를 빼야한다. 예를들어, Wireshark에서 46번째 바이트가 TCP Flag일때 u32에서는 22번째 바이트로 봐야한다는 것이다.

2차시도 – U32 모듈과 SynProxy

iptables에서 u32모듈을 이용한 필터링 규칙이 pkts가 초당 1천[email protected] 단위로 오르는것을 확인했다. 하지만 접속장애는 계속 이어졌다. 이때 VCN Firewall에서 stateful를 설정하면 VNIC의 Conntrack을 소모하는지 몰랐다. 그래서 서버단에서 공격 패킷들이 무언가 영향을 주고 있다고 생각했다. 차단 규칙의 target을 raw에 걸었음에도 문제가 지속되었다. 그래서 eBPF로 iptables 이전 단계에서 필터링 해야하나 생각까지 했다.

u32 룰이 작동되는것 같긴하지만, 접속장애는 여전히 계속됐기 때문에 다른 방법을 찾아봐야 했다. 이번에는 지인(해 서비스 개발자)이 선택한 SynProxy를 적용해 보기로 했다. Syn Proxy는 IPTables단에서 SYN을 인터셉트해서 따로 처리하는 방식이다. SYN Proxy는 SYN Queue를 쓰지않고 SYN을 관리한다. 백로그큐를 소모하지 않고 SYN을 처리하는 방법이다. 이것 또한 pkts수가 오르는것을 보고 작동하는듯 했으나, 접속불가를 해소하진 못했다.

오라클(OCI) VIC – Security rule의 발견

두 방법 모두 접속장애를 해결하지 못했다. iptables 이전의 무언가가 서버에 영향을 준다고 말이 모아졌다. 나는 eBPF를 써야하나.. 생각하던 찰나에 지인분이 오라클 VCN의 security rule이 stateful 하다는것을 발견했다. 80번 포트의 규칙을 stateless로 설정하니 접속 장애 문제가 해결되었다. 나중에 알아보니 각 서버의 유형(shape)에 따라서 conn track의 수가 제한되어 있음을 확인했다.

VCN 보안정책의 Stateful을 설정하면 VNIC 단위로 Conn tracking을 한다. 그러다 보니까 SYN Flood 패킷들 모두가 해당 목록에 들어갔다. 때문에 해당 목록이 꽉 차버렸다. 목록이 꽉 찬 상태에서는 새 연결 요청을 모두 DROP 시키기 때문에 우리측으로 패킷이 들어오지 못했던 것이다.

이 부분은 대형 서버를 구성한다면 마주칠 수도 있는 부분이다. 하지만 이를 언급한 곳이 적거나 없어 보인다. 이 부분을 놓칠 경우, 엄청난 수의 동시접속자를 받을때 서버의 튜닝과 상관없이 VNIC 단에서 접속을 막힐수도 있다. 공격이 오지 않더라도 사전에 수치를 확인해서 서버의 한계를 확인하는게 좋을것 같다.

Syn Proxy를 이용한 받아치기

어쨋든 VCN의 보안규칙을 수정하니 모든 패킷이 서버로 유입되기 시작했다. 최초에는 VNIC의 conntrack 때문에 20kps 언저리로 패킷이 수신되었고, 공격의 규모가 그 정도인줄 알았다. 하지만 패킷이 여과없이 들어오니 초당 225kps임을 확인했다. 생각했던것 보다 규모가 컸던 만큼, u32 모듈과 synproxy 설정간 누락된 것이 없는지 한번더 확인했다.

VCN 설정을 풀고난 뒤 SYN 패킷이 전부 서버로 들어올 때의 그래프이다. 
초당 225kps의 속도로 패킷이 들어왔다.
 SYN Proxy를 사용했다면 우리 서버도 이만큼의 패킷을 만들어 내야했다.

u32를 이용한 패킷 drop은 사람의 손이 필요하다. 잘못 설정할 경우에 정상 유저의 접속을 오히려 막을수있다. 지능형 차단시스템을 사용하지 않는 경우라면 사람의 손 밖에 방법이 없다. 여기엔 [장애 확인 -> 패킷 캡쳐 -> 패킷 분석 -> rule 작성] 과정이 필요하다. 이 부분은 장애 반응속도에 문제가 된다.

u32 모듈과 달리 SYN Proxy는 TCP Handshake를 진행한다. 덕분에 사람이 개입하지 않아도 SYN Flood를 처리할 수 있다. 그러나 단점도 있다. 들어오는 모든 SYN 패킷에 응답을 한다는 점이다. 상대가 200kps로 패킷을 보낸다면, 우리측도 200kps로 SYN-ACK를 보내야 한다. 우리도 1:1로 트래픽을 만들기 때문에, 공격이 대형으로 올 경우 발신 트래픽에 부담을 줄 수 있다.

그러므로 필자는 SYN Proxy와 DROP 방법을 같이 써야 한다고 생각한다. 공격이 예상될 때는 Syn Proxy로 서버 엔지니어가 대응할 시간을 벌어야 한다. 그 사이에는 들어오는 수신 트래픽만큼 발신 트래픽이 생긴다. SYN Proxy로 얻은 시간동안 엔지니어는 패킷을 분석하여 DROP 할 수 있는 규칙을 만들어 적용해야 한다. 트래픽도 중요하지만 접속장애가 나지 않도록 하는것이 더 중요하다고 생각하기 때문이다.

커널 업그레이드를 통한 방어력 상승

최신버전의 리눅스 커널을 사용한다면 SYN Proxy를 굳이 찾지 않아도 된다 2015년의 리눅스의 TCP Listener쪽 코드 리빌딩으로 커널안에서 Syn Proxy를 필적할 수준으로 SYN-ACK를 찍어 낼 수 있게 되었다. 우분투 18.04 LTS만 써도 이 기능을 사용할 수 있다. 이 기회에 서버 커널을 업데이트 하자.

VCN의 보안규칙에서 Egress를 Stateless로 설정할 시 외부접속이 안되는 문제

SYN Proxy를 이용해서 받아치는 경우, InGress뿐 아니라 Egress에도 stateless를 설정해야 한다. 응답을 위한 SYN-ACK도 conntrack에 잡히기 때문이다. 이 경우 한가지 문제점을 만들어낸다. 우리 서버에서 외부로 접속할 때 수신 패킷이 모두 DROP 된다. 즉, 외부접속이 안된다.

우리가 외부로 접속 할 때 서버에 있는 빈 포트를 src port로 걸고 통신을 시도한다. 원격지의 서버는 우리가 기제한 port로 응답패킷을 보낸다. 하지만 방화벽의 추적기능이 꺼져있어서 방화벽 입장에서는 이상한 포트로 패킷을 보냈다고 생각한다. 결국 80, 443등 지정한 포트외로 오는 모든 패킷들이 DROP 된다. 

이것을 막기 위해서는 “발신지 포트가 80 또는 443일 경우” 들어오는 패킷은 허용해 줘야한다. 보안을 위해서는 이런 꼼수(?) 대신에 별도의 네트워크를 붙이고 그쪽을 통하게 하는것이 더 좋지 않을까 생각해 본다.