개발 slecs

서버 보안 필터 강화와 블랙리스트 자동 등록 도입

목차

운영 서버 보안 필터를 손본 건 거창한 이유는 아니었다. 임계값이 너무 느슨하게 설정된 채로 오래 방치됐고, 블랙리스트 관리가 수동이어서 공격이 들어왔을 때 대응이 늦었다. 이번에 그 두 가지를 같이 정리했다.

필터 처리 흐름과 각 단계에서 결정한 것들

필터가 처리하는 흐름 자체는 단순하다.

요청 수신
  → 화이트리스트 IP 확인
  → 봇 UA / 의심 경로 감지
  → Rate Limit 체크 (Redis 슬라이딩 윈도우)
  → 차단 / 블랙리스트 등록 / 통과

각 단계가 독립적으로 동작하는 게 핵심이다. 화이트리스트에서 통과하면 이후 Rate Limit 체크를 건너뛰는 구조인데, 이게 예외 경로 처리와 맞물린다. 헬스체크 엔드포인트나 모니터링 수집 경로는 요청 빈도가 높고 규칙적으로 들어오기 때문에, 순진하게 Rate Limit까지 태우면 모니터링이 스스로 차단당하는 상황이 벌어진다. 그래서 해당 경로들은 필터를 명시적으로 스킵하도록 화이트리스트 단계에서 처리했다. 예외 경로 목록을 코드에 하드코딩하지 않고 설정으로 뺀 게 이번에 신경 쓴 부분 중 하나다. 하드코딩이면 경로가 바뀔 때마다 배포가 필요하다.

이번에 변경한 내용을 정리하면:

항목 기존 변경 후
Rate Limit 임계값 완화 상태 강화 (1분 기준 하향)
공격 패턴 감지 기본 의심 경로 + 페이로드 확장
블랙리스트 수동 DB 자동 등록

Rate Limit 임계값을 얼마나 내리느냐가 제일 고민스러운 부분이었다. 너무 타이트하게 잡으면 정상 사용자가 막히고, 너무 느슨하면 필터 의미가 없다. 슬라이딩 윈도우 방식을 쓰면 고정 윈도우 방식에 비해 경계 시점 버스트 문제가 줄어든다. 고정 윈도우는 매 분 0초에 카운터가 리셋되는 구조라 59초에 한계치를 채운 공격자가 1분 0초에 다시 한계치만큼 쏘는 패턴에 취약하다. Redis로 슬라이딩 윈도우를 구현할 때 쓰는 기본 패턴은 이렇다.

# 슬라이딩 윈도우 Rate Limit (Redis Sorted Set)
now = time.time()
window_start = now - 60  # 1분 윈도우

pipe = redis.pipeline()
pipe.zremrangebyscore(key, 0, window_start)   # 윈도우 밖 항목 제거
pipe.zadd(key, {str(now): now})               # 현재 요청 기록
pipe.zcard(key)                                # 윈도우 내 요청 수 조회
pipe.expire(key, 60)
_, _, count, _ = pipe.execute()

if count > THRESHOLD:
    # 차단 처리

구현이 고정 윈도우보다 조금 복잡한 대신 임계값 근처에서 훨씬 안정적이다. 파이프라인으로 한 번에 처리하면 Redis 왕복 횟수도 줄어든다.

AJAX 요청을 Rate Limit 카운트에서 제외하는 옵션도 이번에 추가했다. 페이지 로드 때 AJAX 호출이 여러 번 나가는 구조에서는 정상 사용자도 임계값에 걸리기 쉽다. X-Requested-With: XMLHttpRequest 헤더 여부로 분기하는 방식이라 구현은 단순한데, 공격자도 이 헤더를 임의로 붙일 수 있다는 트레이드오프가 있다. 지금 구조에서는 봇 UA 감지 단계가 먼저 걸러주기 때문에 현 시점에서 허용 가능한 수준이라 판단했다. 나중에 트래픽 패턴이 달라지면 재검토할 부분이긴 하다.

블랙리스트 자동 등록: 수동이 왜 안 되는가

수동 블랙리스트 관리의 문제는 속도다. 공격 패턴이 감지되고, 알람이 오고, 누군가 로그를 확인하고, 수동으로 IP를 등록하는 사이에 요청은 계속 들어온다. 자동 등록은 감지 즉시 DB에 기록하고 다음 요청부터 화이트리스트 체크 단계에서 막는다.

자동화할 때 실제로 신경 써야 하는 건 오탐이다. 로드 테스트 스크립트, 배치 작업, 크롤러형 사내 도구가 공격 패턴과 겹치는 경우가 생각보다 잦다. 이걸 무작정 차단하면 엉뚱한 내부 작업이 멈춘다. 예외 목록을 따로 관리하는 것도 방법이지만, 현실적으로는 TTL을 적절히 설정해서 오탐이 나도 일정 시간 후 자동 해제되게 하는 쪽이 운영 부담이 낮다.

영구 차단과 TTL 차단을 섞어 쓰는 패턴을 정리하면:

  • 단기 의심 (Rate Limit 초과만): TTL 짧게, 자동 만료 후 복구
  • 반복 공격 패턴 (UA + 경로 + 페이로드 조합): TTL 길게, 알림 발송 후 수동 검토 여부 판단
  • 명백한 공격 패턴 (알려진 스캐너 UA, 취약점 탐색 경로): 장기 또는 영구 차단

자동 차단이 발생하면 슬랙/디스코드 알림을 붙여두는 게 중요하다. 자동화 자체는 빠르지만, 사람이 모르는 상태에서 차단이 쌓이면 오탐을 놓치거나 나중에 로그를 다 뒤져야 상황을 파악할 수 있다. 알림에는 차단된 IP, 감지된 패턴, TTL 만료 예정 시각을 같이 넘기면 로그를 따로 열어보지 않아도 판단이 가능하다. 알림은 부가 기능이 아니라 자동화의 필수 짝이다.

보안 필터 변경을 작게 유지해야 하는 이유

작업 규모 자체는 크지 않았다. 임계값 몇 개 바꾸고, 예외 경로 추가하고, 블랙리스트 등록 로직을 자동화한 게 전부다. 그런데 이런 류의 작업은 한 번에 크게 바꾸려 하면 오히려 리스크가 생긴다. 임계값을 너무 강하게 내리면 정상 트래픽이 막히고, 예외 경로를 잘못 설정하면 모니터링이 죽고, 블랙리스트 자동화가 오탐을 내면 내부 서비스가 막힌다.

보안 필터 관련 변경은 변경 폭을 작게 유지하고 배포 후 일정 시간 로그를 의식적으로 보는 게 기본이다. 이번에도 배포 직후 정상 요청이 의도치 않게 막히는지 짧게 모니터링하고 나서 마무리했다. 430 응답이나 블랙리스트 자동 등록 빈도가 기준치 이상으로 올라가면 바로 설정을 되돌릴 수 있게 롤백 준비도 해뒀다.

이런 작은 수정들이 쌓여서 전체 시스템이 조용해진다. 보안 필터의 목표가 결국 그거다.

댓글 0

첫 댓글 달아줘.