참조: fail2ban 공식 GitHub · Proper fail2ban configuration (공식 Wiki)
1. 왜 fail2ban을 도입했는가
이 블로그 서버는 인터넷에 공개되어 있어 지속적으로 다양한 자동화 스캐닝 공격에 노출됩니다. 기존에는 문제가 있는 IP를 발견할 때마다 nginx 설정에 deny 지시자를 수동으로 추가하고 있었습니다.
# 기존 방식 — 수동으로 IP를 하나씩 추가해야 했음
location / {
deny 82.165.66.87;
deny 20.151.201.236;
...
}
이 방식은 다음과 같은 문제가 있었습니다.
- 반응이 느림: 공격을 발견하고, 로그를 확인하고, 직접 추가해야 함
- 유지보수 부담: 차단 IP가 늘어날수록 nginx 설정이 지저분해짐
- 해제 불가: 차단 기간 개념 없이 영구 차단만 가능
fail2ban은 이 과정을 자동화합니다. 로그를 실시간으로 감시하다가 일정 횟수 이상 공격 패턴이 감지되면 방화벽(iptables/nftables) 레벨에서 자동으로 IP를 차단하고, 설정한 시간이 지나면 자동으로 해제합니다.
2. fail2ban이란
“Fail2Ban scans log files like
/var/log/auth.logand bans IP addresses conducting too many failed login attempts.” — fail2ban GitHub README
fail2ban은 Python으로 작성된 보안 데몬입니다. 동작 원리는 다음과 같습니다.
로그 파일 감시 → 패턴 매칭(regex) → 임계값 초과 시 방화벽 규칙 추가 → 시간 경과 후 자동 해제
주요 특징:
- SSH, nginx, Apache 등 어떤 서비스의 로그도 감시 가능
- iptables, nftables, ufw 등 다양한 방화벽 백엔드 지원
- 차단 시간(
bantime)이 지나면 자동으로 차단 해제 - Python 3.5 이상 필요, 현재 stable 버전: 1.1.0 (2024년 4월 릴리즈)
nginx deny vs fail2ban 차단의 차이
nginx deny는 nginx 프로세스가 요청을 받은 뒤 거절하는 방식입니다. fail2ban은 iptables/nftables 레벨, 즉 nginx에 도달하기 전 커널 단에서 차단하므로 더 효율적입니다.
3. 핵심 개념 — Filter · Jail · Action
fail2ban은 세 가지 핵심 요소로 구성됩니다.
Filter (필터)
로그에서 공격 패턴을 인식하는 정규표현식 규칙입니다. /etc/fail2ban/filter.d/ 디렉토리에 .conf 파일로 저장합니다.
[Definition]
failregex = ^<HOST> .* "POST /api/auth/login HTTP/[^"]+" 429
<HOST>: fail2ban이 자동으로 IP 주소를 캡처하는 특수 토큰failregex: 이 패턴에 매칭되면 "실패 시도 1회"로 카운트
Jail (잠금)
“어떤 로그를, 어떤 필터로, 얼마나 보고, 어떻게 차단할지” 를 정의하는 단위입니다. /etc/fail2ban/jail.d/ 디렉토리에 .conf 파일로 저장합니다.
| 지시자 | 의미 | 예시 |
|---|---|---|
enabled |
jail 활성화 여부 | true |
filter |
사용할 filter 이름 | nginx-login-bruteforce |
logpath |
감시할 로그 파일 경로 | /var/log/nginx/hgkimer.me.access.log |
maxretry |
차단까지 허용하는 실패 횟수 | 5 |
findtime |
실패 횟수를 카운트하는 시간 창(초) | 60 |
bantime |
차단 지속 시간(초) | 3600 |
ignoreip |
절대 차단하지 않을 IP 목록 | 127.0.0.1/8 ::1 <본인IP> |
예시: maxretry=5, findtime=60 이면 "60초 이내에 5번 실패하면 차단"을 의미합니다.
Action (액션)
차단이 결정되었을 때 실제로 무엇을 할지 정의합니다. 기본값은 iptables/nftables로 해당 IP의 연결을 차단하는 것이며, 별도 설정이 없으면 jail.local의 banaction 기본값을 사용합니다. 이 프로젝트에서는 기본 액션(iptables 차단)을 그대로 사용합니다.
4. 설치
Debian/Ubuntu 기준으로 설명합니다.
# 패키지 설치
sudo apt update
sudo apt install -y fail2ban
설치 후 /etc/fail2ban/ 디렉토리가 생성됩니다.
/etc/fail2ban/
├── fail2ban.conf # fail2ban 데몬 설정
├── jail.conf # 기본 jail 설정 (수정 금지)
├── jail.local # 사용자 jail 설정 (여기에 작성)
├── filter.d/ # 필터 파일 모음
└── jail.d/ # jail 파일 모음 (여기에 작성 권장)
중요: 공식 문서는
.conf파일을 직접 수정하지 말고.local파일을 만들어 덮어쓸 것을 강력히 권장합니다. 패키지 업그레이드 시.conf파일은 덮어쓰여지지만.local파일은 보존됩니다.
# jail.conf를 직접 수정하지 않고 복사본을 생성
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
# 서비스 활성화 및 시작
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
5. 설정 파일 구조 이해
fail2ban 설정은 INI 파일 형식을 사용하며, [섹션명] 단위로 구분됩니다.
[DEFAULT] # 모든 jail에 공통 적용되는 기본값
ignoreip = 127.0.0.1/8
[jail-name] # 개별 jail 설정 (DEFAULT를 덮어씀)
enabled = true
filter = my-filter
logpath = /var/log/myapp.log
maxretry = 5
[DEFAULT] 섹션에 설정한 값은 모든 jail의 기본값이 됩니다. 개별 jail에서 같은 키를 설정하면 [DEFAULT] 값을 덮어씁니다.
6. 이 프로젝트에서의 적용
6.1 nginx 로그 설정
fail2ban이 로그를 읽으려면 nginx가 로그를 명시적인 경로에 기록해야 합니다. 기존 nginx/hgkimer.me.conf에는 access_log 지시자가 없어 nginx 기본 로그(/var/log/nginx/access.log)에만 기록되고 있었습니다. 사이트별로 구분된 로그 파일을 갖도록 명시적으로 경로를 지정했습니다.
# nginx/hgkimer.me.conf
server {
server_name hgkimer.me www.hgkimer.me;
access_log /var/log/nginx/hgkimer.me.access.log combined;
error_log /var/log/nginx/hgkimer.me.error.log warn;
...
}
combined는 nginx의 기본 로그 포맷으로, 한 줄의 형태가 다음과 같습니다.
1.2.3.4 - - [17/Apr/2026:12:00:00 +0900] "POST /api/auth/login HTTP/1.1" 429 162 "-" "curl/7.88"
fail2ban의 <HOST> 토큰은 이 형식에서 맨 앞의 IP(1.2.3.4)를 자동으로 추출합니다.
6.2 Filter 작성
Filter 1 — 로그인 무차별 대입 공격 감지
파일: fail2ban/filter.d/nginx-login-bruteforce.conf
[Definition]
failregex = ^<HOST> .* "POST /api/auth/login HTTP/[^"]+" 429
^<HOST> .* "POST /admin/login HTTP/[^"]+" 429
ignoreregex =
이 필터가 필요한 이유:
저는 이미 nginx 설정에서 로그인 경로에 limit_req(rate limiting)가 적용되어 있어, 단시간에 요청이 몰리면 429(Too Many Requests)를 반환하도록 해놓았습니다.
# nginx/hgkimer.me.conf — 이미 적용된 rate limiting
location = /api/auth/login {
limit_req zone=login_limit burst=10 nodelay;
limit_req_status 429;
...
}
429 응답이 발생했다는 것은 rate limit을 초과한 공격 시도가 있었다는 의미입니다. 이 패턴을 감지해 IP를 차단합니다.
^<HOST>: 로그 라인 시작의 IP 주소 캡처HTTP/[^"]+: HTTP 버전(1.0, 1.1, 2.0 등)을 유연하게 매칭429: rate limit 초과 응답 코드
Filter 2 — 악성 URI 스캐닝 감지
파일: fail2ban/filter.d/nginx-bad-uri.conf
[Definition]
failregex = ^<HOST> .* ".*" 444
ignoreregex =
이 필터가 필요한 이유:
기존에 제 서버에는 nginx/bad_uri.conf에는 WordPress 공격, 환경설정 파일 탐색, Log4j 취약점 시도 등 수십 가지 악성 URI 패턴이 정의하여 include 해 놓았습니다.
# nginx/bad_uri.conf — 악성 URI 패턴 정의 (일부)
map $request_uri $bad_word {
~*(\.php|wp-includes|wp-login|wp-admin) 1;
~*(\.env|\.git|DS_Store) 1;
~*(ldap|jndi|dns) 1; # Log4j
...
}
이 패턴에 매칭된 요청은 nginx가 444(연결 즉시 종료)를 반환합니다.
# nginx/hgkimer.me.conf
if ($bad_word) {
return 444;
}
444 응답은 명백한 악의적 스캐닝을 의미하므로, 단 3회만 탐지되어도 24시간 차단합니다.
6.3 Jail 설정
파일: fail2ban/jail.d/nginx-hgkimer.conf
[DEFAULT]
# 자기 차단 방지 — 본인 공인 IP 반드시 추가
ignoreip = 127.0.0.1/8 ::1 <본인_공인IP>
[nginx-login-bruteforce]
enabled = true
filter = nginx-login-bruteforce
logpath = /var/log/nginx/hgkimer.me.access.log
maxretry = 5
findtime = 60
bantime = 3600
[nginx-bad-uri]
enabled = true
filter = nginx-bad-uri
logpath = /var/log/nginx/hgkimer.me.access.log
maxretry = 3
findtime = 60
bantime = 86400
설계 의도:
| Jail | maxretry | findtime | bantime | 이유 |
|---|---|---|---|---|
nginx-login-bruteforce |
5회 | 60초 | 1시간 | 실수로 비밀번호를 틀릴 수 있어 여유를 둠 |
nginx-bad-uri |
3회 | 60초 | 24시간 | 악성 스캐닝은 정상 사용자가 발생시킬 수 없음 |
ignoreip를 반드시 설정해야 하는 이유:
관리자가 잘못된 비밀번호를 여러 번 입력하거나, 브라우저 캐시 등으로 인해 본인 IP가 차단될 수 있습니다. 차단되면 서버에 접속 자체가 불가능해지므로, ignoreip에 본인 공인 IP를 반드시 추가해야 합니다.
7. 서버 적용 순서
# 1. fail2ban 설치
sudo apt update && sudo apt install -y fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
# 2. filter/jail 파일 복사 (레포 루트에서 실행)
sudo cp fail2ban/filter.d/nginx-login-bruteforce.conf /etc/fail2ban/filter.d/
sudo cp fail2ban/filter.d/nginx-bad-uri.conf /etc/fail2ban/filter.d/
sudo cp fail2ban/jail.d/nginx-hgkimer.conf /etc/fail2ban/jail.d/
# 3. jail.d 파일에서 본인 IP 교체
sudo nano /etc/fail2ban/jail.d/nginx-hgkimer.conf
# <본인_공인IP> → 실제 공인 IP로 수정
# 4. nginx 설정 업데이트 및 재로드
sudo cp nginx/hgkimer.me.conf /etc/nginx/sites-available/hgkimer.me
sudo nginx -t && sudo systemctl reload nginx
# 5. fail2ban 시작
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
8. 동작 확인
# 전체 jail 목록 및 상태 확인
sudo fail2ban-client status
# 개별 jail 상태 확인 (차단된 IP 목록 포함)
sudo fail2ban-client status nginx-login-bruteforce
sudo fail2ban-client status nginx-bad-uri
# filter 정규표현식이 로그를 올바르게 파싱하는지 테스트
sudo fail2ban-regex /var/log/nginx/hgkimer.me.access.log \
/etc/fail2ban/filter.d/nginx-login-bruteforce.conf
sudo fail2ban-regex /var/log/nginx/hgkimer.me.access.log \
/etc/fail2ban/filter.d/nginx-bad-uri.conf
# 특정 IP를 수동으로 차단 해제
sudo fail2ban-client set nginx-bad-uri unbanip <IP주소>
# fail2ban 설정 재로드 (서비스 재시작 없이)
sudo fail2ban-client reload
fail2ban-regex 명령의 출력 마지막에 Lines: N matched, M missed 형태로 결과가 나옵니다.
실제 로그파일에 우리가 설정한 조건에 맞는 접속 로그가 있는 상태에서 matched 수가 0이 아니면 필터가 로그를 정상적으로 인식하는 것입니다.