infra
Platform

모듈 맵

[Infra Ops] 웹방화벽 403 분석과 CDN 캐시 관리 실무

0 / 52 완료

펼치기
0 / 52 완료0%

인프라 운영 & SRE · 48 / 52

[Infra Ops] 웹방화벽 403 분석과 CDN 캐시 관리 실무

WAF/WAAP 차단 403 분석, 예외 처리(Whitelist), CDN 캐시 purge, CloudFront/Cloudflare 개념까지

🚨INCIDENT ALERT
HIGH

배포 직후 고객 센터에 전화가 옵니다. "결제 버튼을 누르면 403 에러가 납니다." 확인해보니 WAF에서 차단하고 있습니다. 그런데 그 요청은 완전히 정상적인 결제 요청입니다.

그리고 어제 배포한 새 이벤트 페이지가 일부 사용자에게 아직 이전 버전으로 보입니다. CDN이 이전 콘텐츠를 캐시하고 있기 때문입니다.

이 두 상황 — WAF False Positive와 CDN 캐시 — 은 서비스를 운영하다 보면 반드시 만나는 문제입니다. 이 모듈은 그 대응 방법을 다룹니다.

이번 챕터에서 배울 것
  • 1WAF와 WAAP의 역할 차이를 설명하고 어떤 위협을 탐지하는지 구분할 수 있다
  • 2WAF 차단 로그를 분석해 False Positive 원인을 찾고 예외 규칙을 추가할 수 있다
  • 3CDN의 캐시 동작 원리를 이해하고 배포 후 캐시를 무효화할 수 있다
  • 4CloudFront와 Cloudflare의 캐시 Purge 방법을 각각 실행할 수 있다
  • 5CDN 환경에서 X-Forwarded-For로 실제 클라이언트 IP를 추출할 수 있다

WAF와 WAAP 개념

💡개념

WAF — 웹 공격 패턴 필터링

서버 방화벽에서 443 포트만 열어두었는데 SQL Injection 공격이 들어왔습니다. 방화벽은 포트와 IP만 보기 때문에 HTTPS 요청 안에 담긴 악의적인 쿼리는 통과시킵니다. 운영 중인 서비스가 보안 취약점 스캐닝을 매일 받고 있다는 것을 로그를 보기 전까지는 모릅니다. WAF는 이 HTTP 레이어의 공격을 필터링하는 장치입니다.

WAF(Web Application Firewall)는 HTTP/HTTPS 레이어에서 알려진 공격 패턴을 감지하고 차단합니다. 방화벽이 포트/IP 기반으로 동작한다면, WAF는 요청의 내용(URL, 파라미터, Body, 헤더)을 분석합니다.

WAF — 웹 공격 패턴 필터링확대

WAF가 차단하는 주요 공격:

SQL Injection:    /search?q='; DROP TABLE users; --
XSS:             /comment?text=<script>alert('xss')</script>
Path Traversal:  /api/file?path=../../etc/passwd
Command Injection: /api/exec?cmd=ls;cat /etc/shadow

WAF는 이런 패턴을 Rule로 등록해두고 요청이 매칭되면 차단합니다. 문제는 정상적인 데이터에 이런 패턴이 포함될 때도 있다는 점입니다(False Positive). 예를 들어 SQL 교육 사이트의 게시글에 SELECT * FROM이 포함되면 WAF가 차단합니다.

WAAP (Web Application and API Protection):

WAF에서 확장된 개념으로, 세 가지를 더 합니다.

기능설명
Bot 관리스크래퍼, 무차별 대입 Bot 탐지 및 차단
API 보호OpenAPI 스키마 기반으로 비정상 API 요청 탐지
행동 분석정상 사용자 패턴에서 벗어난 접근 탐지

AWS WAF, Cloudflare WAF, Imperva, Akamai 등이 대표적인 솔루션입니다.

WAF 403 분석

💡개념

차단 로그에서 원인 찾기

WAF를 도입한 다음 날 개발팀에서 "회원가입 API가 403을 반환한다"는 연락이 왔습니다. 사용자 이름 필드에 O'Brien처럼 특수문자가 들어간 요청을 WAF가 SQL Injection으로 오탐한 것입니다. 공격 차단과 정상 요청 차단을 구분하려면 차단 로그에서 어떤 규칙 ID가 발동됐는지를 먼저 확인해야 합니다. 로그를 못 읽으면 WAF가 공격을 막는 건지, 정상 요청을 막는 건지 알 수 없습니다.

