헬스체크 차단 해제
목차
Docker 헬스체크 엔드포인트가 BotBlockFilter에 걸려서 컨테이너가 unhealthy 상태로 찍히고 있었다. 처음엔 앱 문제인 줄 알았는데, 실제로는 /health 요청 자체가 필터 단에서 튕겨 나가고 있던 것. 컨테이너 헬스체크는 짧은 주기로 동일 경로를 반복 호출하는데, 이게 Rate Limit 카운터에 쌓이거나 봇 UA 패턴에 매칭되면서 차단된 상황이었다.
보안 필터의 처리 흐름은 이렇다:
요청 수신
→ 화이트리스트 IP 확인
→ 봇 UA / 의심 경로 감지
→ Rate Limit 체크 (Redis 슬라이딩 윈도우)
→ 차단 / 블랙리스트 등록 / 통과
이번 작업에서 이 흐름 안에 예외 경로 스킵 로직을 끼워 넣었다. 필터가 요청을 잡기 전에 설정된 화이트리스트 경로와 매칭되면 이하 단계를 전부 건너뛰는 구조다.
변경 내역
이번에 건드린 항목을 정리하면:
| 항목 | 기존 | 변경 후 |
|---|---|---|
| Rate Limit 임계값 | 완화 상태 | 강화 (1분 기준 하향) |
| 공격 패턴 감지 | 기본 | 의심 경로 + 페이로드 확장 |
| 블랙리스트 | 수동 | DB 자동 등록 |
/health 경로 |
필터 적용 대상 | 명시적 스킵 |
| AJAX 요청 | Rate Limit 카운트 포함 | 카운트 제외 |
Rate Limit은 이 타이밍에 같이 강화했다. 어차피 예외 경로를 잘 빼두면 정상 트래픽 피해 없이 임계값을 내릴 수 있기 때문. 헬스체크, 모니터링 수집 경로처럼 서버 내부 또는 신뢰 가능한 에이전트가 치는 경로를 명시적으로 제외해 두면, 나머지 공용 경로에 대한 기준은 더 타이트하게 잡아도 된다.
AJAX 요청을 Rate Limit 카운트에서 제외한 건 따로 설명이 필요하다. 페이지 로드 시점에 여러 AJAX 요청이 동시에 나가는 구조에서, 이걸 일반 요청과 동일하게 취급하면 정상 사용자가 Rate Limit에 걸리는 상황이 발생한다. 공격자가 AJAX 엔드포인트를 직접 때리는 경우는 별도 시그니처로 감지하는 편이 낫고, 정상 UX 흐름에서 발생하는 AJAX 트래픽은 카운터에서 분리하는 게 현실적인 균형점이다.
Rate Limit 설계에서 실제로 어려운 부분
임계값 자체보다 "무엇을 카운트 대상으로 볼 것인가"를 정의하는 게 더 어렵다. 동일 IP에서 나오는 요청이라도 헬스체크, AJAX, 정적 에셋 요청, 실제 사용자 액션을 전부 같은 버킷에 던지면 카운터가 너무 빠르게 찬다. 필터가 정교해질수록 버킷 구분과 예외 정책이 복잡해지는 건 피할 수 없다.
Redis 슬라이딩 윈도우 방식을 택하는 이유는 고정 윈도우의 경계 구간 버스팅 문제 때문이다. 고정 윈도우는 윈도우가 리셋되는 경계 직전/직후에 각각 N개씩, 즉 2N 요청이 짧은 시간 안에 통과할 수 있다. 슬라이딩 윈도우는 항상 현재 시점 기준 과거 1분을 보기 때문에 이 버스팅이 없다. 구현 비용은 조금 더 들지만 경계 구간 공격에 대한 방어력 차이가 크다.
다만 Redis가 단일 장애점이 되는 건 따로 챙겨야 한다. Rate Limit 체크 도중 Redis 연결이 끊기면 어떻게 할 건지, 전부 통과시킬 건지 전부 막을 건지, 정책을 명확히 해 두는 게 좋다. 일반적으로는 "fail open", 즉 Redis 장애 시 통과를 선택하는데, 서비스 가용성을 보안보다 우선하는 맥락이라면 그게 맞다. 반대로 장애 시에도 차단을 유지해야 한다면 연결 풀 설정, 타임아웃, 폴백 전략을 같이 설계해야 한다.
예외 경로 관리 방식도 길게 보면 코드 하드코딩보다 설정 분리가 낫다. 코드에 박아두면 경로가 추가될 때마다 배포가 필요하고, 운영 중 긴급하게 예외를 추가해야 하는 상황에서 느리다. 설정 기반으로 분리해 두면 재배포 없이 반영할 수 있고, 어떤 경로가 왜 예외인지 히스토리도 같이 관리하기 쉬워진다.
# 예외 경로 설정 구조 예시
filter_skip_paths:
- path: /health
reason: "Docker healthcheck"
- path: /metrics
reason: "Prometheus scrape"
- path: /readiness
reason: "K8s readiness probe"
reason 필드를 넣어두는 게 포인트다. 나중에 "이거 왜 빠져 있지?" 싶을 때 코드 히스토리 안 뒤져도 된다.
이번 작업 규모 자체는 크지 않았지만, 헬스체크가 차단되는 상황은 운영 중에 꽤 당황스럽다. 앱 로직은 멀쩡한데 컨테이너가 unhealthy로 찍히면 오케스트레이터가 재시작을 반복하거나 트래픽을 빼버리기 때문에, 실제 서비스 영향으로 이어질 수 있다. 보안 필터를 강화할수록 예외 경로 관리는 더 꼼꼼하게 가져가야 한다는 걸 다시 확인한 작업이었다. 이런 작은 수정들이 쌓여서 전체 시스템의 신뢰도가 올라간다.
댓글 0
첫 댓글 달아줘.