Profile picture

[Linux] Fail2Ban으로 경로 기반 스캐닝 대처하는 방법

JaehyoJJAng2025년 02월 20일

개요

운영 중인 웹 서버의 서비스 로그를 확인하다가

다음과 같은 경로를 기반으로 API를 요청하는 로그를 확인할 수 있었습니다.
image


이를 통해 누군가의 악의적인 공격 시도라고 판단하여 이를 막기위한 과정을 글로써 기록해보려고 합니다.

어떤 공격인가요?

이와 유사한 사례들을 검색하면서 디렉토리 횡단 공격(Directory Traversal) 이라는 공격 기법을 알게 되었다.


Directory Traversal

웹 애플리케이션에서 사용자가 원래 접근해서는 안 되는 서버의 디렉터리나 파일에 접근하려고 시도하는 공격 기법입니다.


공격자는 URL이나 입력값에 ../ 같은 경로 이동 문자를 삽입하여 상위 디렉토리로 이동하고, 서버의 민감한 파일에 접근하려고 합니다.


하지만 로그를 통해 감지된 공격은 Directory Traversal처럼 상위 디렉토리나 하위 디렉토리로 이동하려는 시도처럼 보이지는 않았습니다.


대신 .env, .env-backup.tar.gz등 존재 가능성이 높아보이는 민감한 파일명을 무차별적으로 대입해 요청하는 방식처럼 보였습니다.


이러한 공격의 특징은 공격자는 주로 nikto 등의 공격 도구를 사용하여 수천 개의 경로를 탐색하며,

응답 코드(200, 403, 404)나 응답 시간 등을 통해 파일 존재 여부를 파악하는 방식으로 동작합니다.


의도치 않게 백업 파일이 노출될 경우 심각한 보안 위협으로 이어질 수 있기 때문에

웹 서버 차원에서 선제적으로 차단해주는 것이 매우 효과적인 대응책이라고 할 수 있습니다.


Fail2Ban

Fail2ban이란 서버의 로그 파일을 실시간으로 모니터링하여 비정상적인 접근(예: 무차별 암호 대입, 웹 스캐닝)을 시도하는 IP를 감지하고,

iptables와 같은 방화벽 도구와 연동하여 해당 IP를 자동으로 차단하는 리눅스 기반의 침입 방지 소프트웨어(IPS)입니다.


핵심 동작 원리는 간단합니다.

  • 1. 로그 모니터링: 지정된 로그 파일(sshd, nginx 등)을 계속 감시합니다.
  • 2. 패턴 감지: 미리 정의된 정규식(Filter)과 일치하는 비정상 로그가 감지되면 싪패 횟수를 카운트합니다.
  • 3. IP 차단: 정해진 시간(findtime)내에 실패 횟수(maxretry)를 초과하면, 설정된 동작(Action)에 따라 해당 IP를 일정 시간(bantime)동안 차단합니다.

이러한 특징 덕분에 별도의 에이전트 없이 가볍게 동작하며, 다양한 서비스에 플러그인 처럼 적용하여 DoS, 디렉토리 탐색, 무차별 로그인 공격 등 광범위한 위협에 효과적으로 대응이 가능합니다.


Fail2Ban으로 스캐닝 본격적으로 막아보기

이제 Nginx 환경에서 특정 파일 확장자에 대한 스캐닝 공격을 차단하는 시나리오를 기준으로

Fail2Ban을 활용하는 방법을 설명하겠습니다.


1. Nginx 설정: 공격 시도에 403 응답 반환

Fail2Ban이 공격을 감지하려면 로그에 특정 패턴이 남아있어야 합니다!


이를 위해 Nginx 설정 파일에 민감한 파일 접근 시 403 Forbidden을 반환하도록 설정하겠습니다.

이 로그가 Fail2Ban의 감지 트리거가 될겁니다.

# /etc/nginx/nginx.conf 또는 sites-available/default

