콘텐츠로 건너뛰기

[블로그운영] 내 블로그에 오는 요청의 상당수는 사람이 아니라 봇 트래픽이었습니다

안녕하세요? 정리하는 개발자 워니즈입니다.

서버가 남긴 접속 기록 106,863줄을 직접 뜯어봤더니 요청이 가장 많이 몰린 두 경로는 글을 읽으러 온 사람이 브라우저에서 누를 일이 없는 곳이었습니다. 필자는 그동안 요청이 늘어나면 읽는 사람이 늘어난 것이라고만 여기고 있었는데 실제로는 봇 트래픽이 상당한 몫을 차지하고 있었던 셈입니다.

앞으로 지킬 기준은 한 줄로 줄일 수 있는데요 요청량을 볼 때 총량부터 보지 않고 어떤 경로에 몰려 있는지를 먼저 확인합니다.

봇 트래픽 비중과 캐시 구간별 응답 시간을 비교한 그래프

로그를 열어보기 전까지는 전부 사람이라고 여겼습니다

필자는 요청 수가 올라간 날이면 어떤 글이 잘 걸렸는지를 짐작해보는 정도로 넘어가고 있었는데요 정작 그 요청들이 어느 주소로 들어왔는지는 한 번도 확인해본 적이 없었습니다.

그래서 이번에는 요약된 화면 대신 서버가 직접 적어둔 접속 기록 원본을 열어서 106,863줄을 경로 기준으로 하나씩 세어봤는데요 세어보고 나서야 필자가 지금까지 보고 있던 숫자의 정체를 알게 됐습니다.

봇 트래픽이 두 경로에만 몰려 있었습니다

경로별로 정렬해보니 가장 위로 올라온 것이 /xmlrpc.php로 요청 횟수가 10,003회였고 그 바로 아래를 /wp-login.php가 7,657회로 받치고 있었습니다.

두 경로는 성격이 분명한데요 /xmlrpc.php는 외부 프로그램이 글을 올리거나 가져갈 때 쓰던 오래된 원격 연결 창구이고 /wp-login.php는 관리자 로그인 화면인데요 글을 읽으러 온 사람의 이동 경로에는 둘 다 들어올 이유가 없는 주소입니다.

워드프레스 공식 문서의 XML-RPC 설명을 다시 읽어보니 이 창구는 지금은 다른 방식으로 대체된 옛 경로라고 되어 있었는데요 필자 블로그에서 이 주소를 쓰는 프로그램이 하나도 없었기 때문에 여기 쌓인 만 건은 전부 사람이 아닌 쪽이 문을 두드린 흔적이었습니다.

단일 아이피 하나가 전체 요청의 28%였습니다

더 놀라웠던 것은 요청을 보낸 쪽을 기준으로 다시 묶어봤을 때였는데요 주소 하나(103.200.219.x 대역)에서 들어온 요청이 30,424회로 전체의 28%를 차지하고 있었습니다.

사람이 같은 블로그를 삼만 번 넘게 열어볼 수는 없기 때문에 이 숫자는 자동으로 돌아가는 검색기나 공격 도구라고 볼 수밖에 없었습니다. 요청을 보낸 쪽이 몇 곳인지를 세어보지 않으면 이런 쏠림은 그냥 방문이 늘어난 것처럼 보이게 됩니다.

캐시가 막아줄 거라고 믿었는데 아니었습니다

필자는 페이지 캐시를 걸어둔 상태였기 때문에 이런 요청도 캐시가 대신 받아준다고 막연히 생각하고 있었는데요 응답 시간을 재보니 그 생각이 틀렸다는 것을 알게 됐습니다.

캐시에 적중한 정적 페이지는 평균 0.09초에 끝났는데요 반면 /wp-login.php는 0.41초였고 캐시를 우회하는 동적 요청은 0.59초였습니다. 여섯 배가 넘는 차이가 나는 이유는 단순한데요 캐시는 미리 만들어둘 수 있는 페이지에만 걸리고 로그인이나 원격 연결 창구는 애초에 미리 만들어둘 수 있는 대상이 아니기 때문입니다.

즉 공격이 몰리는 지점과 캐시가 걸리는 지점이 처음부터 서로 달랐는데요 캐시는 사람이 읽는 글을 빠르게 내주는 일은 잘 하고 있었지만 봇 트래픽이 향하는 곳에서는 아무 역할도 하지 못했습니다.

