날씨 정보를 불러오는 중...

새벽에 우리 집 서버가 스캔당했다 - 홈랩 무단 접근 탐지와 IP 차단 자동화 구축기

2026-09-15 21:45 · IT기술 · 조회 5
banner

새벽에 우리 집 서버가 스캔당했다 - 홈랩 무단 접근 탐지와 IP 차단 자동화 구축기

어제 오전에 포탈 관리자 페이지에서 방문자 통계를 훑어보다가 낯선 패턴이 눈에 들어왔다. 특정 IP 하나가 짧은 시간에 수백 건씩 요청을 쏟아붓고 있었던 것이다. 처음엔 "봇이겠거니" 하고 넘기려다가, 궁금해서 로그를 제대로 파봤다. 결과적으로는 별일 아니었지만(스포일러: 우리 서버는 뚫리지 않았다), 이 김에 재발 방지용으로 IP 차단을 자동화하는 시스템까지 구축했길래 기록으로 남겨본다.

무슨 일이 있었나

포탈은 k3s 위에 Traefik을 리버스 프록시로 두고 있는데, Traefik이 남기는 액세스 로그를 보니 45.148.10.60이라는 IP에서 짧은 시간에 요청이 몰려 있었다. 조회해보니 네덜란드 암스테르담, AS48090 TECHOFF SRV LIMITED 소속 — 스캐닝 트래픽 출처로 종종 신고되는 호스팅사였다.

alert확인된 수치

  • 공격 시각: 오전 11시 18분 ~ 11시 22분, 딱 4분 사이
  • 요청 수: 492건 (1초에 2~3건 꼴로 자동화 툴 특유의 일정한 간격)
  • 이후로는 뚝 끊김 — 스크립트 한 번 돌리고 지나간 자동 스캔이었던 것

로그를 파고들어보니 - Traefik 액세스 로그 분석

Traefik의 액세스 로그는 JSON 한 줄에 요청 하나씩 담겨 나온다. kubectl logs로 뽑아서 IP로 그냥 grep만 해도 되지만, 통계까지 보려면 파이썬으로 파싱하는 게 편하다.

kubectl logs deploy/traefik -n kube-system --since=24h \
  | grep '45.148.10.60' > attacker.log

python3 -c "
import json, collections
statuses = collections.Counter()
paths = collections.Counter()
for line in open('attacker.log'):
    d = json.loads(line)
    statuses[d['DownstreamStatus']] += 1
    paths[d['RequestPath']] += 1
print(statuses.most_common())
print(paths.most_common(10))
"

결과를 보니 상태 코드는 404가 350건, 405가 65건, 429가 40건, 400이 20건, 200이 14건 — 딱 봐도 "이것저것 두드려보다가 대부분 실패한" 모양새였다. 429(Too Many Requests)가 40건이나 찍혔다는 건 Traefik의 레이트리밋이 중간에 제대로 작동했다는 뜻이라 조금 안심이 됐다.

무엇을 노리고 있었나

요청 경로를 쭉 훑어보니 목적이 명확했다. 워드프레스 취약점을 노리는 전형적인 스캐너였다.

  1. 워드프레스 REST API batch 엔드포인트/wp-json/batch/v1, /?rest_route=/batch/v1 등을 /, /blog/, /wp/, /wordpress/ 같은 온갖 하위 경로 조합으로 시도. 인증 우회로 이어질 수 있는 알려진 워드프레스 취약점 패턴이다.
  2. 특정 플러그인 노림수gravitysmtp라는 워드프레스 플러그인의 mock-data 엔드포인트를 두드림. 이 플러그인에 정보 노출 취약점이 있었던 걸로 안다.
  3. 범용 인증 페이지 무차별 시도/login, /register, /signin, /signup, /account 같은 경로에 POST를 반복적으로 날려봄. 우리 포탈에도 /login은 실제로 존재하는 경로라 400이 찍혔지만(요청 형식이 안 맞아서 튕겨나감), 실패한 로그인 시도로 잡히거나 계정이 잠긴 흔적은 전혀 없었다.

우리 포탈은 워드프레스도, 라라벨도 아니라서 저 경로들이 대부분 존재하지 않는다. 그래서 200으로 응답한 14건도 실은 전부 홈페이지가 그대로 반환된 것뿐이었다(응답 크기가 전부 똑같았다). 즉, 시도는 했지만 아무것도 못 건진 셈이다.

구축기 - iptables + systemd로 IP 차단 영구 적용하기

이번 건 자체는 무해하게 끝났지만, 같은 IP가 또 올 수도 있고 앞으로 다른 스캐너도 계속 찾아올 거라, 아예 노드 방화벽 레벨에서 걸러내는 시스템을 만들어두기로 했다.

