Published on

EKS ALB Ingress WAF 403 차단 로그로 푸는 법

Authors

서버가 403을 돌려주면 보통 애플리케이션(인증/인가)부터 의심합니다. 그런데 EKS + AWS Load Balancer Controller(ALB Ingress) + AWS WAF 조합에서는, 애플리케이션까지 도달하기 전에 WAF에서 잘려서 403이 나는 경우가 흔합니다. 이때 핵심은 “어떤 룰이 왜 막았는지”를 WAF 로그에서 정확히 찾아 ALB/Ingress/앱 로그와 같은 요청으로 연결하는 것입니다.

이 글은 다음을 목표로 합니다.

  • 403이 WAF 차단인지 ALB/Ingress/앱 문제인지 빠르게 구분
  • WAF 로그에서 차단 룰(ruleId / terminatingRuleId) 을 특정
  • 차단된 요청을 재현/검증하고, 최소 범위로 예외(allow) 또는 룰 튜닝 적용
  • 운영에서 흔한 함정(헤더, 경로 정규화, 바디 검사, 레이트 리밋)을 함께 정리

관련해서 ALB Ingress 레벨의 인증(401) 이슈를 함께 점검해야 한다면: EKS ALB Ingress 401 반복 - OIDC·JWT·헤더 점검

1) 먼저: 이 403은 누가 만들었나? (WAF vs ALB vs 앱)

1-1. 클라이언트에서 403 응답 헤더로 1차 판별

WAF 차단일 때 응답은 ALB에서 내려오지만, 특징적인 헤더/바디 패턴이 보입니다.

  • Server: awselb/2.0 가 보이고
  • 바디가 ALB 기본 에러 페이지 형태(HTML)이며
  • 애플리케이션 로그에 해당 요청이 전혀 없다

이면 WAF/ALB 구간에서 잘렸을 확률이 큽니다.

다만 결정타는 WAF 로그입니다. “차단됐다”는 사실과 “어떤 룰이 막았다”는 증거는 WAF 로그에 있습니다.

1-2. ALB 액세스 로그로 ‘타겟까지 갔는지’ 확인

ALB 액세스 로그를 켜두면, 요청이 타겟 그룹까지 전달되었는지 간접적으로 볼 수 있습니다.

  • WAF에서 차단되면 타겟 응답 코드 대신 ALB/WAF 단계에서 403이 찍히고, 타겟 관련 필드가 비정상/비어있을 수 있습니다.
  • 반대로 타겟까지 갔다면 target_status_code 등이 의미 있게 남습니다.

> 운영에서 로그가 누락/지연되어 원인 파악이 늦어지는 일이 많습니다. 수집 파이프라인 점검은 EKS에서 fluent-bit 로그 누락·지연 원인 9가지 도 같이 참고하세요.

2) WAF 로그를 “쓸모 있게” 켜는 방법 (필수)

WAF 로그는 CloudWatch Logs 또는 Kinesis Firehose(S3)로 보냅니다. 운영에서는 보통 S3 적재 + Athena가 가장 분석하기 편합니다.

2-1. WAF Logging Configuration 활성화

WAF Web ACL에 로깅을 켜고, 필요하면 필드 리덕션(redaction) 을 설정합니다(PII/토큰 보호).

  • httpRequest.headersauthorization, cookie 등은 마스킹/제외 권장
  • 대신 디버깅에 필요한 host, user-agent, x-forwarded-for, x-amzn-trace-id 등은 남깁니다

2-2. 룰을 바로 차단하지 말고 Count로 관찰하기

대규모 룰 변경은 장애를 부릅니다. 특히 AWS Managed Rules는 서비스 특성에 따라 오탐이 생길 수 있습니다.

  • 신규 룰/룰 그룹은 먼저 Count 액션으로 적용
  • 로그에서 오탐 여부를 확인한 뒤 Block로 승격

3) WAF 403의 ‘진짜 원인’은 로그의 이 필드에 있다

WAF 로그에서 가장 중요한 필드는 아래입니다.

  • action: BLOCK / ALLOW / COUNT
  • terminatingRuleId 또는 terminatingRuleType
  • ruleGroupList (Managed rule group이면 여기에서 매칭 룰 확인)
  • httpRequest.clientIp, country, uri, args, headers
  • labels (AWS Managed Rules는 라벨로 분류 힌트를 줌)

3-1. CloudWatch Logs Insights 예시 쿼리

CloudWatch Logs로 보낸 경우, Insights로 “차단 Top 룰”부터 뽑습니다.

fields @timestamp, action, terminatingRuleId, httpRequest.uri, httpRequest.clientIp
| filter action = "BLOCK"
| stats count() as blocked by terminatingRuleId
| sort blocked desc
| limit 20

