infra
Platform

모듈 맵

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

0 / 52 완료

펼치기
0 / 52 완료0%

Infra-ops · 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 로그 파일에서 차단 기록 확인 (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)은 전 세계 여러 거점(엣지 서버)에 콘텐츠 사본을 분산 저장해, 사용자에게 가장 가까운 서버에서 응답을 제공합니다.

사용자 (서울) → 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 캐시 관리

💡개념

배포 후 캐시 무효화 절차

배포 후 새 파일이 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초 전에 캐시됐습니다.

실습

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

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

grep -E 'BLOCKED|403|ModSecurity' /var/log/nginx/error.log | tail -20
🔍실행 후 확인할 것
  • 로그에서 차단 규칙 ID를 확인할 수 있는가 (ModSecurity: [id "XXXXX"])
  • 차단된 요청의 URI와 파라미터를 로그에서 읽을 수 있는가
  • 같은 규칙 ID가 반복 차단되는 패턴이 있는가 (False Positive 가능성)
  • 차단 이유(msg 필드)에서 어떤 공격 유형으로 분류됐는지 확인했는가
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'
🔍실행 후 확인할 것
  • Cache-Control 헤더에 max-age 값이 있는가 (없으면 CDN이 캐시하지 않을 수 있음)
  • Age 헤더가 0보다 큰가 (캐시 히트 확인)
  • CF-Cache-Status: HIT 또는 X-Cache: Hit 값이 있는가
  • ETag 또는 Last-Modified 헤더가 있는가 (조건부 요청 지원 여부)

트러블슈팅

원인: 배포 파이프라인에 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는 "설치하면 끝"이 아닙니다. 운영 초기에는 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가 찍히고 있는가

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

지식 확인

퀴즈 — 4문제

Q1

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

Q2

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

Q3

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

Q4

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

0 / 4 답변

🧪 실습으로 확인하기

Nginx 설치 및 기동

초급

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

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

이것도 배워보세요

infra-ops중급 · 65
[Infra Ops] 파일/설정/DB 백업과 재해복구 기초
인프라 서비스 운영 트랙 계속
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점