문제는 이 서버가 Rocky Linux인데 firewalld는 꺼둔 상태였고(k3s랑 같이 쓰면 자잘하게 충돌나는 경우가 있어서), iptables 규칙을 재부팅 후에도 유지해주는 iptables-services 패키지도 설치가 안 되어 있었다는 점이다. 새 패키지 설치 대신, 작은 systemd 유닛 하나로 부팅 시마다 차단 목록을 다시 적용하는 방식을 택했다.

list1단계 — 차단할 IP 목록 파일 만들기

# /etc/ip-blocklist.txt
# 한 줄에 IP 하나, # 뒤는 주석
45.148.10.60  # 2026-09-14 워드프레스 취약점 스캐너, 4분간 492건, 전부 실패

2단계 — 목록을 읽어서 iptables에 적용하는 스크립트

#!/bin/bash
# /usr/local/sbin/apply-ip-blocklist.sh
set -euo pipefail
BLOCKLIST=/etc/ip-blocklist.txt
[ -f "$BLOCKLIST" ] || exit 0

while read -r line; do
  ip="${line%%#*}"
  ip="$(echo "$ip" | xargs)"
  [ -z "$ip" ] && continue
  if ! iptables -C INPUT -s "$ip" -j DROP 2>/dev/null; then
    iptables -I INPUT 1 -s "$ip" -j DROP
    echo "blocked $ip"
  fi
done < "$BLOCKLIST"

iptables -C로 이미 규칙이 있는지 먼저 확인하고 없을 때만 추가하는 게 포인트다. 이렇게 멱등성을 챙겨두면 서비스가 여러 번 재실행돼도 규칙이 중복으로 쌓이지 않는다. 그리고 -I INPUT 1로 맨 앞에 꽂아넣는 이유는, k3s가 kube-router/kube-proxy 용으로 INPUT 체인에 이미 자기 규칙들을 잔뜩 넣어두기 때문이다. 뒤쪽에 추가하면 그 규칙들에 걸려서 내 DROP까지 도달을 못 할 수도 있다.

3단계 — 부팅 시 자동 적용되는 systemd 유닛

# /etc/systemd/system/ip-blocklist.service
[Unit]
Description=Apply static IP blocklist (iptables DROP)
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/apply-ip-blocklist.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
chmod +x /usr/local/sbin/apply-ip-blocklist.sh
systemctl daemon-reload
systemctl enable --now ip-blocklist.service

이제 앞으로 차단할 IP가 생기면 /etc/ip-blocklist.txt에 한 줄 추가하고 systemctl restart ip-blocklist만 실행하면 끝이다. 서버가 재부팅되더라도 이 서비스가 multi-user.target 단계에서 자동으로 규칙을 다시 걸어준다.

왜 굳이 이렇게 만들었나

gear선택 이유

  • firewalld를 다시 켜는 방법도 있었지만, k3s 노드에서 firewalld를 켜두면 k8s가 관리하는 iptables/nftables 규칙과 충돌해서 서비스 통신이 끊기는 사례가 흔하다. 굳이 다시 켜서 리스크를 만들고 싶지 않았다.
  • iptables-services 패키지를 새로 설치하는 방법도 있지만, 딱 IP 하나 영구 차단하자고 시스템 서비스를 새로 얹는 것도 과했다.
  • reverse proxy(Traefik) 레벨에서 미들웨어로 막는 방법도 검토했는데, Traefik의 IPAllowList 미들웨어는 "허용 목록" 방식이라 "이 IP만 빼고 다 허용"을 만들기가 오히려 번거로웠다. 노드 방화벽에서 패킷 자체를 버리는 게 제일 확실하고 직관적이었다.

마무리

결과적으로 이번 건 자체는 위협적이지 않았다 — 워드프레스가 아닌 앱에 워드프레스용 스캐너를 돌렸으니 애초에 먹힐 리가 없었고, 레이트리밋도 제 역할을 했다. 그래도 "별일 아니었다"에서 끝내지 않고 재발 방지 체계를 만들어둔 게 이번 작업의 진짜 소득이라고 생각한다.

혹시 홈랩을 운영 중이라면 한 번쯤 리버스 프록시 액세스 로그를 IP별로 집계해보는 걸 추천한다. 생각보다 이런 자동 스캔은 늘 배경 소음처럼 깔려 있는데, 막상 들여다보기 전까진 잘 모르고 지나간다. 이 글의 iptables + systemd 조합은 방화벽 관리 도구가 따로 없는 소규모 개인 서버에 맞춘 간단한 방식이지 기업용 보안 솔루션 수준은 아니니, 트래픽 규모가 커지면 fail2ban이나 CrowdSec 같은 전용 도구로 넘어가는 게 맞다는 점은 참고하시길.


댓글 0

로그인 후 댓글을 작성할 수 있습니다.