서버 로그에서 비정상 접속 IP 자동 차단 스크립트

Views: 0

배경과 목표

운영 서버는 SSH 무차별 대입, 관리자 경로 스캐닝, 짧은 시간의 폭발적 요청 등 다양한 공격에 일상적으로 노출된다. 상용 툴 없이도 간단한 스크립트와 방화벽 규칙으로 자동 차단 체계를 구축할 수 있다. 목표는 재부팅 없이 즉시 반영되고, 오탐을 줄이며, 차단 이력을 추적·해제할 수 있는 구조를 만드는 것이다. 개인 개발 환경과 기업 운영 환경 모두를 고려해 설계한다.

전체 설계 개요

  1. 로그 소스 결정
  2. 공격 패턴과 시간창(Window)·횟수 기반 임계치 정의
  3. 화이트리스트와 포트·서비스 예외 처리
  4. iptables 또는 nftables로 차단·해제 추상화
  5. 상태 저장과 만료 정책으로 자동 언블록
  6. systemd 타이머로 주기 실행 또는 데몬화
  7. 관제·감사 로그와 롤백(복구) 절차 마련

로그 소스와 대표 패턴

SSH 공격은 Debian/Ubuntu의 /var/log/auth.log, RHEL/CentOS의 /var/log/secure에서 “Failed password”, “Invalid user”, “authentication failure” 패턴으로 포착된다. 웹 서비스는 Nginx/Apache 접근 로그에서 401/403/404 급증, 관리자 경로(/wp-admin, /phpmyadmin, /.git/) 요청, 비정상 User-Agent, 동일 IP의 과도한 요청을 관찰한다. systemd 환경이라면 journalctl -u sshd --since '1 hour ago'로 시간 범위를 손쉽게 제한할 수 있어 비용이 낮다.

임계치와 시간창 설계

임계치는 시간창과 실패 횟수의 조합으로 정의한다. SSH는 1시간 내 실패 10~20회, 웹은 5분 내 300회 수준처럼 서비스 특성에 맞춘다. 운영 초기에는 모니터링 모드로 일주일 정도 돌려 오탐·미탐을 확인하고, 이후 실제 차단을 활성화한다. 재발 방지를 위해 1회 위반은 1시간 차단, 재발 시 24시간처럼 가중 만료를 둘 수도 있다.

화이트리스트와 예외

사내 고정 IP, VPN 게이트웨이, 배포 파이프라인, 모니터링 노드는 반드시 화이트리스트로 제외한다. 파일 권한은 600으로 제한하고, CIDR 대역 지원이 필요하면 줄 단위로 10.0.0.0/24처럼 기록한다. 특정 포트만 관리 접근이라면 포트 기반 예외를 두어 차단 범위를 최소화한다.

iptables와 nftables 선택

여전히 많은 환경에서 iptables가 사용되지만, 최신 배포판은 nftables를 권장한다. 혼재 환경을 지원하려면 스크립트에서 두 백엔드를 자동 감지해 분기한다. iptables는 룰 순서가 중요해 INPUT 체인의 상단에 DROP을 삽입하고, nftables는 set을 사용해 성능과 관리성을 높인다. IPv6를 병행 운영한다면 v4·v6를 각각 처리하거나 공통 래퍼를 둔다.

초기 nftables 준비 스니펫

# 테이블·체인·세트가 없을 때만 생성
nft list tables | grep -q "inet filter" || nft add table inet filter
nft list chains inet filter | grep -q "input" || nft add chain inet filter input { type filter hook input priority 0; }
nft list sets inet filter | grep -q "blocked_ips" || nft add set inet filter blocked_ips { type ipv4_addr; flags interval; }
nft list sets inet filter | grep -q "blocked_ips6" || nft add set inet filter blocked_ips6 { type ipv6_addr; flags interval; }
nft list ruleset | grep -q "@blocked_ips"  || nft add rule inet filter input  ip  s @blocked_ips  drop
nft list ruleset | grep -q "@blocked_ips6" || nft add rule inet filter input ip6 s @blocked_ips6 drop

핵심 스크립트: SSH 실패 기반 자동 차단

아래 스크립트는 최근 1시간 동안 SSH 로그인 실패가 임계치 이상인 IP를 차단한다. iptables/nftables를 자동 감지하고, 차단 현황과 만료를 파일 DB로 관리한다. /usr/local/bin/auto_block.sh로 저장 후 실행권한을 준다.

