fail2ban을 Cloudflare와 같이 사용하기

2026년 8월 29일

제 웹 서비스들에 리버스 프록시 목적을 위해 SWAG Docker 이미지를 꽤 오랜 기간 동안 사용해 왔습니다. 어느 시점인지는 모르겠지만, Cloudflare의 DDoS 보호(기술적으로는 “오랜지 구름모양 버튼”이라고 하더군요)를 켜고 나서 fail2ban과 관련 설정이 제대로 작동하는지 확인하는 것을 잊어버렸습니다.

결국 오늘 알아내고 말았는데, 예전에 있었던 이상한 증상이 이해되기 시작되더라고요. (왜 갑자기 서버가 응답을 멈췄지? 그야 너가 빌어먹을 키르기스스탄에서 작동하는 봇넷과 같은 IP로 취급되니까 그렇겠지.) 그래서 온라인으로 알아보다가 이 블로그 글을 발견했는데, 이 분의 설정에서 변경한 몇 가지 부분을 적어내리고 싶었습니다.

구글이 방화벽을 관리하면 인생이 참 편해지네요

만약 제 블로그를 전부터 읽어오셨다면, Tailscale과 구글 클라우드 VM을 사용하여 홈 서버에 공인 IP 주소를 부여한 것을 알고 있을 겁니다. 그 말인 즉슨, 만약 원글 블로그에서 사용한 실제 IP를 불러올 때 사용하는 허가된 IP 리스트를 가져오는 방식을 사용하게 된다면, nginx가 Cloudflare IP 범위를 보지 못하고 Tailscale IP를 보게 되어 실제 IP를 불러오지 못하게 됩니다.

해결책은 의외로 간단했습니다. (적어도 제 환경에서는요). 그냥 nginx.conf 설정 파일의 http 블록 내부에서 다음 두 줄을 추가해주면 됩니다:

set_real_ip_from 100.10.10.80;
real_ip_header CF-Connecting-IP;

물론, 여기에서 100.10.10.80을 구글 클라우드 VM에 할당되어 있는 Tailscale IP로 변경해줘야 하고요.

그 다음, 구글 클라우드 콘솔의 “Network Security > Firewall policies” 메뉴에 들어가서, 새 방화벽 룰을 만들어줍니다. 이름은 아무거나로 설정하고, 타겟 태그(target tag)를 cloudflare-only와 같이 설정해줍니다. 그리고 소스 필터(source filters)에다가는 Cloudflare가 공개한 IP 범위 목록을 띄어서 넣어 줍니다.

이게 완벽하냐고요? 아니요. 만약 Cloudflare가 사용하는 IP 대역을 변경하게 된다면 구글의 방화벽이 트래픽을 막을 가능성이 있습니다. 하지만 그건 미래 나의 문제이고, 제가 호스팅하는 웹 서비스들은 그렇게 중요하지도 않으며, Cloudflare가 IP 대역을 마지막으로 업데이트한 시점이 2023년 9월인 것을 감안하면 괜찮을 것 같습니다. (아마도요.)

(알겠다고요, 아마도 언젠가는 gcloud CLI를 사용해서 대역들을 주기적으로 업데이트해주는 스크립트를 작성할 수도 있겠죠. 그리고 만약 해가 서쪽에서 뜨면 블로그 글로도 적어볼 수 있고요. 그건 오늘이 아니니까 일단 기대는 접으시라구요!)

만약 VM에 IPv6가 활성화되어 있다면 공개된 IPv6 IP 대역 목록도 비슷하게 추가해줘야 되는데, 제 경우에는 기본값이 비활성화라 해줄 필요가 없었습니다. (IPv6를 비활성화해도, Cloudflare가 IPv6 클라이언트를 IPv4로 매핑하기 때문에 로그에 가끔씩 IPv6 주소를 볼 수 있습니다!)

그 다음, VM 설정값을 변경해줍니다. 기존에 HTTP와 HTTPS 트래픽을 기본으로 허가해주는 태그들을 삭제하고, cloudflare-only 태그를 추가해줍니다. 이제 Cloudflare에서만 트래픽을 VM을 통해 홈 서버로 보낼 수 있죠.