특정 URI만 막히는지 확인:

fields @timestamp, action, terminatingRuleId, httpRequest.uri
| filter action = "BLOCK" and httpRequest.uri like /\/api\//
| stats count() as blocked by httpRequest.uri, terminatingRuleId
| sort blocked desc
| limit 50

3-2. S3 + Athena로 장기 분석(운영 추천)

S3에 WAF 로그(JSON)를 쌓는다면 Athena 테이블을 만들어서 빠르게 집계할 수 있습니다.

SELECT
  terminatingruleid,
  COUNT(*) AS blocked
FROM waf_logs
WHERE action = 'BLOCK'
  AND from_unixtime(timestamp/1000) > now() - interval '1' day
GROUP BY 1
ORDER BY blocked DESC
LIMIT 20;

> 포인트: “403이 났다”가 아니라 어떤 룰이 BLOCK 했는지(terminatingRuleId) 를 먼저 확정하세요. 그 다음에야 예외를 안전하게 설계할 수 있습니다.

4) ALB Ingress와 WAF 연결 구조를 이해해야 삽질이 줄어든다

EKS에서 ALB Ingress는 AWS Load Balancer Controller가 ALB와 리스너 룰을 만들고, WAF는 보통 ALB(또는 CloudFront) 에 Web ACL을 연결합니다.

  • 클라이언트 → (CloudFront) → ALB(+WAF) → TargetGroup → Pod
  • WAF가 막으면 Pod까지 요청이 안 감 → 앱 로그에 흔적 없음

또한 ALB Ingress는 경로 기반 라우팅을 하므로, WAF에서 보는 uri는 보통 클라이언트가 호출한 원본 경로입니다. 다만 앱이 내부적으로 리라이트를 한다면(다른 Ingress 컨트롤러처럼) 혼동이 생길 수 있는데, ALB Ingress는 NGINX처럼 강력한 리라이트를 지원하지 않으므로 대개 단순합니다.

5) 가장 흔한 WAF 403 패턴 6가지와 로그로 확인하는 법

5-1. AWSManagedRulesCommonRuleSet의 Body/QueryString 오탐

  • 증상: 특정 POST 요청(JSON 바디)만 403
  • 로그 힌트: AWSManagedRulesCommonRuleSet 하위 룰이 terminating

대응:

  • 해당 엔드포인트에만 예외 적용(URI 스코프)
  • 또는 바디 검사 크기/형식을 조정(가능하면 API 설계 변경)

5-2. SQLi/XSS 룰이 정상 파라미터를 공격으로 인식

  • 증상: 검색 API에서 q=select ... 같은 입력이 403
  • 로그 힌트: AWSManagedRulesSQLiRuleSet 또는 XSSRuleSet

대응:

  • 해당 파라미터만 예외(예: q 파라미터)
  • 완전 disable은 지양

5-3. Bot Control / Anonymous IP / IP Reputation 차단

  • 증상: 특정 ISP/국가/프록시에서만 403
  • 로그 힌트: clientIp, country, labels에 bot/anon 관련 라벨

대응:

  • 사내 NAT/모니터링/서드파티 헬스체커 IP allowlist
  • CloudFront를 쓴다면 원본 IP 전달 헤더 구성 점검

5-4. Rate-based rule(요청 폭주)로 403

  • 증상: 트래픽 급증 시 특정 경로만 403이 튐
  • 로그 힌트: rate 기반 룰이 terminating, rateBasedRuleList

대응:

  • 경로별 rate limit 분리
  • 인증 사용자/토큰 기준으로 키를 잡을 수 있으면 더 안전
  • 증상: 일부 사용자만 403, 대개 쿠키가 큰 브라우저 세션
  • 로그 힌트: request headers에 비정상적으로 큰 값

대응:

  • 세션 쿠키 정리, 토큰 길이 관리
  • 불필요한 헤더 전달 최소화

5-6. 실제로는 WAF가 아니라 ALB Idle timeout/504와 혼동

403과 별개로, “차단”처럼 보이지만 사실은 타임아웃일 수 있습니다. 특히 스트리밍/롱폴링 API에서 자주 섞입니다.

6) 차단된 요청을 ‘같은 요청’으로 추적하는 실전 워크플로

6-1. 상관관계 ID를 하나로 통일

추천은 다음 중 하나를 표준으로 정하는 것입니다.

  • X-Request-Id를 클라이언트→서버까지 유지
  • 또는 traceparent(W3C Trace Context)

ALB는 기본적으로 애플리케이션에 요청 ID를 강제하지 않으므로, 앱/클라이언트에서 넣는 편이 낫습니다.