WAF가 403을 반환할 때 가장 먼저 확인할 것은 차단된 규칙 ID와 차단 이유입니다. WAF 콘솔이나 로그에서 확인합니다.

WAF vs WAAP와 차단 로그 대응 — WAF는 SQL Injection·XSS·Path Traversal 등 알려진 공격 패턴을 규칙으로 차단, WAAP는 WAF + Bot 관리 + API 보호 + 행동 분석으로 확장. 정상 요청이 차단(False Positive)되면 로그에서 규칙 ID 확인→실제 공격인지 검토→최소 범위 예외→문서화→재테스트, "WAF 끄기"는 금지확대

로컬 터미널
# WAF 로그 파일에서 차단 기록 확인 (AWS WAF + ALB 예시)
# CloudWatch Logs에서 조회하거나, S3 버킷에 저장된 로그 확인

# 로컬 WAF(ModSecurity + Nginx) 로그 예시
grep "403" /var/log/nginx/error.log | grep "ModSecurity" | tail -20
# 예시 출력:
# ModSecurity: Access denied with code 403 (phase 2). Pattern match "SELECT.*FROM" at ARGS:body.
# [id "942100"] [msg "SQL Injection Attack Detected via libinjection"]

# 차단된 요청의 전체 내용 재현
curl -v -X POST https://example.com/api/data \
  -H "Content-Type: application/json" \
  -d '{"query": "SELECT id FROM products WHERE name=?", "param": "apple"}'
# → 이 요청이 차단됐다면 body에 SQL 키워드가 있기 때문

WAF 차단 분석 체크리스트:

1. 어떤 규칙 ID가 매칭됐는가?
2. 어떤 파라미터/헤더/Body 값이 트리거됐는가?
3. 해당 값이 실제 공격 패턴인가, 아니면 정상 데이터인가?
4. 정상 데이터라면 어떤 예외 조건으로 허용할 것인가?
   (경로? 파라미터명? 특정 IP? 특정 규칙만 제외?)
💡개념

WAF 예외 규칙(Whitelist) 추가

SQL 교육 플랫폼에서 수강생이 SELECT * FROM users를 게시판에 올렸는데 WAF가 차단했습니다. 차단 로그에서 오탐을 확인했지만, 해결책은 애매합니다. 게시판 경로 전체를 WAF에서 제외하면 그 경로로 들어오는 진짜 공격도 모두 통과됩니다. 최소 범위로 예외를 설정하는 기술이 필요합니다. 특정 규칙 ID만, 특정 파라미터만, 특정 경로에서만 예외 처리하는 방법이 있습니다.

False Positive가 확인됐다면 최소 범위의 예외 규칙을 추가합니다. 경로 전체를 허용하는 것보다 해당 파라미터 또는 규칙 ID만 예외 처리하는 것이 안전합니다.

로컬 터미널
# ModSecurity 예외 규칙 예시 (/etc/nginx/conf.d/modsecurity-exceptions.conf)

# 방법 1: 특정 경로에서 특정 규칙 ID만 비활성화
SecRule REQUEST_URI "@beginsWith /api/education/content" \
    "id:9001,phase:1,t:none,nolog,pass,\
     ctl:ruleRemoveById=942100"
# 942100 규칙만 해당 경로에서 제외

# 방법 2: 특정 파라미터에서 규칙 비활성화
SecRule REQUEST_URI "@beginsWith /api/posts" \
    "id:9002,phase:2,t:none,nolog,pass,\
     ctl:ruleRemoveTargetById=942100;ARGS:content"
# /api/posts의 content 파라미터에서만 942100 규칙 제외

# AWS WAF에서 예외 규칙 추가 (AWS CLI)
# 특정 헤더가 있는 요청을 WAF 규칙 평가 전에 허용
aws wafv2 create-rule-group \
  --name "AllowedBotRequests" \
  --scope CLOUDFRONT \
  --capacity 100 \
  --region us-east-1

예외 규칙 추가 후 변경 이력을 반드시 문서화합니다. 예외 규칙이 누적되면 WAF 효과가 무력화될 수 있습니다.

CDN 개념

💡개념

CDN이 동작하는 방식