server {
    ...

    # 🔒 민감 파일 접근 차단 (403 응답)
    location ~* (\.env|\.sql|\.tar|\.gz|\.zip|\.conf|\.bak) {
        return 403;
    }
    ...
}

설정 후 Nginx를 재시작합니다.

sudo nginx -t && sudo systemctl reload nginx

2. Fail2Ban 설치 및 기본 설정

Fail2Ban을 설치하고 서비스가 정상적으로 실행되는지 확인합니다.

sudo apt update
sudo apt install fail2ban -y
sudo systemctl status fail2ban

또한 Fail2Ban의 기본 설정 파일은 다음 경로에 밀집되어 있습니다.

ls /etc/fail2ban

action.d  fail2ban.conf  fail2ban.d  filter.d  jail.conf  jail.d  paths-arch.conf  paths-common.conf  paths-debian.conf  paths-opensuse.conf

여기서 jail.conf 파일은 기본 템플릿 파일이므로, 건들지 않는 것을 권장합니다.


3. Jail 설정 파일 생성

/etc/fail2ban/jail.d/ 디렉토리에 nginx-scan.conf 파일을 생성하여 새로운 차단 규칙(Jail)을 정의하겠습니다.

위에서 잠깐 언급했지만, jail.conf를 직접 수정하는 대신, 별도의 설정 파일을 만드는 것을 강력 권장합니다.

sudo vi /etc/fail2ban/jail.d/nginx-scan.conf
[nginx-scan]
enabled = true
filter = nginx-scan
action = iptables[name=NGINX-SCAN, port=http, protocol=tcp]
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 60
bantime = 604800  # 7일 (초 단위)
backend = polling

각 옵션에 대해 간략하게 표로 정리해보겠습니다.

옵션 설명
enabled 해당 Jail의 활성화 여부입니다.
filter 사용할 필터 이름입니다. /etc/fail2ban/filter.d/nginx-scan.conf 파일을 가리킵니다.
action IP 차단 시 수행할 동작입니다. iptables를 사용해 http 포트를 차단합니다.
logpath 모니터링할 로그 파일의 경로입니다.
maxretry 차단되기 전까지의 최대 실패 횟수입니다.
findtime maxretry를 카운트할 시간 범위(초)입니다. (예: 60초 안에 5번 실패 시)
bantime 차단 유지 시간(초)입니다.
backend 로그 감지 방식입니다. polling은 파일 시스템 변경을 직접 감시하여 안정적인 감지를 보장합니다.

참고로 actionNGINX-SCAN은 iptables에서 f2b-NGINX-SCAN이라는 새로운 체인으로 생성해주는 이름을 지정해주는 것입니다.
(f2b- 접두사는 Fail2Ban이 자동으로 붙입니다.)
image


4. Filter 설정 파일 생성

/etc/fail2ban/filter.d/ 디렉토리에 nginx-scan.conf 파일을 생성하여 어떤 로그 패턴을 "실패"로 간주할지 정규식으로 정의합니다.

sudo vi /etc/fail2ban/filter.d/nginx-scan.conf
[Definition]
failregex = ^<HOST> - - \[.*\] "(GET|POST|HEAD) /\S*\.?(env|sql|tar|gz|zip|conf|bak)\S* HTTP/1\.[01]" 403
ignoreregex =
  • failregex: 이 정규식에 매칭되고 응답 코드가 403인 로그를 실패로 간주합니다. <HOST>는 Fail2Ban이 자동으로 클라이언트 IP로 치환합니다.

설정 완료 후 Fail2Ban을 재시작하여 변경사항을 적용합니다.

sudo systemctl restart fail2ban

5. 대망의 테스트!

이제 403 응답과 IP 차단이 정상적으로 동작하는지 한 번 봅시다!


maxretry를 5번으로 지정했기 때문에 5번 요청을 해보겠습니다.

for _ in {1..5}; do curl -I http://<IP>/.env

