AWS WAF에서 특정 경로만 안전하게 허용하기

케이 (3)2026년 10월 01일
Tags #AWS#WAF

image

AWS WAF에서 /@username, /p/123 같은 경로만 안전하게 열려면, 경로 정규식을 좁게 쓰는 것보다 규칙 순서와 rate-based 규칙이 더 중요합니다. 이 글은 그 판단 과정을 정리한 것입니다.

문제 상황

DDoS 때문에 기본 액션을 Block으로 두고 필요한 것만 여는 웹 ACL에서, 아래 네 가지 경로 패턴을 국가·IP 조건과 상관없이 허용해야 했습니다.

  • /@{사용자명}
  • /p/{ID}
  • /v/{ID}
  • /s/{ID}

처음 든 질문은 두 가지였습니다. 와일드카드에 자릿수까지 제한을 걸면 더 안전한가, 그리고 이 경로를 Allow로 빼도 괜찮은가. 결론부터 말하면 자릿수 제한은 조금 도움이 되는 정도이고, 진짜 위험은 Allow 규칙이 이후 규칙을 통째로 건너뛰게 만든다는 점이었습니다.

WAF에는 와일드카드가 없고, 정규식으로 표현합니다

/p/* 같은 식의 와일드카드 문법은 AWS WAF 규칙에 없습니다. 경로를 검사하는 방법은 크게 두 가지입니다.

  • String match: /p/로 시작하는지(starts with)처럼 문자열 위치 조건으로 비교합니다. 대소문자를 구분하므로 필요하면 LOWERCASE 같은 text transformation을 겁니다.
  • Regex match / Regex pattern set: 문자 범위와 자릿수를 제한하려면 이 방법만 가능합니다.

AWS 문서에 따르면 AWS WAF는 PCRE(libpcre)의 패턴 문법을 일부 예외와 함께 지원합니다. 문서에 명시된 미지원 항목은 다음과 같습니다.

  • 역참조(backreference)와 측정 부분식(capturing subexpression)
  • 서브루틴 참조와 재귀 패턴
  • 조건 패턴, 백트래킹 제어 동사
  • \C, \R, \K 디렉티브
  • 콜아웃과 내장 코드
  • 원자적 그룹(atomic grouping)과 소유 수량자(possessive quantifier)

zero-width assertion은 일부만 지원한다고 되어 있지만 정확한 범위를 적은 목록은 찾지 못했습니다. 지원되지 않는 문법을 쓰면 API가 오류를 반환하므로, 패턴은 저장 단계에서 검증됩니다.

정규식 패턴 세트에 패턴을 여러 개 넣으면 OR로 평가됩니다. 그래서 경로 형태마다 패턴을 나눠 넣는 방식이 괄호 없이 쓸 수 있어 안전합니다.

정규식 설계: 시작과 끝을 반드시 고정합니다

경로 정규식의 핵심은 앞뒤 앵커(^, $)와 허용 문자셋입니다. 실제 ID 형식에 맞게 고치세요. 아래는 예시입니다.

^/@[A-Za-z0-9_]{3,10}$
^/[pvs]/[A-Za-z0-9_-]{1,40}$
  • $ 앵커: 없으면 /p/123/../../admin 같은 경로도 앞부분만 맞으면 통과합니다.
  • 사용자명은 상한과 하한을 모두 서비스 규칙에 맞축니다: {3,10}처럼 서비스가 실제로 받는 범위와 같아야 합니다.
  • ID 길이는 하한을 느슬하게: ID 길이가 나중에 늘어도 정상 유저가 막히지 않도록 상한만 정합니다.

하위 경로(/@abcde/contents, /@abcde/shorts)까지 한 줄로 쓰려면 선택 그룹을 쓰면 됩니다.

^/@[A-Za-z0-9_]{3,10}(?:/(?:contents|shorts|products))?/?$

이 패턴은 /@abcde, /@abcde/, /@abcde/contents, /@abcde/contents/를 모두 허용합니다. 끝 슬래시를 허용하지 않으려면 /?를 빼면 됩니다. (?:...)가 미지원 목록의 "capturing subexpression"에 해당하는지는 문서만으로 확정하지 못했습니다. 저장이 안 되면 괄호 없이 최상위 |로 풀어 쓰거나 패턴을 나눠 넣으면 됩니다.

^/@[A-Za-z0-9_]{3,10}/?$|^/@[A-Za-z0-9_]{3,10}/contents/?$|^/@[A-Za-z0-9_]{3,10}/shorts/?$|^/@[A-Za-z0-9_]{3,10}/products/?$

한글 등 다국어 사용자명을 허용한다면 퍼센트 인코딩된 값이 들어오므로 URL_DECODE 변환 후 문자셋에 해당 범위를 넣어야 합니다.

URI path만 보면 쿼리 스트링은 상관없습니다

규칙의 검사 대상을 URI path로 지정하면 ? 뒤의 쿼리 스트링은 검사에 포함되지 않습니다. AWS 문서는 URL www.xyz.com/images/daily-ad.jpg?UserName=abc#section2를 예로 URI path(/images/daily-ad.jpg), 쿼리 스트링(UserName=abc), 프래그먼트(section2)를 별도 구성 요소로 나눠 설명합니다.

그래서 /@abcde?page=2나 /p/123?utm_source=x 같은 요청도 $ 앵커가 붙은 정규식에 매칭됩니다. 주의할 점이 두 가지 있습니다.

  • 검사 대상을 잘못 고르면 정상 요청이 막힙니다. 전체 URI 같은 옵션을 고르면 쿼리가 포함되어 $ 때문에 쿼리가 붙은 요청이 전부 불일치합니다.
  • 쿼리 스트링은 이 규칙에서 아무 검증도 받지 않습니다. 필요하면 쿼리 스트링 크기 조건을 AND로 추가해 상한을 둘 수 있습니다.

문서에는 text transformation을 쓰지 않으면 AWS WAF가 URI를 정규화하지 않고 클라이언트가 보낸 그대로 검사한다고 되어 있습니다. 그래서 %2e%2e 같은 인코딩 우회를 막으려면 URL_DECODE를 걸어 두는 편이 안전합니다.

Allow는 종료 액션이라 규칙 순서가 방어를 좌우합니다

이 글에서 가장 중요한 내용입니다. AWS WAF의 Allow는 요청을 보호 대상 리소스로 넘기는 종료(terminating) 액션이고, 종료 액션이 실행되면 그 뒤 규칙은 평가하지 않습니다. AWS 모범 사례 가이드도 Allow 규칙은 뒤에 오는 규칙이 차단했을 요청까지 허용해 버릴 수 있어, 특히 상단에 둘 때 주의해야 한다고 설명합니다.

그러므로 /p/* 같은 경로를 Allow로 빼는 순간, 그 규칙보다 뒤에 있는 rate-based 규칙이나 Bot Control 같은 규칙은 해당 경로에 적용되지 않습니다. 유효한 형식의 경로로 대량 요청을 보내는 DDoS는 정규식으로 막을 수 없기 때문에, 자릿수를 맞춰도 그대로 통과합니다. 즉 정규식은 “어떤 요청이 후보인가”를 정하는 역할이고, 대량 요청 방어는 다른 규칙이 맡아야 합니다.

두 가지 설계 방향이 있습니다.

  1. "허용 패턴이 아니면 Block" 방식: NOT 문장으로 경로 조건을 뒤집어 Block 규칙을 만듭니다. 허용 경로도 이후 규칙(rate-based 등)을 계속 거칩니다. NOT 문장은 내부 문장이 매칭되지 않을 때 매칭되는 논리 부정으로, 예를 들면 "경로가 허용 패턴이 아니면 Block"처럼 쓸 수 있습니다.
  2. Allow를 쓰되 rate-based 규칙을 그보다 앞에 둔다: 다음 섹션의 구성입니다.

권장 규칙 구성: 경로 전용 rate-based 규칙 뒤에 Allow

경로를 국가·IP 필터보다 앞에서 허용해야 한다면, Allow 규칙 바로 앞에 그 경로만 세는 rate-based Block 규칙을 두세요. rate-based 규칙은 scope-down 문장으로 추적·제한 대상 요청을 좁힐 수 있습니다. 문서는 기본 WCU를 2, 사용자 지정 집계 키 1개당 30을 더하고 scope-down 문장의 WCU는 따로 더하라고 설명합니다.

| 순서 | 규칙 | 액션 | 역할 | | --- | --- | --- | --- | | 1 | 악성 IP 차단 등 기존 방어 규칙 | Block | 알려진 나쁜 트래픽은 새 경로에 닿기 전에 차단 | | 2 | 경로 전용 rate-based 규칙 | Block | 4개 경로에 대해 IP당 요청 수 초과 시 차단 | | 3 | 경로 Allow 규칙 | Allow | 정규식과 GET 조건이 맞으면 허용, 이후 국가 필터를 건너뜀 | | 4 | 국가·IP 필터 등 나머지 규칙 | — | 그 외 요청 처리 | | 기본 | 웹 ACL 기본 액션 | Block | 어느 규칙에도 맞지 않으면 차단 |

2번이 3번 앞에 있다는 점이 핵심입니다. 순서가 바뀌면 이 경로는 rate 제한을 통째로 건너뀝니다. 기존 전역 rate 규칙이 뒤에 있어도 이 경로에는 적용되지 않으므로, 경로 전용 rate 규칙이 유일한 flood 방어선이 됩니다. 임계치는 이 경로의 정상 트래픽 로그(예: IP당 최대 요청 수)를 보고 정하세요. 문서만으로 정할 수 있는 값이 아닙니다.

적용 절차: Count 모드로 검증한 뒤 전환합니다

운영 중인 웹 ACL에 규칙을 바로 Allow·Block으로 넣으면 정상 요청이 막히거나 의도하지 않은 경로가 열릴 수 있습니다. 아래 순서를 권합니다.

  • [ ] 정규식 패턴 세트를 만들고 저장이 되는지 확인합니다(미지원 문법이면 저장 단계에서 오류가 납니다).
  • [ ] 검사 대상을 URI path로 지정하고 URL_DECODE를 설정합니다.
  • [ ] 경로 전용 rate-based 규칙을 Count로 만들어 로그에서 정상 트래픽이 임계치를 넘지 않는지 봅니다.
  • [ ] Allow 규칙을 Count로 만들어 정규식에 맞는 요청과 안 맞는 요청을 비교합니다.
  • [ ] 두 규칙의 순서를 확인합니다(rate-based가 Allow보다 앞).
  • [ ] 로그가 깨끗하면 rate-based 규칙을 Block, Allow 규칙을 Allow로 바꿉니다.
  • [ ] 전환 후에도 샘플링된 요청과 지표를 일정 기간 봅니다.

한계와 참고 문서

이 글의 내용 중 문서로 확인하지 못한 부분은 아래와 같습니다. 실제 적용 전에 직접 확인해 보세요.

  • 정규식 패턴의 길이 제한은 확인한 문서에 없었습니다.
  • 비캡처 그룹 (?:...)가 미지원 목록에 걸리는지, 지원되는 zero-width assertion의 범위는 문서만으로 확정하지 못했습니다.
  • rate-based 규칙의 적절한 임계치는 문서로 정해지지 않으며, 서비스의 정상 트래픽에 따라 정해야 합니다.
  • CloudFront나 프록시 뒤에 웹 ACL이 붙는 구조에서는 IP 집계 방식을 별도로 확인해야 합니다.

참고 문서:


0개의 댓글