미국 원서버에서 한국 사용자에게 이미지를 직접 서비스하니 로딩이 느리다는 민원이 들어왔습니다. 네트워크 거리 때문에 발생하는 레이턴시는 서버 스펙을 올려도 해결되지 않습니다. CDN은 전 세계 거점에 콘텐츠를 복사해두고 가장 가까운 곳에서 응답하게 합니다. 동시에 원서버에 집중되던 트래픽을 분산시켜 DDoS 부하 감소 효과도 있습니다.

CDN(Content Delivery Network)은 전 세계 여러 거점(엣지 서버)에 콘텐츠 사본을 분산 저장해, 사용자에게 가장 가까운 서버에서 응답을 제공합니다.

CDN 엣지 캐싱 동작 — 오리진 서버의 정적 콘텐츠(이미지·JS·CSS)를 서울·도쿄·프랑크푸르트 같은 지역 엣지에 캐싱해, 사용자는 가까운 엣지에서 응답받는다. Cache HIT면 엣지가 즉시 응답해 오리진까지 안 가고(빠름·오리진 부하 없음), MISS면 오리진에서 가져와 TTL 동안 캐싱한다. CDN 뒤에서는 remote_addr가 엣지 IP이므로 X-Forwarded-For로 실제 클라이언트 IP를 확인하고, 배포 후엔 purge나 버전 쿼리스트링으로 stale 캐시를 무효화한다확대

사용자 (서울) → CloudFront 엣지 (서울)
                        ↓ 캐시 없으면
                원본 서버 (us-east-1)
                        ↓ 콘텐츠 전달 + 캐시 저장
                CloudFront 엣지 (서울)
                        ↓ 다음 요청부터는 캐시에서 응답
사용자 (서울) ←

CDN을 사용하는 이유:

  • 정적 파일(JS, CSS, 이미지) 응답 속도 향상 (지리적 근접성)
  • 원본 서버 트래픽 감소 (캐시 히트율만큼 요청 안 옴)
  • DDoS 완화 (엣지에서 일부 흡수)

캐시 동작 결정 요소:

Cache-Control: max-age=86400    → CDN이 86400초(1일) 캐시
Cache-Control: no-cache         → 매 요청마다 원본 확인
Cache-Control: no-store         → 캐시 금지
ETag, Last-Modified             → 조건부 요청으로 변경 여부 확인
💡개념

요청이 CDN 엣지와 WAF를 지나 오리진에 닿기까지 — 5단계

사용자가 주소창에 URL을 넣고 Enter를 누르면, 그 요청은 오리진(원본) 서버로 곧장 가지 않습니다. CDN 엣지에서 캐시를 먼저 확인하고, WAF 규칙을 통과하고, 그제야 오리진에 닿습니다. 이 경로를 단계로 알면 "정상 요청인데 403", "배포했는데 옛 화면", "로그에 실제 IP가 안 찍힘" 같은 증상이 어느 단계의 문제인지 바로 좁혀집니다. 요청 처리는 단순히 "서버가 응답한다"가 아니라 아래 관문들을 순서대로 지난 결과입니다.

TEXT
[클라이언트]  GET https://example.com/page
   │
   ① CDN 엣지 도착                    (사용자와 가장 가까운 PoP)
   │
   ② 캐시 조회                        (캐시 키로 HIT/MISS 판정)
   │    ├─ HIT  → 엣지가 즉시 응답, 오리진까지 안 감
   │    └─ MISS → 아래로 진행
   │
   ③ WAF 규칙 평가                    (시그니처·레이트리밋·봇 판정)
   │    → 매칭 + 임계치 초과면 여기서 403 차단, 오리진 못 감
   │
   ④ 오리진 요청                      (엣지가 XFF 헤더에 실제 IP 담아 전달)
   │
   ⑤ 응답을 캐시에 저장 후 반환        (Cache-Control 따라 TTL 동안 보관)
   ▼
[클라이언트]  응답 수신

각 단계에서 무슨 일이 일어나고, 틀어지면 어떤 증상인가:

단계하는 일여기서 틀어지면
① 엣지 도착DNS가 사용자를 가장 가까운 CDN PoP(엣지)로 보낸다. 오리진 IP는 감춰지고 클라이언트는 엣지하고만 통신오리진 IP가 노출·직접 접근 가능하면 CDN·WAF를 건너뛰는 우회로가 생김(오리진 우회)
② 캐시 조회엣지가 캐시 키(보통 URL, 때로 쿠키·헤더 포함)로 저장된 응답이 있는지 본다. HIT면 오리진까지 안 가고 즉시 응답개인화 응답을 캐시 키에 세션 없이 HIT시키면 남의 응답이 재사용됨(캐시 오염) / 배포 후 옛 파일은 purge 안 한 것
③ WAF 평가HIT가 아니면 WAF가 URL·파라미터·헤더·본문을 룰셋(예: OWASP CRS)과 대조하고, 레이트리밋·봇 판정을 더해 이상 점수가 임계치를 넘으면 차단정상 요청이 룰에 걸리면 오탐(False Positive) 403 / 표기를 바꾼 공격은 룰을 비껴가 통과(우회)
④ 오리진 요청통과한 요청만 오리진으로. 엣지가 출발지가 되므로 실제 클라이언트 IP는 X-Forwarded-For · CF-Connecting-IP 헤더에 담아 전달오리진이 XFF를 안 읽으면 접근 로그·IP 차단·레이트리밋이 전부 엣지 IP 기준이 됨
⑤ 응답 캐시오리진 응답을 Cache-Control · ETag에 따라 엣지에 저장하고 클라이언트에 반환. 다음 같은 요청은 ②에서 HIT동적 응답에 no-store · private가 없으면 캐시돼 오염 / 정적인데 max-age가 없으면 매번 MISS로 오리진 부하

즉 요청은 "엣지 캐시(②) → 방화벽(③) → 오리진(④)"이라는 관문을 순서대로 지나며, 각 관문이 서로 다른 증상을 만듭니다. 응답 헤더 한 번으로 어느 관문인지 좁힐 수 있습니다 — curl -I에서 CF-Cache-Status(또는 X-Cache)가 HIT면 ②에서 끝난 것(오리진 로그엔 안 남음), 403이고 WAF 룰 ID가 로그에 찍히면 ③, 오리진 로그의 출발지가 죄다 엣지 IP면 ④의 XFF 미처리입니다.

CDN 캐시 관리

💡개념

배포 후 캐시 무효화 절차

배포 후 새 파일이 CDN에 반영되지 않는 가장 흔한 원인은 캐시 무효화(Purge)를 안 한 것입니다.

CloudFront 캐시 무효화:

로컬 터미널
# 전체 경로 무효화 (/* 주의: 비용 발생, 10개 이상부터 과금)
aws cloudfront create-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --paths "/*"

# 특정 파일만 무효화 (권장)
aws cloudfront create-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --paths "/static/js/app.js" "/static/css/main.css"

# 무효화 진행 상태 확인
aws cloudfront list-invalidations \
  --distribution-id EDFDVBD6EXAMPLE \
  --query 'InvalidationList.Items[0].{Id:Id,Status:Status}'

Cloudflare 캐시 무효화:

로컬 또는 서버
# Cloudflare API로 모든 캐시 삭제
curl -X POST "https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/purge_cache" \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  --data '{"purge_everything": true}'

# 특정 URL만 삭제 (권장)
curl -X POST "https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/purge_cache" \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  --data '{
    "files": [
      "https://example.com/static/js/app.123abc.js",
      "https://example.com/static/css/main.456def.css"
    ]
  }'

배포 파이프라인에 캐시 무효화 통합:

로컬 터미널
# CI/CD 스크립트 예시 (GitHub Actions의 deploy step)
# 1. 파일 빌드 및 S3 업로드
aws s3 sync ./dist s3://my-bucket/static/ --delete

# 2. CDN 캐시 무효화
aws cloudfront create-invalidation \
  --distribution-id $CF_DISTRIBUTION_ID \
  --paths "/static/*"

# 3. 무효화 완료 대기 (선택)
aws cloudfront wait invalidation-completed \
  --distribution-id $CF_DISTRIBUTION_ID \
  --id <INVALIDATION_ID>
💡개념

CDN 뒤에서 실제 클라이언트 IP 확인

CDN을 사용하면 웹 서버에 오는 요청의 IP는 CDN 엣지 서버 IP입니다. 로그에 CDN IP만 쌓이면 접근 로그 분석, 차단, 속도 제한이 전부 무의미해집니다.