Cloudflare 설정하기 (feat. UI 헤매기)

원글에서는 설정해야 되는 메뉴 여러 개를 나열하는데, 최근들어 Cloudflare의 UI가 탐색하기 꽤 복잡해진 것 같습니다. 물론, 제품군이 많아진 영향도 있겠지만, 그렇다고 제가 이해해줄 필요는 없겠죠…

어쨌든, 다음과 같이 설정을 해줍니다:

  1. 커스텀 목록 (Custom list)을 생성해줍니다.

    https://dash.cloudflare.com/?to=/:account/configurations/lists

    URL 안에 있는 계정 ID와 목록 ID를 메모해둡니다.

  2. API 토큰을 생성합니다.

    https://dash.cloudflare.com/profile/api-tokens/

    여기에서 필요한 권한은 Account.Account Filter Lists입니다.

  3. WAF 룰을 만들어줍니다.

    https://dash.cloudflare.com/?to=/:account/:zone/security/security-rules

    “IP Source Address” “is in list”를 선택하고, 앞서 만들어둔 목록을 선택합니다.

fail2ban 설정하기

원글의 설정 파일도 나쁘진 않은데, 요즘에는 대부분의 배포판이 jp보단 jq를 기본으로 배포하는 것 같아, 한번 수정해보았습니다:

[Definition]
actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
                 -d '[{"ip":"'"<cfip>"'","comment":"Created by fail2ban <name>"}]' \
                 <_cf_api_url>
actionunban = id=$(curl -s -X GET <_cf_api_prms> \
                   "<_cf_api_url>?search=<cfip>&per_page=1" \
                   | { jq -r '.result[0].id // ""' 2>/dev/null; })
              if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi;
              curl -s -o /dev/null -X DELETE <_cf_api_prms> \
                   -d '{"items":[{"id":"'"$id"'"}]}' \
                   <_cf_api_url>
_cf_api_url = https://api.cloudflare.com/client/v4/accounts/<cfaccountid>/rules/lists/<cfbanlistid>/items
_cf_api_prms = -H 'Authorization: bearer <cfapitoken>' -H 'Content-Type: application/json'

[Init]
cfip = <ip>

[Init?family=inet6]
cfip = $(fail2ban-python -c 'import sys; from fail2ban.server.ipdns import IPAddr; a = IPAddr(sys.argv[1]+"/"+sys.argv[2]); print(str(a))' "<ip>" 64)

그 다음 /etc/fail2ban/action.d/cloudflare-list.local 파일을 생성하고, 하단의 예시에 아까 메모해두었던 정보를 치환해줍니다. 원글과 다른 점이 없지만 혹시나 URL이 추후에 접속 불가능할 수도 있어 여기에 복사해둡니다:

[Init]
cfapitoken = <api-token>
cfaccountid = <account-id>
cfbanlistid = <list-id>

이제 원글과는 조금 다르게 나아갈 부분인데, 제 경우에는 nginxfail2ban이 외부에 공개되는 서버의 모든 부분을 담당하기 때문에, 이 새로운 차단 액션이 모든 fail2ban jail에 적용되었으면 합니다. 그렇게 하려면 설정이 무척 간단해지는데, /etc/fail2ban/jail.local을 편집하고 기본 banaction을 바꿔주면 되거든요:

[DEFAULT]
banaction = cloudflare-list

저장하고, 재시작하면 설정이 마무리됩니다!

테스트해보기 위해, 모바일 데이터로 접속한 다음에 휴대폰 IP를 한번 차단해봅니다:

fail2ban-client set <random_jail_name> banip <phone-ip>
fail2ban-client set <random_jail_name> unbanip <phone-ip>

원글 작성자인 Ken Harris님께 감사의 말씀 드립니다! 글이 CC BY-NC-SA 4.0 라이선스로 공개되어 있기 때문에, 이 블로그 글도 동일한 라이선스를 가져갑니다.