Fail2ban은 가장 먼저 설치되고, 대개 거기에서 멈춥니다. 패키지는 깔렸고 서비스는 돌고 있으며 sshd는 보호되는 것처럼 보입니다. 반년 뒤에야 그 jail이 이 시스템에 존재하지도 않는 파일을 읽고 있었고, 나머지 전부 — 메일, 관리 패널, 사이트의 로그인 폼 — 은 한 번도 보호된 적이 없었다는 사실이 드러납니다.

사이트가 있는 평범한 VPS에서 무엇을 켤 가치가 있는지, 어떤 숫자를 넣을지, 그리고 jail이 설정 파일에 존재하기만 하는 게 아니라 실제로 작동하는지 어떻게 확인할지 살펴봅시다.

먼저 sshd가 무언가를 잡고 있는지 확인하기

명령 하나입니다.

sudo fail2ban-client status sshd

출력에는 Currently failed, Total failed, Total banned 줄이 있습니다. 인터넷에 최소 하루는 놓여 있던 서버에서 Total failed가 0이라면 그 jail은 작동하지 않는 것입니다. 현실과 맞춰 보세요.

sudo lastb | wc -l

lastb에는 수천 번의 실패한 시도가, Fail2ban에는 0이 — 이것이 뜻하는 바는 하나뿐입니다. 필터가 엉뚱한 곳을 보고 있다는 것.

가장 흔한 원인은 Debian 12입니다. 기본으로 rsyslog를 더 이상 설치하지 않아 파일 /var/log/auth.log가 아예 없는데, 표준 sshd jail은 바로 그 파일을 읽도록 설정돼 있습니다. 이에 대해 어떤 오류도 나오지 않습니다. 서비스는 시작되고 상태는 표시되며 카운터는 0에 머뭅니다. 해결책은 systemd의 저널로 옮기는 것입니다.

[sshd]
enabled = true
backend = systemd

다른 선택지는, 평문 auth.log가 다른 도구에 필요하다면 rsyslog를 패키지로 되돌리는 것입니다. Ubuntu 24.04도 똑같이 동작합니다.

자기 설정은 어디에 두는가

/etc/fail2ban/jail.conf는 건드리지 마세요. 패키지 업데이트 때 덮어써지고, 거기에 한 모든 수정은 언젠가 조용히 사라집니다. 당신의 설정은 /etc/fail2ban/jail.local로 갑니다 — 이 파일은 마지막에 읽히며 공통 파일보다 우선합니다.

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.25
bantime  = 1h
findtime = 10m
maxretry = 5
backend  = systemd

자기 주소를 넣은 ignoreip는 사치가 아니라 보험입니다. 오타 하나로 스스로를 밖에 가두는 일은 생각보다 쉽게 일어납니다. 고정 주소가 없다면 SSH 말고 들어갈 두 번째 길을 마련해 두세요 — 업체의 콘솔이나 VNC.

그림을 바꾸는 jail: recidive

평범한 jail의 기억은 짧습니다. 10분에 다섯 번, 한 시간 차단, 그리고 한 시간 뒤 모든 것이 처음부터 다시. 봇은 그것과 잘 지내며 내일도 모레도 돌아옵니다. recidive는 바로 그 틈을 막습니다. 시스템 로그가 아니라 Fail2ban 자신의 로그를 읽습니다. 즉 Fail2ban이 이미 차단한 상대를 차단하는 것입니다.

[recidive]
enabled  = true
bantime  = 1w
findtime = 1d
maxretry = 5

이렇게 읽으세요. 「하루에 다섯 번 차단됐다면 일주일 퇴장」. 이 jail은 파일 /var/log/fail2ban.log를 필요로 하므로, Fail2ban 자체의 로그를 syslog로 옮겼다면 recidive에도 backend = systemd가 필요합니다.

실무에서는 이 jail 하나의 효과가 나머지 전부를 세밀히 조정하는 것보다 더 눈에 띄는 경우가 많습니다. 단골들이 떨어져 나가고 로그에는 신선한 배경 소음만 남습니다.

서버에 사이트가 있다면 또 무엇을 켤까