#!/usr/bin/env bash
set -euo pipefail

# 기본 설정(환경변수로 덮어쓰기 가능)
LOG="${LOG:-/var/log/auth.log}"
STATE_DIR="${STATE_DIR:-/var/lib/auto_block}"
DB="$STATE_DIR/blocked.tsv"         # ip<TAB>blocked_at<TAB>until<TAB>reason
WL="${WL:-/etc/auto_block_whitelist}"
THRESHOLD="${THRESHOLD:-10}"        # 임계 실패 횟수
WINDOW_SEC="${WINDOW_SEC:-3600}"    # 1시간
BAN_SEC="${BAN_SEC:-86400}"         # 24시간
MODE="${MODE:-enforce}"             # monitor | enforce

mkdir -p "$STATE_DIR"; touch "$DB"; [ -f "$WL" ] || touch "$WL"

has_cmd(){ command -v "$1" >/dev/null 2>&1; }

block_ip(){
  local ip="$1"
  if has_cmd nft; then
    nft add element inet filter blocked_ips  { $ip } 2>/dev/null || true
    [[ "$ip" =~ : ]] && nft add element inet filter blocked_ips6 { $ip } 2>/dev/null || true
  else
    iptables -C INPUT -s "$ip" -j DROP 2>/dev/null || iptables -I INPUT -s "$ip" -j DROP
  fi
}

unblock_ip(){
  local ip="$1"
  if has_cmd nft; then
    nft delete element inet filter blocked_ips  { $ip } 2>/dev/null || true
    nft delete element inet filter blocked_ips6 { $ip } 2>/dev/null || true
  else
    iptables -D INPUT -s "$ip" -j DROP 2>/dev/null || true
  fi
}

is_whitelisted(){
  local ip="$1"
  # 정밀 일치 또는 CIDR 포함 여부 검사
  if grep -qx "$ip" "$WL"; then return 0; fi
  # CIDR 지원(간단 검증): ipcalc/grepcidr 사용 가능 시 활용
  if has_cmd grepcidr; then grepcidr -q -f "$WL" <<<"$ip" && return 0; fi
  return 1
}

now_ts(){ date +%s; }

cleanup_expired(){
  local now; now=$(now_ts)
  awk -F'\t' -v now="$now" 'NF>=3 && ($3=="" || $3>now){print $0}' "$DB" > "$DB.tmp" && mv "$DB.tmp" "$DB"
}

scan_candidates(){
  # systemd 환경이면 journalctl이 훨씬 효율적
  if has_cmd journalctl; then
    journalctl -u sshd --since "@$(( $(now_ts) - WINDOW_SEC ))" 2>/dev/null | grep -E "Failed password|Invalid user|authentication failure" || true
  else
    # tail 범위는 트래픽에 맞춰 조정
    tail -n 20000 "$LOG" | grep -E "Failed password|Invalid user|authentication failure" || true
  fi
}

extract_ip(){
  # 한 줄의 로그에서 IPv4/IPv6 중 하나 추출
  local line="$1"
  if [[ "$line" =~ ([0-9]{1,3}\.){3}[0-9]{1,3} ]]; then
    grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" <<<"$line" | head -n1
  else
    grep -oE "([0-9a-fA-F:]{2,})" <<<"$line" | head -n1
  fi
}

scan_and_block(){
  declare -A count
  while IFS= read -r ln; do
    ip="$(extract_ip "$ln" || true)"
    [[ -z "${ip:-}" ]] && continue
    ((count["$ip"]++)) || true
  done < <(scan_candidates)

  for ip in "${!count[@]}"; do
    # 화이트리스트·임계치 확인
    is_whitelisted "$ip" && continue
    [[ ${count[$ip]} -lt $THRESHOLD ]] && continue

    if [[ "$MODE" == "monitor" ]]; then
      logger -t auto_block "MONITOR would block $ip after ${count[$ip]} fails"
      continue
    fi

    # 이미 DB에 있으면 스킵
    if ! grep -q "^$ip\t" "$DB"; then
      block_ip "$ip"
      ts=$(now_ts); until=$(( ts + BAN_SEC ))
      printf "%s\t%d\t%d\t%s\n" "$ip" "$ts" "$until" "sshd" >> "$DB"
      logger -t auto_block "Blocked $ip after ${count[$ip]} fails; until=$until"
    fi
  done
}

