Cloudflare + Fail2Ban 연동
Cloudflare API를 연동하여 Fail2Ban을 사용하는 방법을 소개합니다.
이전에 Fail2Ban을 이용해서 SSH 공격을 방어하는 방법을 소개했다. Fail2Ban은 로그를 읽어서 로그 패턴에 맞는 대상을 찾아 해당 IP 접근을 차단하는 식으로 사용된다. 도메인 제공자로 Cloudflare를 사용하면 fail2ban을 연동해서 DNS 단에서 차단할 수 있다. 오늘은 Vaultwarden에 fail2ban을 연동해서 비밀번호를 연속으로 틀린 IP를 DNS 단에서 차단해볼 것이다.
원리
원리는 간단하다. 특정 로그 파일을 지정하고 패턴을 지정했을 때, 동일 IP + 동일 로그가 룰에 맞게 등장을 하면 해당 IP를 Cloudflare에 요청하여 차단하는 것이다.
환경
- Docker
- Docker-compose
- Cloudflare
적용
나 같은 경우에는 Vaultwarden을 Docker를 이용하여 구축했기 때문에 fail2ban도 Docker를 통해 배포하였다.
먼저, 완성된 구조를 참고하고 가시라.
fail2ban 디렉터리와 vw-data/vaultwarden.log의 위치에 주목하면 된다. (꼭 이걸 준수할 필요는 없다.)
먼저, filter.d/vaultwarden-admin.local 파일을 만들고 아래와 같이 작성한다.
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*Invalid admin token\. IP: <ADDR>.*$
ignoreregex =
그 다음, filter.d/vaultwarden.local 파일을 만들고 아래와 같이 작성한다.
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =
이 파일들은 로그에서 찾을 패턴을 정규식으로 정의한 것이다.
admin.local 파일은 Vaultwarden의 관리자 콘솔 로그인 시도에 실패한 경우를 정의한 것으로 보이고, vaultwarden.local 파일은 웹 사이트에서의 ID/PW 인증에 실패한 경우를 정의한 것으로 보인다.
그 다음, jail.d/vaultwarden.local 파일을 만들고 아래와 같이 작성한다.
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
logpath = /vaultwarden/vaultwarden.log
bantime = 3600
findtime = 600
maxretry = 5
# Docker 네트워크의 FORWARD 체인 사용
banaction = iptables-allports
action = cloudflare
iptables-allports
logpath의 경우에는 아래 docker-compose 파일을 참고하여 fail2ban 컨테이너 내를 기준으로 log의 위치를 정확히 지정해야한다.
그 다음, action.d/iptables.local 파일을 만들고 아래와 같이 작성한다.
[Init]
blocktype = DROP
[Init?family=inet6]
blocktype = DROP
그 다음, action.d/cloudflare.conf 파일을 만들고 아래와 같이 작성한다.
#
# Author: Mike Rushton
#
# IMPORTANT
#
# Please set jail.local's permission to 640 because it contains your CF API key.
#
# This action depends on curl (and optionally jq).
# Referenced from http://www.normyee.net/blog/2012/02/02/adding-cloudflare-support-to-fail2ban by NORM YEE
#
# To get your CloudFlare API Key: https://www.cloudflare.com/a/account/my-account
#
# CloudFlare API error codes: https://www.cloudflare.com/docs/host-api.html#s4.2
[Definition]
# Option: actionstart
# Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false).
# Values: CMD
#
actionstart =
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop =
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck =
# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
# API v1
#actionban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=ban' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
# API v4
actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
-d '{"mode":"block","configuration":{"target":"<cftarget>","value":"<ip>"},"notes":"Fail2Ban <name>"}' \
<_cf_api_url>
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
# API v1
#actionunban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=nul' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
# API v4
actionunban = id=$(curl -s -X GET <_cf_api_prms> \
"<_cf_api_url>?mode=block&configuration_target=<cftarget>&configuration_value=<ip>&page=1&per_page=1¬es=Fail2Ban%%20<name>" \
| { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; })
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> "<_cf_api_url>/$id"
_cf_api_url = https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules
_cf_api_prms = -H 'X-Auth-Email: <cfuser>' -H 'X-Auth-Key: <cftoken>' -H 'Content-Type: application/json'
[Init]
# If you like to use this action with mailing whois lines, you could use the composite action
# action_cf_mwl predefined in jail.conf, just define in your jail:
#
# action = %(action_cf_mwl)s
# # Your CF account e-mail
# cfemail =
# # Your CF API Key
# cfapikey =
cfuser =
cftoken =
cftarget = ip
[Init?family=inet6]
cftarget = ip6
하단에 cfuser, cftoken은 Cloudflare에 접속해서 Global API Key 얻은 뒤 채우면 된다. 원문에 따르면 Fine Granted Token은 현재 미지원인 것으로 보인다.
그 다음, docker-compose에 아래와 같이 서비스를 추가하고 구동하면 된다.
services:
fail2ban:
container_name: fail2ban
restart: always
image: crazymax/fail2ban:latest
environment:
- TZ=Asia/Seoul
- F2B_DB_PURGE_AGE=30d
- F2B_LOG_TARGET=/data/fail2ban.log
- F2B_LOG_LEVEL=INFO
- F2B_IPTABLES_CHAIN=INPUT
volumes:
- ./data/fail2ban:/data
- ./data/vw-data/vaultwarden.log:/vaultwarden/vaultwarden.log:ro
network_mode: "host"
privileged: true
cap_add:
- NET_ADMIN
- NET_RAW