이 요청들이 서버 부담으로 그대로 쌓였습니다

응답 시간에 요청 횟수를 곱해서 대략의 처리 부담을 계산해봤더니 /wp-login.php 쪽이 51 CPU분 정도였고 /xmlrpc.php 쪽이 35 CPU분 정도로 나왔습니다.

혼자 보면 큰 숫자가 아닌 것처럼 느껴지는데요 필자가 쓰는 서버는 2 vCPU짜리 소형 사양이라 이 정도 부담이 몰리는 시간대에는 실제로 읽는 사람의 응답이 밀릴 수밖에 없습니다. 글을 하나도 더 읽히지 않은 상태로 서버의 처리 시간을 이만큼 내주고 있었던 것입니다. 코어 파일을 하나씩 검증했던 기록도 결국 같은 계열의 확인 작업이었는데요 눈에 보이는 화면이 멀쩡하다는 것과 서버가 무엇을 받고 있는지는 별개의 문제였습니다.

총량이 아니라 경로를 보기로 했습니다

이번에 얻은 것은 대책이 아니라 관찰 결과 하나인데요 필자는 요청량이 늘어난 것을 좋은 신호로만 읽고 있었지만 그 안에는 읽는 사람의 방문과 자동화된 요청이 섞여 있었습니다. 봇 트래픽은 총량 안쪽에 조용히 섞여 들어오기 때문에 합계 숫자만 보고 있으면 끝까지 구분되지 않습니다.

그래서 앞으로는 숫자가 움직인 날에 총량을 먼저 보는 대신 어떤 경로가 그 움직임을 만들었는지를 열어보려고 하는데요 경로 상위 목록과 요청을 보낸 쪽의 상위 목록 두 개만 뽑아봐도 사람인지 아닌지가 꽤 빠르게 갈립니다.

봇 트래픽에 관해 많이 묻는 질문

Q. 접속 기록을 직접 열어보지 않으면 알 수 없는 걸까요? 요약된 화면만 보면 경로별로 나뉘어 있지 않기 때문에 알기 어렵다고 느꼈는데요 필자도 원본을 열어보고서야 두 경로에 만 건씩 쌓여 있다는 사실을 알게 됐습니다. 경로별 상위 목록과 요청을 보낸 쪽 상위 목록만 세어봐도 대략의 윤곽은 잡히는 것 같습니다.

Q. 요청이 몰린 것을 곧바로 공격이라고 볼 수 있나요? 한 번의 요청만으로는 단정하기 어렵다고 보는데요 다만 로그인 화면과 원격 연결 창구처럼 사람이 들어올 이유가 없는 주소에 만 건 단위로 쌓여 있다면 성격은 꽤 분명해지는 것 같습니다. 여기에 주소 하나가 전체의 28%를 차지하는 쏠림까지 겹치면 사람의 방문으로 설명하기는 어려워집니다.

Q. 캐시를 더 세게 걸면 부담이 줄어들까요? 필자가 재본 결과로는 그렇게 되지 않았는데요 부담이 몰리는 지점이 캐시에 담아둘 수 없는 동적 주소이기 때문에 캐시 설정을 조여도 그 요청은 그대로 서버까지 도달합니다. 캐시는 읽는 사람의 체감 속도를 올려주는 장치이지 문을 두드리는 요청을 걸러주는 장치가 아니라고 정리했습니다.

정리

  • 접속 기록 106,863줄을 경로 기준으로 세어보니 /xmlrpc.php 10,003회와 /wp-login.php 7,657회가 상위를 차지했습니다.
  • 두 경로는 모두 읽으러 온 사람이 브라우저에서 누를 일이 없는 주소였습니다.
  • 요청을 보낸 쪽으로 묶어보면 주소 하나가 30,424회로 전체의 28%였습니다.
  • 캐시 적중은 0.09초였지만 캐시가 걸리지 않는 로그인과 원격 연결 창구는 0.41초와 0.59초로 요청 하나당 부담이 훨씬 컸습니다.
  • 두 경로가 만든 처리 부담은 51 CPU분과 35 CPU분 규모였고 2 vCPU 서버에서는 작지 않은 양이었습니다.
  • 그래서 요청량은 총량이 아니라 경로별로 나눠서 보기로 기준을 바꿨는데요 봇 트래픽은 경로를 열어봐야 겨우 드러나는 종류의 요청이었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다