개발 slecs

서버 보안 필터 강화

목차

보안 필터는 작업할 때마다 긴장이 따른다. 잘못 건드리면 공격자보다 정상 사용자가 먼저 막히고, 반대로 너무 느슨하게 두면 실질적으로 의미가 없어진다. 이번 작업은 BotBlockFilter에 Supply API 경로 스킵을 추가하면서 전반적인 임계값 재조정도 함께 진행했다.

왜 이 시점에 건드렸나

운영 중인 보안 필터는 크게 세 영역을 처리한다.

  • IP 기반 Rate Limit - Redis 슬라이딩 윈도우 방식
  • 봇 UA, 의심 경로/페이로드 감지
  • 블랙리스트 자동 등록

이전 작업에서 Rate Limit 임계값을 다소 완화해둔 상태였다. 정상 트래픽까지 막힌다는 피드백이 있었기 때문인데, 완화 이후로 일부 스캐닝 트래픽이 필터를 통과하기 시작했다. 임계값을 다시 하향하되 이번엔 예외 경로를 명확히 분리해서 두 마리 토끼를 잡는 방향으로 잡았다.

필터 처리 흐름 자체는 바뀌지 않았음.

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

흐름은 단순한데 각 단계의 파라미터와 예외 목록이 누적되면서 관리 포인트가 늘어난 게 이번 작업의 배경이다. 처음 세팅할 때야 변수가 몇 개 없지만, 운영하다 보면 "이 경로는 빼줘", "이 IP는 카운트 다르게 해줘" 같은 요구사항이 쌓인다. 그게 코드 여기저기에 흩어져 있으면 나중에 전체 그림을 파악하기 어려워진다.

변경 내용 상세

항목 기존 변경 후
Rate Limit 임계값 완화 상태 강화 (1분 기준 하향)
공격 패턴 감지 기본 패턴 의심 경로 + 페이로드 확장
블랙리스트 수동 등록 DB 자동 등록
Supply API 경로 필터 적용 스킵 처리
AJAX 요청 Rate Limit 카운트 포함 카운트 제외 옵션 추가

Supply API 경로 스킵이 이번 변경에서 핵심이었다. 헬스체크 엔드포인트나 모니터링 수집 경로처럼 내부에서 주기적으로 요청을 보내는 경로들이 Rate Limit에 걸리면 운영 감시 자체가 끊긴다. 모니터링이 죽어 있는 상태에서 다른 장애가 겹치면 감지가 늦어지는 구조가 만들어진다. 이런 경로는 필터 진입 전에 먼저 빼줘야 한다.

예외 경로 처리는 코드 레벨에서 명시적으로 두는 게 낫다고 생각함. 설정 파일로 빼면 배포 타이밍 이슈가 생기고, 필터 코드 안에 하드코딩으로 묻어두면 나중에 찾기 어렵다. 대략 이런 구조로 처리했다.

SKIP_PATHS = [
    "/health",
    "/metrics",
    "/supply/api/",   # Supply API 경로 추가
]

def should_skip(path: str) -> bool:
    return any(path.startswith(p) for p in SKIP_PATHS)

class BotBlockFilter:
    def process_request(self, request):
        if should_skip(request.path):
            return None  # 필터 스킵, 다음 미들웨어로
        return self._run_filter_chain(request)

리스트가 길어지면 prefix 매칭 대신 트라이(trie) 구조로 바꾸는 게 성능상 낫지만, 현재 규모에서는 오버엔지니어링이라 판단해서 단순하게 뒀다. 예외 경로가 수십 개를 넘어가는 시점에 다시 생각하면 될 것 같다.

AJAX 요청 Rate Limit 제외는 약간의 트레이드오프가 있다. 페이지 하나 로드할 때 AJAX 콜이 여러 개 나가는 구조라 동일 기준을 적용하면 정상 사용자가 먼저 막힌다. 그래서 AJAX를 일반 카운트에서 빼되, AJAX 전용 임계값을 별도로 두는 방식으로 처리했다. 완전히 제외하면 AJAX만 노려서 스캐닝하는 패턴을 못 잡기 때문이다. 분리된 카운터를 관리하는 게 번거롭지만 이 방법이 제일 현실적이었음.

공격 패턴 감지 확장은 의심 경로 목록과 페이로드 패턴 두 가지를 건드렸다. 경로 쪽은 .env, .git, wp-admin 류의 스캐닝 시도와 최근 자주 보이는 패턴들을 추가했고, 페이로드는 SQL 인젝션 변형 패턴 몇 가지를 더 넣었다. 완벽한 감지는 불가능하고, WAF나 별도 솔루션이 하는 것처럼 정교하게 만들 것도 아니라서 "명백히 악의적인 것"을 걸러내는 수준에서 선을 그었다.

블랙리스트 자동 등록 - 운영에서 실제로 어떻게 작동하나

수동 블랙리스트는 속도 문제가 있다. 공격성 요청을 감지하고 나서 누군가 콘솔에 들어가서 추가하는 사이에 수백 개의 요청이 더 들어온다. DB에 자동 등록하고 다음 요청부터 즉시 반영하는 구조가 훨씬 실용적이다.

자동화할 때 놓치기 쉬운 부분이 오탐 처리다.

  • 로드 테스트 도구는 UA가 이상하거나 요청 패턴이 봇과 비슷하다. 테스트 전에 테스트 서버 IP를 화이트리스트에 미리 넣어두지 않으면 자동 차단된다.
  • 배치 작업이나 크론잡이 내부 IP가 아닌 경로로 요청을 때리는 경우도 있다. 이것도 봇으로 오인당하기 좋다.
  • TTL을 설정해서 일정 시간 후 자동 해제되도록 하면 오탐 복구가 수월해진다. 영구 차단은 운영 부담이 크고 블랙리스트가 계속 비대해진다.

알림 연동도 함께 붙였다. 자동 차단이 발생하면 슬랙으로 차단된 IP와 감지된 패턴, 요청 샘플이 들어오게 해뒀다. 이걸 안 달면 자동화가 잘 돌고 있는지 아닌지를 로그 뒤져야 알 수 있어서 운영 관점에서 불안하다. 알림이 너무 많이 오면 무감각해지니까 일정 횟수 이상의 차단이 발생했을 때만 오도록 조건도 넣었음.

자동 등록 이후의 흐름은 이렇다.

차단 감지
  → 공격 점수 계산 (패턴 매칭 가중치 합산)
  → 임계값 초과 시 DB 블랙리스트 INSERT (TTL 포함)
  → 캐시 무효화 (다음 요청부터 즉시 적용)
  → 슬랙 알림 발송

점수 계산 방식은 단순한 합산이다. .env 접근 시도 +5, 알려진 악성 봇 UA +3, Rate Limit 초과 기록 +2, 이런 식으로. 합산 점수가 특정 값을 넘으면 자동 차단. 단계를 나눠서 점수가 낮은 쪽은 Rate Limit만 강화하고 높은 쪽은 완전 차단하는 방식도 고려했는데, 일단 단순하게 시작하고 운영 데이터를 보면서 조정하기로 했다.

보안 필터 관련 작업은 "완료"가 없다. 공격 패턴이 계속 바뀌고, 서비스 구조도 바뀌고, 예외 경로도 늘어난다. 이번 변경이 당장 운영 안정성에 기여하는 건 맞지만, 몇 달 뒤에 다시 보면 또 손볼 데가 나올 것이다. 중요한 건 그때 빠르게 파악하고 수정할 수 있게 코드와 설정을 한 곳에 모아두는 것이다.

댓글 0

첫 댓글 달아줘.