Nginx
# Nginx에서 X-Forwarded-For 헤더 활용
server {
    # 실제 클라이언트 IP를 X-Real-IP로 설정
    set_real_ip_from 103.21.244.0/22;  # Cloudflare IP 대역 예시
    set_real_ip_from 173.245.48.0/20;
    real_ip_header CF-Connecting-IP;   # Cloudflare 전용 헤더

    # AWS CloudFront 사용 시
    # set_real_ip_from <CloudFront-IP-ranges>;
    # real_ip_header X-Forwarded-For;

    # 로그 포맷에 실제 IP 포함
    log_format main_cdn '$realip_remote_addr $http_x_forwarded_for '
                        '[$time_local] "$request" $status';
    access_log /var/log/nginx/access.log main_cdn;
}
로컬 또는 서버
# CDN 캐시 상태 확인 (응답 헤더로)
curl -s -I https://example.com/static/app.js \
  | grep -E "Cache-Control|Age|CF-Cache-Status|X-Cache"

# 출력 예시:
# Cache-Control: max-age=86400
# Age: 3600                     ← 캐시된 지 3600초 경과
# CF-Cache-Status: HIT          ← Cloudflare 캐시에서 응답
# X-Cache: Hit from cloudfront  ← CloudFront 캐시에서 응답

Age: 0이면 캐시 미스(원본 서버에서 직접 응답), Age: N이면 N초 전에 캐시됐습니다.

CDN 뒤에서 실제 클라이언트 IP 확인 — CDN을 쓰면 웹 서버에 오는 요청의 src IP는 CDN 엣지 서버 IP뿐이라, 그대로 두면 로그에 CDN IP만 쌓여 접근 로그 분석·IP 차단·속도 제한이 전부 무의미해진다. 해결은 CDN 엣지가 실제 클라이언트 IP(203.0.113.55)를 CF-Connecting-IP나 X-Forwarded-For 헤더에 담아 전달하고, Nginx가 set_real_ip_from(CDN 신뢰 대역)과 real_ip_header로 그 헤더를 읽어 $realip_remote_addr로 실제 IP를 복원하는 것이다. 핵심은 신뢰할 수 있는 프록시 대역만 set_real_ip_from에 등록하는 것 — X-Forwarded-For는 누구나 위조할 수 있어 아무 헤더나 믿으면 공격자가 IP를 속여 차단·rate limit을 우회하므로, CDN·LB 대역에서 온 XFF만 신뢰하고 그 밖의 XFF는 무시한다. 여러 프록시를 거치면 XFF에 IP가 콤마로 누적되어 real_ip_recursive on으로 실제 IP를 찾는다확대

실습

1WAF 차단 로그 분석 및 예외 처리 절차 실습

WAF 차단 로그를 확인합니다. 실제 WAF 환경이 없다면 Nginx error 로그에서 403 패턴을 검색합니다. 로그에서 차단 규칙 ID, 차단된 파라미터, 요청 URI를 식별하고 False Positive 여부를 판단하는 절차를 연습합니다.

grep -E 'BLOCKED|403|ModSecurity' /var/log/nginx/error.log | tail -20
🔍실행 후 확인할 것
  • ModSecurity 로그에서 [id "XXXXX"] 규칙 ID를 먼저 찾는다 — 같은 ID가 하루에 50건 이상 반복되면 정상 트래픽을 막는 False Positive 가능성. 10건 미만이면 실제 공격 시도일 가능성 높음
  • msg 필드에서 공격 유형 확인 — "SQL Injection", "XSS Attack" 등이 나오면 규칙 분류 가능. severity가 CRITICAL이면 즉시 차단 유지, WARNING이면 False Positive 여부 분석 후 결정
  • 차단된 URI가 정상 기능의 엔드포인트이고 같은 규칙이 반복되는 조합이면 — SecRuleRemoveById로 해당 경로에 한해 규칙 예외 처리. 전역으로 비활성화하면 실제 공격도 통과됨
2CDN 캐시 상태 및 Age 헤더 확인

CDN이 설정된 도메인에서 정적 파일의 캐시 헤더를 확인합니다. Age 값이 캐시된 시간(초)이며, 0이면 방금 원본에서 가져온 것입니다. 같은 명령을 30초 간격으로 두 번 실행해 Age가 증가하는 것을 확인합니다.

