본문 바로가기
서버 인프라 실무

Fail2ban 고급편 + UFW 튜닝, 그리고 Nginx/HAProxy와 SSH 터널의 역할 분리 전략

by yamoojin83 2025. 10. 26.

Fail2ban 고급편 + UFW 튜닝, 그리고 Nginx/HAProxy와 SSH 터널의 역할 분리 전략

이전 글에서 Fail2ban + UFW로 SSH 브루트포스를 막는 기본 구성을 만들었습니다. 이번에는 공격 강도에 따라 차단 시간이 늘어나는 증가형 정책, 화이트/블랙 리스트 운영, GeoIP 차단, Webhook 알림까지 확장하고, HTTP 계층(nginx/haproxy)에서 레이트 리밋과 4xx/5xx 연동으로 애플리케이션 보호까지 완성합니다. 마지막으로 SSH 터널프록시(nginx/haproxy)의 역할을 명확히 분리하는 운영 규칙을 제시합니다.


1) 증가형 차단 정책으로 “같은 IP 반복 공격” 제압

한 번 막아도 같은 IP가 계속 두드리면 차단 시간을 점점 늘리는 게 효과적입니다. Fail2ban 0.11+는 bantime.increment를 기본 지원합니다.

# /etc/fail2ban/jail.local (발췌)
[DEFAULT]
findtime  = 10m
maxretry  = 5
banaction = ufw

# 증가형 bantime
bantime           = 1h
bantime.increment = true
bantime.factor    = 2       # 1h → 2h → 4h ...
bantime.maxtime   = 24h
bantime.overalljails = true # 모든 jail 누적 실패 반영

[sshd]
enabled = true
backend = systemd

bantime.overalljails는 sshd뿐 아니라 nginx, postfix 등 다른 jail에서의 실패까지 누적해 집요한 공격자를 신속히 격리합니다.


2) 화이트리스트/블랙리스트: 실수 차단 방지 + 고의 공격 신속 격리

관리망/배스천 IP는 화이트리스트, 악성 대역은 블랙리스트로 처리합니다.

# /etc/fail2ban/jail.local (발췌)
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 10.10.0.0/16 203.0.113.0/24
# 상시 블랙리스트(커스텀 action)
sudo tee /etc/fail2ban/action.d/ufw-blacklist.conf >/dev/null <<'AC'
[Definition]
actionstart = /bin/true
actionstop  = /bin/true
actioncheck = /bin/true
actionban   = /usr/sbin/ufw insert 1 deny from <ip> to any
actionunban = /usr/sbin/ufw delete deny from <ip> to any
AC
# 임계치 초과한 IP를 즉시 블록 (필요시)
sudo fail2ban-client set sshd banip 198.51.100.23

3) GeoIP 기반 차단(선택)

해외에서만 오는 공격이 심하면 GeoIP 차단을 고려할 수 있습니다. Fail2ban의 geoip 필터/액션 플러그인을 쓰거나, UFW/iptables 앞단에 nginx geoip2 모듈을 둘 수 있습니다. 운영 난이도가 있으니 신중하게 적용하고, 합법적 해외 사용자 흐름은 예외 처리하세요.

# nginx (geoip2 모듈 설치 필요) 예시 스니펫
geoip2 /etc/nginx/geoip/GeoLite2-Country.mmdb {
  $geoip2_country_code country iso_code;
}
map $geoip2_country_code $allowed_country {
  default 0;
  KR 1; JP 1;
}
server {
  if ($allowed_country = 0) { return 403; }
  ...
}

4) Slack/Webhook 알림: 차단 사실을 즉시 공유

메일보다 메신저가 빠른 팀은 Webhook 알림을 선호합니다. 앞선 기본편에서 만든 slack-notify.conf를 고도화해 whois, 역방향 DNS까지 포함할 수 있습니다.

# /etc/fail2ban/action.d/slack-notify-rich.conf
[Definition]
actionban = /bin/bash -lc '\
  WHO=$(/usr/bin/whois <ip> | sed -n "1,40p" | sed "s/\"/'\''/g"); \
  RDNS=$(getent hosts <ip> | awk "{print \\$2}" | head -1); \
  /usr/bin/curl -X POST -H "Content-type: application/json" \
  --data "{\"text\":\"BANNED <ip> (<name>), rdns: ${RDNS:-n/a}\n\`\`\`${WHO}\`\`\`\"}" <WEBHOOK>'
# jail.local
[DEFAULT]
action = slack-notify-rich[WEBHOOK=https://hooks.slack.com/services/XXX/YYY/ZZZ]

5) HTTP 계층 보호: nginx 레이트 리밋 + Fail2ban 연동

봇이 로그인/가입/검색을 과도하게 두드리는 경우, HTTP 계층에서 먼저 속도를 제한하는 것이 자원 보전에 효과적입니다. nginx의 limit_req로 1차 제어, 429/401/403/404 패턴을 Fail2ban으로 학습시켜 2차 차단을 겁니다.

5-1) nginx 레이트 리밋

# /etc/nginx/conf.d/ratelimit.conf
limit_req_zone $binary_remote_addr zone=api:10m rate=5r/s;   # 초당 5건

