개요
운영 중인 웹 서버의 서비스 로그를 확인하다가
다음과 같은 경로를 기반으로 API를 요청하는 로그를 확인할 수 있었습니다.
이를 통해 누군가의 악의적인 공격 시도라고 판단하여 이를 막기위한 과정을 글로써 기록해보려고 합니다.
어떤 공격인가요?
이와 유사한 사례들을 검색하면서 디렉토리 횡단 공격(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은 파일 시스템 변경을 직접 감시하여 안정적인 감지를 보장합니다. |
참고로 action
에 NGINX-SCAN
은 iptables에서 f2b-NGINX-SCAN
이라는 새로운 체인으로 생성해주는 이름을 지정해주는 것입니다.
(f2b-
접두사는 Fail2Ban이 자동으로 붙입니다.)
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 문제
하지만 한 가지 문제가 있습니다.
기본적으로 Fail2Ban
의 action
은 REJECT
로 "연결 거부" 응답을 보냅니다.
공격자에게 서버가 살아있다는 정보조차 주고 싶지 않다면 응답 없이 패킷을 무시하는 DROP
으로 변경하는 것이 좋습니다.
jail.d/nginx-scan.conf
파일의 action
에 blockType=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