curl -s -I https://example.com/static/app.js | grep -E 'Cache-Control|Age|CF-Cache-Status|X-Cache|ETag'
🔍실행 후 확인할 것
  • Age 헤더를 먼저 확인 — 0이면 방금 원본에서 가져온 것(캐시 미스), 1 이상이면 CDN에서 캐시된 응답. 30초 간격으로 두 번 요청했을 때 Age가 30 증가하면 캐시가 동작 중인 것
  • Cache-Control에 max-age가 없으면 CDN이 캐시 안 할 수 있음 — 정적 파일은 최소 3600(1시간), JS/CSS/이미지는 86400(1일) 이상을 권장. no-store나 private이 있으면 CDN 캐시 불가
  • CF-Cache-Status: MISS이고 Age: 0인 조합이면 CDN 캐시 미동작 — Cache-Control 헤더가 있는데도 캐시 안 되면 쿠키나 Authorization 헤더가 있는 요청이어서 CDN이 캐시를 우회하는 것

트러블슈팅

원인: 배포 파이프라인에 CDN Purge 단계가 없거나, Purge를 실행했지만 일부 엣지 서버에 전파되기 전에 사용자가 접근한 경우입니다.

로컬 또는 서버
# 1단계: 실제로 이전 버전이 캐시되어 있는지 확인
curl -s -I https://example.com/static/js/app.js | grep -E "Age|Cache|ETag"
# Age가 0이 아니면 캐시된 것

# 2단계: 즉각 Purge 실행 (CloudFront)
aws cloudfront create-invalidation \
  --distribution-id $DISTRIBUTION_ID \
  --paths "/static/js/app.js" "/static/css/main.css"

# 3단계: Purge 완료 확인 (InProgress → Completed)
watch -n 5 "aws cloudfront list-invalidations \
  --distribution-id $DISTRIBUTION_ID \
  --query 'InvalidationList.Items[0].Status'"

# 4단계: 파일명에 해시 포함으로 근본 해결 (Cache Busting)
# app.js → app.abc123.js (빌드 도구가 자동 생성)
# 파일명이 바뀌면 CDN이 별도 파일로 인식해 이전 캐시를 쓰지 않음
# webpack: output.filename = '[name].[contenthash].js'

장기적 해결책은 Cache Busting입니다. 빌드 시 파일 내용의 hash를 파일명에 포함시키면, 파일이 변경될 때마다 URL이 달라져 CDN이 자동으로 새 파일을 가져옵니다.

원인: 사용자가 입력한 데이터나 애플리케이션이 전송하는 데이터에 SELECT, INSERT, WHERE 같은 SQL 키워드가 포함되면 WAF의 SQL Injection 규칙이 트리거됩니다. SQL 교육 플랫폼, 코드 에디터, 검색 서비스에서 자주 발생합니다.

로컬 또는 서버
# 1단계: 차단된 요청 재현
curl -v -X POST https://example.com/api/editor/save \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"code": "SELECT name FROM users WHERE id = 1"}'
# → 403이 나오면 WAF 차단 확인

# 2단계: WAF 로그에서 규칙 ID 확인
grep "942100\|942200\|942300" /var/log/waf/access.log | tail -5
# 942xxx 시리즈 = SQL Injection 관련 규칙

# 3단계: 해당 경로만 예외 처리 (ModSecurity 기준)
# /etc/nginx/conf.d/waf-exceptions.conf
# SecRule REQUEST_URI "@beginsWith /api/editor" \
#   "id:5001,phase:2,nolog,pass,\
#    ctl:ruleRemoveTargetById=942100;ARGS:code"

# 4단계: 예외 규칙 반영 후 재테스트
sudo nginx -t && sudo systemctl reload nginx
curl -v -X POST https://example.com/api/editor/save \
  -H "Content-Type: application/json" \
  -d '{"code": "SELECT name FROM users WHERE id = 1"}'
# → 200 응답이 와야 함

심화 — 룰은 우회되고, 캐시는 오염된다

💡개념

심화: WAF는 어떻게 뚫리는가 — 정규화(normalization)와 이상 점수