server {
  location /api/ {
    limit_req zone=api burst=20 nodelay; # 버스트 20
    proxy_pass http://127.0.0.1:8080;
  }
}

5-2) 429/403 과다 발생 IP 차단(Fail2ban)

# /etc/fail2ban/filter.d/nginx-ratelimit.conf
[Definition]
failregex = ^<HOST> - .* ".*" .* 429 .*$
            ^<HOST> - .* ".*" .* 403 .*$
ignoreregex =
# /etc/fail2ban/jail.local (추가)
[nginx-ratelimit]
enabled = true
port    = http,https
logpath = /var/log/nginx/access.log
backend = auto
maxretry = 50
findtime = 5m
bantime  = 2h

API를 무차별로 때리는 IP는 먼저 429(과다 요청)로 누르되, 지속 시 Fail2ban이 장기 차단합니다. 정상 사용자의 일시 버스트를 고려해 burstmaxretry로그 기반으로 튜닝하세요.


6) HAProxy 환경: 스틱 테이블 + HTTP 에러 기반 봉쇄

HAProxy는 스틱 테이블로 IP별 요청 수/오류 수를 추적해 즉시 차단(deny)하거나, 로그를 Fail2ban과 연동할 수 있습니다.

# haproxy.cfg (발췌)
frontend fe_http
  bind :80
  stick-table type ip size 200k expire 10m store http_req_rate(10s),http_err_rate(10s)
  http-request track-sc0 src
  acl abuse  sc_http_req_rate(0) gt 50
  acl bad   sc_http_err_rate(0) gt 20
  http-request deny if abuse or bad

위처럼 L7에서 감지해 즉시 deny하고, 추가로 syslog → Fail2ban 경로를 연결하면 재범 IP는 장기 차단이 가능합니다.


7) SSH 터널과 프록시의 역할 분리

운영에서 흔한 실수는 SSH 리버스 터널로 서비스 포트를 외부에 그대로 노출하는 것입니다. 보안/감사/레이트 리밋이 사라지므로, 원칙을 다음처럼 둡니다.

  • 원칙 1: 외부 노출은 오직 프록시 계층(nginx/haproxy)에서만. 방화벽/인증/레이트리밋/로깅의 단일 관문.
  • 원칙 2: SSH 터널(-L/-R/-D)은 운영자 일시 접근 또는 내부 간 통신에만 사용.
  • 원칙 3: 서버측 sshd_configAllowTcpForwarding local, GatewayPorts no, PermitOpen host:port화이트리스트 운영.
  • 원칙 4: 터널이 장기 필요하면 systemd 서비스로 등록하고, 만료/점검 스케줄을 걸어 존재 이유를 주기적으로 검토.

8) UFW 튜닝: 서비스별 rate-limit, 로깅 레벨

UFW는 간단한 rate-limit 규칙을 제공합니다(주로 SSH에 유용).

# SSH rate-limit (분당 연결 시도 제한)
sudo ufw limit OpenSSH

# 서비스 포트 정책 예시
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny  8080/tcp    # 백엔드는 내부에서만 열기

# 로깅 레벨(저/중/높음)
sudo ufw logging medium

8080 같은 백엔드 포트는 deny로 막고, 프록시가 있는 80/443만 allow로 노출합니다. 이 구조가 되어야 nginx/haproxy의 레이트 리밋·보안 헤더·TLS 정책이 강제됩니다.


9) 운영 점검 루틴

# Fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
zgrep -a "Ban " /var/log/fail2ban.log* | tail -50

# nginx
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log

# UFW
sudo ufw status numbered

# 포트 노출 확인
ss -lntup | grep -E ':22|:80|:443|:8080'

10) 흔한 함정과 해결

  • 정상 사용자가 자주 막힘: maxretry/findtime를 완화하고, 사내 NAT(여러 사용자 한 IP) 환경을 고려해 특정 경로는 화이트리스트.
  • 로그 형식 불일치로 필터 미동작: nginx 로그 포맷이 기본과 다르면 failregex를 맞춰 수정.
  • SSH 터널이 외부로 퍼짐: GatewayPorts yes가 아닌지 확인, 반드시 no. 필요한 경우에도 127.0.0.1 바인딩.
  • 백엔드 포트가 외부에서 열림: UFW에서 8080 deny, 보안그룹/클라우드 방화벽도 같이 점검.

11) 최종 체크리스트

  • Fail2ban 증가형 차단 활성(bantime.increment) + 공용 누적(overalljails)
  • 관리망 화이트리스트와 상시 블랙리스트 분리
  • nginx/haproxy에서 레이트 리밋 적용, 429/403 과다 IP는 Fail2ban으로 장기 차단
  • UFW: 80/443만 외부 허용, 백엔드 포트는 내부 전용
  • SSH 터널은 일시 접근용, 외부 노출은 프록시만 담당
  • 알림(메일/Slack)과 주간 리포트로 이상행위 가시화

이 구성으로 L3(방화벽) → L4/L7(프록시) → 애플리케이션까지 단계별 방어망이 갖춰집니다. 다음 글에서는 Let’s Encrypt 자동갱신의 안정화(이중 인증서, 갱신 모니터링)다운타임 없는 인증서 교체 전략을 이어서 다루겠습니다.