case "${1:-run}" in
  run)      cleanup_expired; scan_and_block ;;
  unblock)  ip="${2:-}"; [[ -n "$ip" ]]; unblock_ip "$ip"; grep -v "^$ip\t" "$DB" > "$DB.tmp" && mv "$DB.tmp" "$DB" ;;
  list)     cat "$DB" ;;
esac

포인트는 세 가지다. 첫째, journalctl --since 사용으로 증분 범위만 스캔해 비용을 낮춘다. 둘째, v4·v6를 모두 고려해 IP를 추출한다. 셋째, DB 파일에 차단·만료 시각을 함께 남겨 주기 실행 시 자동 정리한다.

웹 로그 기반 확장

Nginx에서 5분 내 401/403/404 응답이 많은 IP를 차단하는 간단한 예시는 다음과 같다. 리버스 프록시 뒤라면 실제 클라이언트 IP가 기록되도록 real_ip_recursive onset_real_ip_from 설정을 먼저 점검한다.

WINDOW_MIN=5
LIMIT=300
since="$(date -d "-${WINDOW_MIN} min" +"%d/%b/%Y:%H:%M")"
awk -v s="$since" '$4 ~ s && ($9 ~ /^(401|403|404)$/){print $1}' /var/log/nginx/access.log \
| sort | uniq -c | awk -v L="$LIMIT" '$1>=L{print $2}' | while read -r ip; do
  /usr/local/bin/auto_block.sh run >/dev/null 2>&1 # 상태 정리
  # 여기서는 block_ip만 호출하는 별도 래퍼를 두어도 된다
done

systemd로 자동화

서비스와 타이머를 구성해 5분마다 실행한다.

# /etc/systemd/system/auto_block.service
[Unit]
Description=Auto block abnormal IPs
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/auto_block.sh run
# /etc/systemd/system/auto_block.timer
[Unit]
Description=Run auto_block every 5 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min

[Install]
WantedBy=timers.target

활성화는 다음 순서로 진행한다.

systemctl daemon-reload
systemctl enable --now auto_block.timer

영속화와 재부팅 대응

iptables 사용 시 현재 룰을 저장해 부팅 후 복원한다.

iptables-save > /etc/iptables/rules.v4

nftables는 /etc/nftables.conf에 규칙을 반영하고 부팅 시 자동 로드한다. 차단 DB(blocked.tsv)는 /var/lib/auto_block 아래에 두어 로그 로테이션과 충돌하지 않도록 분리한다.

관제·감사 로깅과 알림

logger -t auto_block를 통해 syslog로 차단·해제 이벤트를 남기면 중앙 로그 수집기에서 추적할 수 있다. 알림이 필요하면 메일 전송 혹은 웹훅 호출을 스크립트에 추가해 운영팀 채널로 통보한다. 감사성을 위해 IP, 발생 카운트, 최초·마지막 탐지 시각, 만료 시각을 한 줄에 기록한다.

대용량·컨테이너 환경 고려

초고빈도 환경에서는 마지막 처리 오프셋을 기록해 증분 파싱하고, awk|sort|uniq -c 대신 gawk 카운팅이나 파이프라인 최적화를 통해 CPU 사용률을 낮춘다. 컨테이너 내부 차단은 네임스페이스 제약이 있으므로 노드 방화벽이나 eBPF 기반 CNI에서 처리하는 편이 일관적이다. 인그레스·로드밸런서 레이트 리미팅과 본 스크립트를 함께 사용하는 계층적 방어가 효과적이다.

검증·롤백 절차

운영 반영 전에는 모니터링 모드로 3~7일 가동해 오탐을 점검한다. 실제 차단 전에는 화이트리스트를 우선 정비하고, 차단 전후 방화벽 스냅샷을 저장해 긴급 복구 시간을 줄인다. 언블록 서브커맨드로 즉시 복귀가 가능하도록 하고, 업무 시간 외 점진적 확대 적용을 권장한다.

마무리

로그 파싱과 임계치 로직, 화이트리스트, 방화벽 추상화, 만료·해제, 자동화, 관제까지 갖추면 fail2ban 없이도 충분히 견고한 자동 차단 체계를 운영할 수 있다. 서비스 특성에 맞춰 시간창과 횟수, 만료 시간을 주기적으로 재평가하면 오탐을 줄이면서 방어력을 높일 수 있다. 위 스크립트와 절차를 기반으로 각 환경에 맞게 패턴과 정책을 조정해 즉시 적용해보자.