WAF가 SQL Injection 시그니처를 갖고 있다고 그 계열 공격이 다 막히는 것은 아닙니다. 시그니처는 '정해진 문자열 패턴'을 보는데, 공격자는 같은 의미를 다른 표기로 바꿔 패턴을 비껴갑니다.

  • 우회는 표기를 바꾸는 것: 이중 URL 인코딩, 유니코드 이스케이프, 대소문자 섞기, SQL 주석 삽입으로 union selectuni%256fn/**/sel%256fct처럼 위장하면 원문 패턴과 매칭되지 않습니다. 그래서 WAF는 검사 전에 입력을 정규화(normalize) 합니다 — URL 디코드, HTML 엔티티 디코드, 소문자화 같은 변환을 거친 뒤 룰을 적용합니다. WAF의 정규화 방식과 애플리케이션이 실제로 값을 해석하는 방식 사이에 틈이 있으면, 그 틈이 곧 우회 경로가 됩니다.
  • 단일 룰 차단이 아니라 이상 점수(anomaly scoring): OWASP CRS 같은 룰셋은 룰 하나에 걸렸다고 바로 막지 않고, 걸린 룰마다 점수를 더해 임계치를 넘을 때 차단합니다. 민감도(paranoia level)를 올리면 더 많이 잡지만 오탐도 함께 늘어, 운영은 '탐지율과 오탐의 저울질'입니다. 그래서 WAF 튜닝은 켜고 끝나는 일이 아니라 점수·임계치를 계속 조정하는 일입니다.
  • 한계 — WAF는 앱 수정의 대체재가 아니다: WAF는 알려진 패턴을 앞단에서 거를 뿐 0-day·논리 취약점·인가 결함은 못 봅니다. 정상으로 보이는 요청에 숨은 공격도 놓칠 수 있습니다. WAF는 시간을 벌어 주는 방어 심화(defense in depth)의 한 겹이지, 파라미터라이즈드 쿼리·입력 검증 같은 근본 수정을 대신하지 않습니다.

정리하면, WAF를 뚫는 공격은 '새로운 공격'이 아니라 '같은 공격의 다른 표기'인 경우가 많고, 막는 쪽의 무기는 정규화의 정확성과 점수 기반 튜닝입니다.

상황: 개인화된 응답(로그인 사용자의 이름·장바구니 요약)이 엉뚱한 사용자에게 그대로 노출됩니다. 배포와 무관하고, 특정 URL에서 간헐적으로 재현됩니다.

원인: 개인화 응답이 CDN 엣지에 캐시돼 여러 사용자에게 재사용됐습니다. 오리진이 그 응답에 Cache-Control: privateno-store를 붙이지 않았고, CDN의 캐시 키에 세션 쿠키가 포함되지 않아, 먼저 접근한 로그인 사용자의 응답이 캐시된 뒤 같은 URL로 오는 모두에게 반환된 것입니다. 캐시 키에 들어가지 않은 입력(쿠키·인증 헤더)에 응답이 의존할 때 생기는 전형적인 캐시 오염입니다.

진단: 문제 URL의 응답 헤더에서 CF-Cache-Status(또는 X-Cache)가 HIT인지, Cache-Controlprivate·no-store가 없는지 확인합니다. 로그인·비로그인 두 세션으로 같은 URL을 요청해 응답이 섞이는지 재현하고, 응답에 Set-Cookie가 실린 채 캐시에 함께 저장됐는지 봅니다. 정적 자산은 캐시되어야 정상이므로, 캐시돼선 안 되는 '동적·개인화 경로'가 캐시되고 있는지가 핵심입니다.

해결: 개인화·인증 응답은 오리진에서 Cache-Control: private, no-store로 명시하고, CDN은 세션 쿠키가 있는 요청을 캐시 우회하거나 쿠키를 캐시 키(또는 Vary)에 포함하도록 설정합니다. 근본적으로는 캐시 가능한 정적 경로와 사용자별 동적 경로를 분리하고, 'CDN 기본값이 알아서 해주겠지'에 기대지 않고 오리진이 올바른 캐시 헤더를 책임지게 합니다. 캐시 키 규율이 곧 정보 노출을 막는 보안 경계입니다.

💼
실무 맥락
현업 패턴

실제 업무에서 이 지식이 쓰이는 상황:

WAF는 "설치하면 끝"이 아닙니다. 운영 초기에는 False Positive 처리가 주요 업무입니다. 새 기능을 배포할 때마다 WAF 로그를 확인해 정상 요청이 차단되는지 모니터링하고, 예외 규칙을 추가하면서 WAF를 튜닝합니다.

CDN은 배포 프로세스의 일부입니다. 빌드 → 업로드 → CDN Purge를 세트로 묶어두지 않으면, 배포 후 "왜 이전 버전이 보이냐"는 민원이 반복됩니다.

WAF 운영 일상 루틴:

로컬 터미널
# 매일 오전: WAF 차단 현황 확인
# CloudWatch Logs Insights 쿼리 예시
# fields @timestamp, action, httpRequest.uri, ruleGroupList.0.terminatingRule.ruleId
# | filter action = "BLOCK"
# | sort @timestamp desc
# | limit 50