HTTP/1.1 403 Forbidden
Server: nginx/1.24.0 (Ubuntu)
Date: Thu, 19 June 2025 18:30:30 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive

HTTP/1.1 403 Forbidden
Server: nginx/1.24.0 (Ubuntu)
Date: Thu, 19 June 2025 18:30:30 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive

HTTP/1.1 403 Forbidden
Server: nginx/1.24.0 (Ubuntu)
Date: Thu, 19 June 2025 18:30:31 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive

HTTP/1.1 403 Forbidden
Server: nginx/1.24.0 (Ubuntu)
Date: Thu, 19 June 2025 18:30:31 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive

HTTP/1.1 403 Forbidden
Server: nginx/1.24.0 (Ubuntu)
Date: Thu, 19 June 2025 18:30:32 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive

.env 요청을 보내자 모두 403 응답을 받고 있습니다.


그리고 fail2ban status를 확인하여 로그가 잘 감지되었고, IP 차단이 정상적으로 이루어졌는지

다음 명령을 실행하여 확인해봅시다.

sudo fail2ban-client status nginx-scan

Status for the jail: nginx-scan
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     6
|  `- File list:        /var/log/nginx/access.log
`- Actions
   |- Currently banned: 1
   |- Total banned:     1
   `- Banned IP list:   192.168.219.110

위 상태를 보면 총 6번 요청에 대해 failed가 되었고, 192.168.219.110 IP가 Banned IP list에 추가된 것을 볼 수 있습니다.


6. [트러블슈팅] REJECT 문제

하지만 한 가지 문제가 있습니다.


기본적으로 Fail2BanactionREJECT로 "연결 거부" 응답을 보냅니다.

공격자에게 서버가 살아있다는 정보조차 주고 싶지 않다면 응답 없이 패킷을 무시하는 DROP으로 변경하는 것이 좋습니다.


jail.d/nginx-scan.conf 파일의 actionblockType=DROP을 추가해주세요.

action = iptables[name=NGINX-SCAN, port="http,https", protocol=tcp, blocktype=DROP]

7. [트러블슈팅] 도커 환경에서의 여러 문제

7-1. 차단이 안되는 문제

Fail2Ban 설정을 DROP으로 바꿨음에도 차단된 IP에서 계속 요청을 보낼 수 있는 문제가 발생하였습니다.


도커는 컨테이너 네트워킹을 위해 DOCKER-USER라는 별도의 iptables 체인을 사용하며,

이 체인이 Fail2Ban이 생성한 f2b-* 체인보다 우선순위가 높습니다.


따라서 Fail2Ban이 IP를 차단해도 Docker 컨테이너로 들어오는 트래픽은 이 규칙을 우회하게 되는거죠!


따라서 Fail2Ban의 차단이 도커 컨테이너에 적용되도록 하려면, 생성된 f2b-* 체인을 FORWARD 체인에 명시적으로 연결해야 합니다.


먼저 아래 명령어로 f2b-* 체인을 모두 확인해보겠습니다.

sudo iptables -nL | grep 'f2b'

Chain f2b-NGINX-SCAN (0 references)

위처럼 f2b-NGINX-SCAN이라는 체인이 있지만, 이를 호출하고 있지는 않습니다.


아래 명령어로 해당 체인의 룰을 확인해봅시다.

sudo iptables -L f2b-NGINX-SCAN -n --line-numbers
Chain f2b-NGINX-SCAN (0 references)
num  target     prot opt source               destination
1    REJECT     0    --  192.168.219.110      0.0.0.0/0
2    RETURN     0    --  0.0.0.0/0            0.0.0.0/0

이전에 5번 호출하여 DROP이 되었던 IP(192.168.219.110)가 보이네요.


아래 명령을 실행하여 f2b-NGINX-SCAN 체인을 FORWARD로 연결해줍시다.

sudo iptables -I FORWARD 1 -j f2b-NGINX-SCAN

여기서 -I FORWARD 1은 FORWARD 체인의 맨 앞에 연결한다는 뜻으로, 가장 먼저 실행되도록 하는 설정입니다.


이후 다시 확인해보면 1 references 부분을 통해 연결된 것을 볼 수 있을겁니다.

sudo iptables -nL | grep 'f2b'

f2b-NGINX-SCAN  6    --  0.0.0.0/0            0.0.0.0/0            tcp dpt:80
Chain f2b-NGINX-SCAN (1 references)

iptables 설정을 영구적으로 저장해줍시다.

sudo iptables-save

7-2. nginx 컨테이너 로그를 읽지 못함

nginx를 컨테이너로 운영하고 있는 상황이라면 Fail2Ban이 nginx의 컨테이너 로그를 찾지 못해 에러가 발생할 수 있습니다.


기본 nginx 도커 이미지는 로그(access.log, error.log)를 실제 파일로 저장하지 않고,

표준 출력(stdout)과 표준 에러(stderr)로 보내는 심볼릭 링크(/dev/stdout, /dev/stderr)로 설정되어 있습니다.


따라서 호스트에 로그 파일을 마운트해도 파일이 생성되지 않아 Fail2Ban이 감시할 수 없죠.


이를 해결하기 위해서는 심볼릭 링크를 제거하고 실제 로그 파일을 생성하는 커스텀 Nginx 이미지를 생성해야 합니다.

FROM nginx:latest

# 🔧 기존 심볼릭 링크 제거
RUN rm /var/log/nginx/access.log /var/log/nginx/error.log

# 📄 빈 파일 생성 및 권한 설정
RUN touch /var/log/nginx/access.log /var/log/nginx/error.log && \
    chmod 644 /var/log/nginx/access.log /var/log/nginx/error.log

docker compose를 수정해줍시다.

image 대신 build를 사용하도록 변경하고, 로그 디렉토리를 호스트와 마운트해주면 됩니다.

services:
  nginx:
    build:
      context: ./nginx
      dockerfile: Dockerfile
    container_name: onetime-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /home/ubuntu/nginx/logs:/var/log/nginx # 호스트와 컨테이너 로그 디렉토리 마운트
    ...

마지막으로 Fail2Ban이 호스트에 마운트된 로그 파일을 바라보도록 수정하면 끝입니다.

# jail.d/nginx-scan.conf
logpath = /home/ubuntu/nginx/logs/access.log

[번외] 디스코드 웹훅으로 알림 받기!

IP가 차단되거나 해제될 때마다 Discord로 알림을 받아서 실시간으로 차단 상황을 모니터링하는 프로세스를 도입해봅시다!


1. action.d/discord-webhook.conf 파일 생성해주기

[Definition]
actionban = /bin/bash -c 'curl -X POST -H "Content-Type: application/json" \
  -d "{\"content\": \"🚫 <ip> 이(가) **<name>** jail에서 차단되었습니다.\"}" \
  {웹훅_URL}'
actionunban = /bin/bash -c 'curl -X POST -H "Content-Type: application/json" \
  -d "{\"content\": \"✅ <ip> 이(가) **<name>** jail에서 차단 해제되었습니다.\"}" \
  {웹훅_URL}'

2. jail.d/nginx-scan.conf에 액션 추가하기

기본 action 설정 아래에 새로운 줄로 discord-webhook 액션을 추가합니다.

action = iptables-multiport[...]
         discord-webhook[name=NGINX-SCAN]

이제 IP가 차단될 때마다 아래와 같이 Discord 메시지를 받게 됩니다.


[번외] 차단 IP 해제

테스트를 하면서 제 IP가 차단이 되었습니다.


차단되어 있는 제 IP를 해제해주도록 하겠습니다.

sudo fail2ban-client set nginx-scan unbanip 192.168.219.110
    Tag -

Loading script...