유용한 순서대로:

  • nginx-http-auth 또는 apache-auth — 기본 인증에 대한 비밀번호 추측. 관리 영역이나 staging 사이트가 웹 서버 비밀번호 뒤에 있다면 필요합니다;
  • nginx-botsearch — 알려진 경로 탐색: /wp-login.php, /phpmyadmin, /.env. 이건 침입이 아니라 정찰이며, 다른 모든 것에 앞섭니다;
  • nginx-limit-req — Nginx 자체에 limit_req_zone이 정의돼 있을 때만 작동합니다. 없으면 jail은 켜져 있어도 쓸모가 없습니다;
  • postfix-sasldovecot — 메일을 직접 운영한다면 필수입니다. 메일함에 대한 비밀번호 추측은 끊임없이 이루어지며 대개 아무도 보지 않습니다.
[nginx-botsearch]
enabled = true
logpath = /var/log/nginx/access.log

사이트 자체의 로그인 폼은 별개의 이야기입니다. 애플리케이션에는 실패한 로그인의 자체 로그가 없고, access.log에서 실패한 시도는 200이나 302를 반환하는 평범한 POST로 보입니다 — 어떤 범용 필터로도 성공한 것과 구분되지 않습니다. 길은 둘입니다. 애플리케이션이 실패를 syslog에 쓰게 하거나(대부분의 콘텐츠 관리 시스템에는 이를 위한 플러그인이 있습니다), 로그인 페이지로 향하는 요청의 빈도를 제한하거나. 두 번째는 탐지기가 아니라 제한기이며, 그것을 미리 아는 편이 낫습니다.

숫자들

bantime = 10m이 바로 사람들이 Fail2ban을 쓸모없다고 판단하게 만드는 기본값입니다. 10분이면 봇이 돌아오기에 충분합니다. 첫 차단에 한 시간, 거기에 일주일짜리 recidive를 더하는 편이 영구 차단보다 훨씬 잘 작동합니다. 영구 차단은 시간이 지나면 끝없이 긴 규칙 목록으로 변합니다.

SSH에 maxretry = 3은 스스로를 밖에 가두는 확실한 방법입니다. 10분에 다섯 번도 추측을 그만큼 잘 끊습니다.

느린 추측 — 5분에 한 번 — 은 결코 findtime 안에 들어오지 않습니다. 그렇다고 창을 하루로 늘릴 이유는 되지 않습니다. 자기 동료들에게 오탐 차단이 걸릴 뿐입니다. 느린 추측에 듣는 것은 임계값이 아니라 비밀번호 인증을 끄는 일입니다.

차단이 패킷까지 닿는지 확인하기

Fail2ban은 외부 명령을 부를 뿐입니다. banaction이 실제로 트래픽을 거르는 것과 맞지 않으면, 로그에는 신나는 Ban 198.51.100.7이 쌓이는 동안 패킷은 계속 도착합니다. Debian 12는 기본으로 nftables를 쓰고, UFW가 켜져 있다면 올바른 선택은 banaction = ufw입니다. 확인하려면:

sudo nft list ruleset | grep -c f2b
sudo iptables -S | grep f2b

둘 중 적어도 하나는 규칙을 보여 줘야 합니다. 필터 자체는 공격을 기다리지 않고도 시험할 수 있습니다.

sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf

출력 아래쪽에 몇 줄이 일치했는지 적혀 있습니다. 0이면 필터와 로그가 만나지 못한다는 뜻이며, 그 이상 설정을 다듬을 이유가 없습니다.

Fail2ban이 하지 못하는 것

분산 추측은 막지 못합니다. 한 번씩만 시도하는 천 개의 주소는 어떤 설정으로도 임계값에 닿지 않습니다. 원인을 없애지도 않습니다 — 차단된 주소가 약한 비밀번호를 되돌려 주지는 않으니까요. 그리고 평판에 대해서는 아무것도 모릅니다. 어제 남의 서버를 뚫는 데 하루를 보낸 주소도, 당신에게 오기 전까지는 깨끗합니다. 여기에서 실무의 조합이 나옵니다. 비밀번호 대신 키, 소음 제한기로서의 Fail2ban, 그리고 남의 경험이라는 지식으로서의 공유 차단 목록(CrowdSec).

또 하나의 문제는 이 모든 것을 가끔은 누군가 봐야 한다는 점입니다. 몇 주씩 jail마다 fail2ban-client status를 치는 사람은 없고, 늘어나는 차단 건수는 무언가 이미 망가진 뒤에야 눈에 띕니다. 아래 데모 페이지에서는 같은 데이터가 한 페이지에 있습니다. jail 목록, 지금 누가 차단돼 있는지, 그리고 시도가 어디에서 오는지.