# 배포 직후 WAF 모니터링 (5분간)
watch -n 10 "grep '403' /var/log/nginx/access.log | grep \"$(date +'%d/%b/%Y:%H:')\" | wc -l"
# 403 카운트가 급증하면 WAF False Positive 가능성

# CDN 캐시 히트율 확인 (Cloudflare Analytics API)
curl "https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/analytics/dashboard" \
  -H "Authorization: Bearer <TOKEN>" \
  | python3 -m json.tool | grep -A3 "cacheStatus"

배포 체크리스트에 추가할 항목:

배포 후 5분 체크리스트:
□ WAF 차단 로그에 새로운 규칙 ID가 등장했는가
□ 배포된 정적 파일에 대해 CDN Purge 실행했는가
□ curl -I로 Age 헤더가 0인지 확인 (캐시 무효화 완료)
□ X-Forwarded-For 로그에 CDN IP가 아닌 실제 IP가 찍히고 있는가

명령어·단축키 빠른 참조

이 모듈에서 다룬 WAF 차단 분석·CDN 캐시 무효화·실제 클라이언트 IP 복원 명령을 실전 옵션과 함께 모았습니다.

명령어/단축키용도자주 쓰는 예
curl -ICDN 캐시 상태를 응답 헤더로 확인curl -sI https://cdn/app.js | grep -iE 'age|cf-cache-status|x-cache'
curl -v -X POSTWAF에 차단된 요청 재현·검증curl -v -X POST https://ex.com/api -d '{"code":"SELECT ..."}' → 403 확인
grep ModSecurity차단 로그에서 규칙 ID 찾기grep 403 /var/log/nginx/error.log | grep ModSecurity | tail
SecRule … ctl:ruleRemoveByIdModSecurity 예외를 최소 범위로ctl:ruleRemoveTargetById=942100;ARGS:code (파라미터만)
aws wafv2AWS WAF 규칙·규칙그룹 관리aws wafv2 create-rule-group --scope CLOUDFRONT …
aws cloudfront create-invalidationCloudFront 캐시 무효화--distribution-id ID --paths "/static/*"
aws cloudfront list-invalidations무효화 진행 상태 확인--query 'InvalidationList.Items[0].Status' (InProgress→Completed)
aws cloudfront wait invalidation-completed무효화 완료까지 대기배포 스크립트에서 --id <INVALIDATION_ID>
curl … /purge_cacheCloudflare 캐시 무효화--data '{"files":["https://ex.com/app.js"]}' (전체는 purge_everything)
aws s3 sync정적 파일 오리진(S3) 업로드aws s3 sync ./dist s3://bucket/static/ --delete
set_real_ip_from / real_ip_headerCDN 뒤 실제 클라이언트 IP 복원Nginx real_ip_header CF-Connecting-IP; (신뢰 대역만)
nginx -t && systemctl reload nginx예외·설정 문법 검증 후 무중단 반영예외 규칙 추가 뒤 필수

관련 모듈로 더 깊이:

다음 모듈에서는 취약점 조치와 보안 심사 대응 실무 — 정기 보안 감사 절차와 패치 적용을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

WAF에서 정상 요청이 차단될 때(False Positive) 올바른 대응 순서는?

Q2

CDN이 오래된 콘텐츠를 캐시하고 있을 때 해결 방법은?

Q3

WAAP(Web Application and API Protection)가 WAF보다 더 많은 위협을 탐지하는 이유는?

Q4

CDN을 사용할 때 실제 클라이언트 IP를 애플리케이션 서버에서 얻는 방법은?

Q5

WAF가 SQL Injection이나 XSS 같은 웹 공격을 막는 기본 원리는?

Q6

CDN을 두면 원본(origin) 서버 부하와 사용자 응답 지연이 줄어드는 이유는?

Q7

[심화] WAF가 SQL Injection 시그니처를 갖고 있는데도 같은 계열의 공격이 통과했다. 가장 근본적인 이유는?

Q8

[심화] 로그인한 사용자의 개인화된 응답(이름·장바구니 요약)이 다른 사용자 화면에도 보인다는 신고가 들어왔다. CDN을 의심할 때 가장 먼저 확인할 것은?

0 / 8 답변

🧪 실습으로 확인하기

Nginx 설치 및 기동

초급

Linux 서버에 Nginx를 설치하고 systemd 서비스로 등록하여 80포트에서 응답하는 상태까지 만든다.

40📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요