6-2. WAF 로그에서 특정 요청을 잡고, 같은 시간대 ALB 로그와 매칭

  • WAF 로그에서 @timestamp, clientIp, uri, headers.user-agent 확보
  • ALB 액세스 로그에서 같은 조건으로 검색
  • 앱 로그에는 없다면 WAF 차단 확정

6-3. 재현은 curl로 최소화

브라우저는 변수가 많습니다. 차단된 요청의 핵심 요소(메서드/경로/쿼리/바디/헤더)를 줄여가며 재현합니다.

curl -i 'https://api.example.com/search?q=select+1'

curl -i -X POST 'https://api.example.com/v1/items' \
  -H 'Content-Type: application/json' \
  -H 'X-Request-Id: debug-20260223-001' \
  --data '{"name":"test","memo":"<script>alert(1)</script>"}'

재현이 되면, WAF 로그에서 X-Request-Id로 검색할 수 있게 헤더 로깅을 남겨두는 게 가장 빠릅니다(민감정보는 제외).

7) “예외 처리”를 가장 안전하게 하는 방법(룰 끄지 말기)

오탐이 확인되면 흔히 룰 그룹 전체를 Disable 하려 합니다. 하지만 운영 보안 관점에서 최악의 선택입니다. 대신 아래 우선순위를 권장합니다.

  1. 정확히 막는 terminatingRuleId 확인
  2. 해당 룰에 대해
    • 특정 URI만 예외
    • 특정 쿼리 파라미터만 예외
    • 특정 헤더/바디 필드만 예외
  3. 예외 적용 후에도 Count로 잠시 관찰 → 문제 없으면 반영

7-1. Terraform 예시: Managed Rule Group + Rule Action Override

아래는 개념 예시입니다(리소스/필드명은 환경에 맞게 조정).

resource "aws_wafv2_web_acl" "api" {
  name  = "api-acl"
  scope = "REGIONAL"

  default_action { allow {} }

  rule {
    name     = "AWSManagedRulesCommonRuleSet"
    priority = 10

    override_action { none {} }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"

        # 특정 서브룰만 COUNT로 내려 오탐 관찰/완화
        rule_action_override {
          name = "SizeRestrictions_BODY"
          action_to_use { count {} }
        }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "common"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "api-acl"
    sampled_requests_enabled   = true
  }
}

7-2. URI 스코프 예외(정밀)

특정 엔드포인트에서만 오탐이 난다면, 스코프다운(scope-down)으로 범위를 줄입니다.

statement {
  managed_rule_group_statement {
    name        = "AWSManagedRulesSQLiRuleSet"
    vendor_name = "AWS"

    scope_down_statement {
      not_statement {
        statement {
          byte_match_statement {
            search_string = "/health"
            field_to_match { uri_path {} }
            positional_constraint = "STARTS_WITH"
            text_transformation {
              priority = 0
              type     = "NONE"
            }
          }
        }
      }
    }
  }
}

핵심은 “/health는 검사에서 제외”처럼 정말 필요한 범위만 줄이는 것입니다.

8) 운영 체크리스트: 403 차단을 ‘로그로’ 끝내기

  • WAF 로깅이 켜져 있고(CloudWatch/S3) 조회가 가능하다
  • action=BLOCK 이벤트에서 terminatingRuleId를 확인했다
  • 같은 요청을 ALB 액세스 로그에서 찾았고, 타겟까지 안 갔음을 확인했다
  • 재현 curl을 만들었고, 변경 후 재검증할 수 있다
  • 예외는 룰 전체 disable이 아니라 URI/파라미터/서브룰 단위로 최소화했다
  • 변경은 Count → Block 단계로 롤아웃했다
  • 로그 파이프라인 누락/지연이 없도록 모니터링한다

9) 마무리: 결론은 “403을 보지 말고 terminatingRuleId를 보라”

EKS에서 ALB Ingress 뒤 WAF 403은 증상이 아니라 결과입니다. 원인은 거의 항상 WAF 로그의 terminatingRuleId와 rule group 내부 매칭에 있습니다. 이 값을 기준으로 재현 요청을 만들고, 예외를 최소 범위로 적용하면 보안 수준을 유지하면서도 장애를 빠르게 끝낼 수 있습니다.

다음 단계로는,

  • 차단 Top 룰을 대시보드화(CloudWatch Metrics/Athena 집계)
  • 신규 릴리즈 시 Count 기반 사전 관찰
  • 요청 ID/트레이싱 표준화

를 추천합니다. 이렇게 해두면 “갑자기 403” 같은 운영 이슈가 로그만으로 정리되는 구간까지 갈 수 있습니다.