공격 지도에서 한 나라 위에 짙은 얼룩이 보이면 「그 나라를 통째로 막자」는 규칙이 저절로 떠오릅니다. 그러나 그것으로 얻는 것은 거의 없습니다. 출구 국가를 바꾸는 데 공격자가 쓰는 시간은 몇 분이고, 차단에 걸리는 쪽은 내 방문자와 출장 중인 나 자신입니다. 더 나쁜 것은, 하나의 클라우드에서 오던 눈에 보이던 크롤러가 차단된 뒤에는 가정용 주소로 옮겨 가 사람과 구분할 수 없게 된다는 점입니다.
아래는 같은 서버에서 실제로 결과를 내는 다섯 단계입니다. 스캐너를 끊고, 속도를 제한하고, 필요한 것은 끊지 않고, 그래도 필요하다면 올바르게 차단하는 것. 모두 한 번에 확인하고 설정할 수 있습니다.
1단계. 얼룩을 실제로 만들고 있는 쪽을 확인한다
먼저 요청 기록을 찾습니다. 사이트가 여러 개 올라간 서버에서는 access.log가 비어 있는 경우가 많고, 기록은 각 가상 호스트의 로그로 들어갑니다.
sudo ls -lS /var/log/apache2/*access*.log /var/log/apache2/*requests*.log 2>/dev/null
이후의 모든 명령에서는 이 목록의 맨 위에 올라온 파일로 바꿔 넣습니다. 먼저 하루 동안의 상위 주소, 그다음 맨 위에 있는 주소가 정확히 무엇을 요청하는지 봅니다.
sudo awk '{print $1}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -20
sudo grep '^203\.0\.113\.' /var/log/apache2/access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20
두 번째 명령이 핵심 질문에 답합니다. 출력에 내 사이트의 서로 다른 페이지가 수십 개 나온다면 내용 수집기입니다. 해롭지 않으며 방문자로 세지 않는 것으로 충분합니다. 거기에 /wp-login.php, /.env, /phpmyadmin처럼 애초에 없는 것들이 보인다면 스캐너이고, 그것은 2단계가 맡습니다. nginx라면 로그는 /var/log/nginx/에 있고 항목은 같습니다.
2단계. 없는 것을 찾는 요청에 대한 jail
요청된 경로는 위조할 수 없습니다. 주소나 User-Agent와 달리 그 자체가 의도이기 때문입니다. 필터 /etc/fail2ban/filter.d/web-probes.local:
[Definition]
failregex = ^<HOST> .* "(GET|POST|HEAD) /(wp-login\.php|wp-admin|xmlrpc\.php|\.env|\.git/|phpmyadmin|administrator/)
ignoreregex =
jail /etc/fail2ban/jail.d/web-probes.local:
[web-probes]
enabled = true
port = http,https
filter = web-probes
logpath = /var/log/apache2/*access.log
maxretry = 1
findtime = 1h
bantime = 1d
여기서 maxretry = 1은 타당합니다. 남의 관리자 페이지에 우연히 들어가는 일은 없기 때문입니다. 적용한 뒤 필터가 내 로그의 줄을 실제로 잡아내는지 확인합니다.
sudo fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/web-probes.local
sudo systemctl reload fail2ban && sudo fail2ban-client status web-probes
3단계. 속도를 제한한다
스캐너도 크롤러도 어느 나라에서 왔는지와 무관하게 속도로 정체가 드러납니다. 한 주소에서 분당 서른 페이지라는 기준선은 목록을 넘겨 보는 사람에게는 닿지 않습니다.
[http-flood]
enabled = true
port = http,https
filter = apache-overflows
logpath = /var/log/apache2/site_access.log
maxretry = 30
findtime = 60
bantime = 1h
결정적인 것은 logpath에 어떤 로그를 적느냐입니다. 그 패턴에 관리 패널 자체나 관리자 영역의 로그까지 들어가면 자기 자신을 차단하게 됩니다. 그곳의 페이지 하나가 스타일과 스크립트에 대한 요청을 열 개 남짓 끌고 오므로, 서른 개는 두 번의 클릭으로 채워집니다. 그래서 패널에는 jail의 패턴과 겹치지 않는 별도의 로그 파일을 주고, 정적 파일에는 긴 캐시 기간을 주어 페이지를 옮길 때마다 브라우저가 다시 요청하지 않게 합니다. 그래도 일어났을 때의 증상은 HTML은 오는데 꾸밈은 오지 않고 페이지가 멈춰 있는 상태입니다. 차단 해제:
sudo fail2ban-client set http-flood unbanip 203.0.113.10
4단계. 끊으면 안 되는 것을 지킨다
차단을 켜기 전에 예외를 적어 둡니다. 그러지 않으면 가장 먼저 걸리는 것이 바로 필요한 쪽입니다.
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24 66.249.64.0/19
이 줄에 반드시 들어가야 하는 것과 그 이유:
- 자기 주소 또는 사무실의 서브넷. 서버 접근 권한을 잃는 가장 흔한 원인은 공격이 아니라 스스로 만든 규칙입니다.
66.249.64.0/19대역. 구글의 검색 로봇은 User-Agent로 식별되지만, JavaScript를 실행하는 쪽은 평범한Chrome/Linux로 찾아오며 스스로를 밝히지 않습니다. 「데이터센터에서 오는 트래픽을 막는다」류의 규칙은 바로 그쪽을 때리고, 그다음에는 페이지가 조용히 색인에서 빠집니다. 의심스러운 주소는 역방향 조회로 확인할 수 있습니다.dig -x 66.249.66.1 +short는googlebot.com영역의 이름을 돌려주어야 합니다.- 결제 알림이 오는 주소. 결제 시스템의 webhook은 AWS와 Azure 서버에서 옵니다. 이 클라우드들을 통째로 닫으면 결제 확인이 도달하지 않게 되고, 그 사실은 돈은 빠져나갔는데 권한이 없다는 고객의 연락으로 알게 됩니다. 대역은 사용 중인 결제 사업자의 문서에서 가져옵니다.
5단계. 그래도 차단이 필요하다면 국가가 아니라 서브넷을 막는다
하나의 네트워크에서 와서 며칠씩 이어지고 같은 유형의 요청을 동반하는 꾸준한 밀집은, 목록을 따라 내려가는 한 명의 실행자입니다. 이런 경우는 범위를 좁혀 기한을 두고 차단합니다.
sudo ufw deny from 198.51.100.0/22 to any
sudo ufw status numbered
두 번째 명령으로 규칙 번호를 적어 두십시오. 나중에 차단을 푸는 것도 바로 그 번호를 통해서입니다(sudo ufw delete <번호>). 스스로 기한을 정하고 — 이를테면 한 달 — 해제를 일정표에 적어 둡니다. 「영구히」 넣은 규칙은 한 해가 지나면 아무도 손대지 못하는 목록이 됩니다. 그 주소들이 왜 거기 들어갔는지 아무도 기억하지 못하기 때문입니다.
어느 서브넷을 막을지는 1단계에서 얻은 주소로 알아볼 수 있습니다: whois 198.51.100.17 | grep -iE 'route|cidr|netname'.
이 다섯 단계 뒤에 달라지는 것
스캐너는 출신이 아니라 의도에 따라 끊기고 첫 요청에서 걸립니다. 크롤러는 속도 상한에 부딪힙니다. 검색 로봇과 결제 알림은 그대로 통과합니다. 넓게 남은 차단은 하나뿐이고, 그것도 범위가 좁으며 기한과 이유가 분명합니다.
지도가 사라지는 것은 아니지만 역할은 달라집니다. 국가를 닫기 위해서가 아니라, 밀집 자체를 알아차리고 일주일 전과 견주어 보기 위해 쓰이게 됩니다. 그러려면 당일의 한 장면이 아니라 기록이 필요합니다. 그런 지도가 차단 기록, 열린 포트 목록과 나란히 어떻게 보이는지는 아래 데모 페이지에서 확인할 수 있습니다.