카테고리 보관물: Cloud

CloudFront + Legacy Origin 연동

origin 직접 접근을 막아야 했던 이유

AWS CloudFront와 HAProxy를 활용한 CloudFront Legacy Origin 연동 구성 및 트러블슈팅을 정리합니다. legacy 서버를 보호하기 위해 CloudFront와 WAF 레이어를 구성하며 발생한 다양한 이슈를 기록한 내용입니다.
도메인 변경과 HAProxy를 origin으로 하는 구조에서 모든 트래픽이 반드시 CloudFront를 경유하도록 강제하고, origin의 직접 접근을 차단하는 것이 주요 목표입니다.

본문은 CloudFront 구성이 주요 목적이 아니므로 전체적인 과정은 생략하고 Legacy Origin 연동 과정만 다루도록 하겠습니다.

아키텍처

전체 트래픽 흐름은 다음과 같습니다.

Browser (HTTPS)
    → CloudFront (WAF, CDN, TLS 종료)
        → HAProxy :80 (X-CF-Secret 검증)
            → Apache :443

WAF bypass 방지 원리: CloudFront는 모든 origin 요청에 X-CF-Secret custom origin header를 추가합니다. HAProxy는 www.example.com로 들어오는 요청 중 이 헤더가 없는 경우 403을 반환합니다. 브라우저가 HAProxy IP로 직접 접근하더라도 CloudFront를 거치지 않으면 403으로 차단됩니다.

CloudFront Legacy Origin 연동 구성도로, CloudFront가 X-CF-Secret 헤더를 붙인 요청만 HAProxy를 지나 Apache origin에 도달하고 헤더 없는 직접 접근은 403으로 차단되는 흐름과 경로별 캐시 정책을 보여 줍니다.
HAProxy는 CloudFront가 붙인 X-CF-Secret 헤더로 경유 여부를 판별하고, CloudFront는 경로별로 캐시 정책을 나눕니다.

HAProxy 핵심 설정

frontend http_front
    bind *:80
    acl is_cf_www    hdr(host) -i www.example.com
    acl has_cf_secret hdr(X-CF-Secret) -i <SECRET_VALUE>

    # CloudFront 아닌 직접 접근 차단
    http-request deny deny_status 403 if is_cf_www !has_cf_secret

    # CF 경유 표시 후 backend 라우팅
    http-request set-var(req.cf_routed) str(yes) if is_cf_www has_cf_secret
    http-request set-header X-Forwarded-Proto https if is_cf_www has_cf_secret
    http-request redirect scheme https code 301 unless { var(req.cf_routed) -m found }
    use_backend www.example.com if { var(req.cf_routed) -m found }

frontend https_front
    bind *:443 ssl crt /etc/letsencrypt/live/complex_dev.pem
    acl is_www.example.com hdr(host) -i www.example.com
    acl is_www.example.com hdr(host) -i <MY_IP>
    acl has_cf_secret hdr(X-CF-Secret) -i <SECRET_VALUE>
    http-request deny deny_status 403 if is_www.example.com !has_cf_secret
    use_backend www.example.com if is_www.example.com

주의: HAProxy의 redirect 지시어는 use_backend보다 항상 먼저 처리됩니다. CloudFront 트래픽을 판별한 뒤 리다이렉트를 선택적으로 건너뛰려면 http-request redirect와 set-var를 조합해야 합니다.

CloudFront Behaviors 구성

우선순위 Path pattern Cache policy Origin request policy
0 /wp-admin/* CachingDisabled AllViewer
1 /wp-login.php CachingDisabled AllViewer
2 (Default) * UseOriginCacheControlHeaders-QueryStrings –

/wp-admin/*와 /wp-login.php는 세션·쿠키 의존 동적 페이지이므로 캐시하지 않고 AllViewer로 모든 요청 정보를 origin에 전달합니다. Default behavior는 공개 페이지 캐시에 origin의 Cache-Control 헤더를 그대로 사용합니다.

CloudFront 경유 후 깨진 네 가지

1. 로그인 페이지 스타일 깨짐 — CloudFront 쿼리스트링 미전달

현상: /wp-login.php 등의 페이지가 스타일 미적용 상태로 표시됨.

원인: CloudFront의 Behavior 기본 Cache policy는 UseOriginCacheControlHeaders는 쿼리스트링을 cache key에 포함하지 않고 origin에도 전달하지 않습니다. WordPress의 load-styles.php?c=0&dir=ltr&load[]=...가 파라미터 없이 호출되어 0바이트 빈 CSS를 반환했고, CloudFront가 이를 max-age=31536000(1년) TTL로 캐시했습니다.

# 진단: 쿼리스트링 유무에 따른 응답 차이

curl -s -o /dev/null -w "%{size_download}"   "http://<origin>/wp-admin/load-styles.php"
# → 0 bytes (파라미터 없으면 빈 응답)

curl -s -o /dev/null -w "%{size_download}"   "http://<origin>/wp-admin/load-styles.php?c=0&dir=ltr&load[chunk_0]=login,..."
# → 110,881 bytes (정상 CSS)

해결: Cache policy를 UseOriginCacheControlHeaders-QueryStrings로 변경하고 CloudFront invalidation(/*) 실행. 이후 정상 표시 확인.

2. 캐시 플러그인 충돌 — W3 Total Cache drop-in 잔존

현상: admin-ajax.php 500 오류, CSS 응답 0바이트.

원인: W3 Total Cache 플러그인 삭제 후에도 WordPress drop-in 파일 /wp-content/object-cache.php가 남아 모든 object cache 요청을 가로채 오류를 반환.

# 오류 메시지
W3 Total Cache Error: some files appear to be missing or out of place.
Please re-install plugin or remove /var/www/html/wp-content/object-cache.php.

# 해결
rm /var/www/html/wp-content/object-cache.php

교훈: 캐시 플러그인 삭제 시 drop-in 파일(object-cache.php, advanced-cache.php, db.php)은 자동 삭제되지 않습니다. CloudFront 등 외부 CDN을 사용하는 경우 서버 사이드 캐시 플러그인은 제거하는 것이 좋습니다.

3. 배경 이미지 미표시 — PHP 직렬화 데이터 손상

현상: 메인 페이지 배경 이미지가 외부에서 표시되지 않음.

원인 1: DB의 theme_mods_twentyfourteen에 배경 이미지 URL이 구 도메인(www.sierracloud.dev)으로 저장.

원인 2: SQL REPLACE()로 URL을 교체했으나 PHP 직렬화 문자열의 길이 prefix(s:N:)가 업데이트되지 않아 unserialize 실패 → 테마 설정 전체 소실.

-- 잘못된 방법 (직렬화 길이 불일치 발생)
UPDATE sc_options
SET option_value = REPLACE(option_value,
  'https://www.sierracloud.dev',
  'https://www.example.com')
WHERE option_name = 'theme_mods_twentyfourteen';
-- s:79:"https://www.example.com/..." → 실제 길이는 76 → unserialize 실패

-- 올바른 방법: 전체 직렬화 값을 정확히 재구성하여 UPDATE
UPDATE sc_options
SET option_value = 'a:10:{...s:16:"background_image";s:76:"https://www.example.com/wp-content/uploads/.../image.jpg";}'
WHERE option_name = 'theme_mods_twentyfourteen';

교훈: WordPress DB의 PHP 직렬화 데이터에 SQL REPLACE()를 사용하면 문자열 길이 prefix가 맞지 않아 반드시 손상됩니다. URL 교체 작업에는 wp search-replace CLI를 사용하세요.

4. HTML 30일 캐시 — .htaccess mod_expires 설정

현상: CloudFront가 메인 페이지 HTML을 30일간 캐시. 콘텐츠 업데이트 미반영.

# 수정 전
ExpiresByType text/html "access 1 month"
ExpiresDefault "access 1 month"

# 수정 후
ExpiresByType text/html "access 0 seconds"
ExpiresDefault "access 1 day"

구성 수정 요약

항목 설정
CloudFront → Origin 프로토콜 HTTP (port 80)
WAF bypass 방지 X-CF-Secret custom origin header 검증
기본 Cache policy UseOriginCacheControlHeaders-QueryStrings
wp-admin / wp-login 처리 CachingDisabled + AllViewer
HTML Cache-Control max-age=0 (캐시 제외)
정적 자산 Cache-Control max-age=31536000 (WordPress 자동 버전 관리)

참고

관련 포스트:

참고 문서: CloudFront Custom Origin Headers (AWS 공식) · HAProxy ACL 설정 가이드

Inspecting Direct Connect Traffic with AWS GWLB: Asymmetric AZ Drops and a Lambda Failover

레거시 IPS를 대신할 검사 지점이 필요했던 상황

IDC와 AWS가 DX(Direct Connect)로 연결된 하이브리드 환경에서 AWS GWLB 기반의 IPS 인라인 검사를 DX 구간에 넣은 기록입니다. GWLB 엔드포인트가 한 AZ에만 놓이는 특성 때문에 가용성과 비대칭 트래픽 문제를 차례로 겪었습니다. EventBridge와 Lambda로 라우팅 테이블의 다음 홉을 바꾸는 방식으로 마무리했습니다.

기존에 쓰던 레거시 IPS가 수명을 다하면서, 보안 요건상 AWS 쪽에 IPS를 새로 구성해야 했습니다. DX를 거쳐 오가는 트래픽을 빠짐없이 검사하는 것이 목표였습니다.

AWS에서 서드파티 보안 어플라이언스를 트래픽 경로에 끼워 넣는 관리형 수단이 GWLB(Gateway Load Balancer)입니다. GWLB는 원래 패킷을 Geneve(UDP 6081)로 감싸 어플라이언스에 넘기고, 검사를 마친 패킷을 돌려받아 원래 경로로 보냅니다. Cisco IPS를 새로 도입하는 만큼 Geneve 지원 여부부터 확인했고, 지원 사양임을 확인한 뒤 설계에 들어갔습니다.

기존 네트워크를 건드리지 않는 배치

기존 네트워크 구조를 크게 바꾸면 여러 계정의 VPC에 영향이 갈 수 있었습니다. TGW 같은 공용 구간은 형상 변경을 최소화하고, 변경은 DX 진입 구간에 모았습니다.

  • 진입 구간 VPC: IDC에서 DX를 거쳐 AWS로 들어오는 구간의 VPC에 GWLB 엔드포인트를 두었습니다.
  • Inspection VPC: 별도 VPC를 만들어 GWLB와 GWLB 엔드포인트 서비스를 두고, 그 뒤에 IPS를 연결했습니다.
  • 라우팅: 진입 구간 VPC의 라우팅 테이블에서 다음 홉을 GWLB 엔드포인트로 지정했습니다. GWLB 엔드포인트가 있는 서브넷의 라우팅 테이블은 외부로 나가는 기존 경로를 그대로 유지했습니다.

이렇게 하면 DX를 거치는 인바운드·아웃바운드 패킷이 모두 GWLB 엔드포인트로 넘어가고, GWLB를 거쳐 IPS에서 검사됩니다. IPS 검사를 마친 패킷은 엔드포인트가 있는 서브넷으로 되돌아와 원래대로 라우팅됩니다.

AWS GWLB 검사 구성도: IDC에서 DX로 들어온 트래픽이 진입 구간 VPC의 GWLB 엔드포인트를 거쳐 Inspection VPC의 Cisco IPS에서 검사된 뒤 기존 경로로 돌아가는 구조
DX 진입 구간 VPC에 GWLB 엔드포인트를 두고, 별도 Inspection VPC에서 IPS로 검사하는 AWS GWLB 배치입니다.

AWS GWLB 엔드포인트가 한 AZ에만 놓이는 문제

ALB나 NLB는 구성할 때 여러 AZ를 함께 선택해 가용성을 확보할 수 있습니다. GWLB 엔드포인트는 그와 달리 서브넷 하나, 즉 AZ 하나에만 만들어집니다.

라우팅 테이블의 한 경로에는 다음 홉을 하나만 지정할 수 있습니다. GWLB 엔드포인트를 다음 홉으로 지정하면 모든 트래픽이 그 엔드포인트의 AZ 하나로만 흐릅니다. AZ 하나에 문제가 생기면 DX 구간 전체가 영향을 받는 구조였습니다.

AZ별 라우팅 분리가 부른 비대칭 드롭

첫 번째 대응은 라우팅 테이블을 AZ별로 나누고, AZ마다 GWLB 엔드포인트를 만들어 각각 지정하는 방식이었습니다. AZ 하나가 빠져도 이론상으로는 나머지 AZ가 트래픽을 받습니다.

그런데 왕복 경로가 서로 다른 AZ를 지나는 경우가 생겼습니다. C존에서 출발한 연결은 C존 엔드포인트를 거쳐 DX로 나가고, IDC에서 SYN을 처리합니다. SYN/ACK가 A존으로 돌아오면 C존에서 SYN을 본 IPS와는 다른 경로로 검사됩니다. A존 경로의 IPS 세션 테이블에는 이 연결의 SYN 기록이 없으므로, 비대칭 트래픽으로 판단해 드롭했습니다.

상태 기반(stateful) 검사 장비는 한 연결의 양방향 패킷을 같은 장비가 봐야 합니다. AZ별로 경로를 나누면 이 전제가 깨지므로, AZ별 라우팅 분리는 이 구성에 맞지 않는 방법이었습니다.

AWS GWLB 엔드포인트를 AZ별로 나눴을 때 C존에서 나간 SYN의 응답이 A존으로 돌아와 IPS가 드롭하는 비대칭 흐름과 EventBridge·Lambda 다음 홉 전환 보완 개념도
AZ별 경로 분리가 상태 기반 IPS의 비대칭 드롭을 부른 흐름과, 엔드포인트 하나를 유지하며 장애 때만 다음 홉을 바꾸는 보완 방식입니다.

EventBridge와 Lambda로 만든 다음 홉 전환

그래서 가용성은 라우팅 분리 대신 자동화로 확보했습니다. AZ 단위의 트래픽 이상을 감시하는 EventBridge 규칙을 만들었습니다. EventBridge가 이상을 감지하면 트리거용 Lambda가 실행됩니다. Lambda는 다시 라우팅 테이블의 다음 홉을 다른 AZ의 GWLB 엔드포인트로 바꾸는 Lambda를 호출해 경로를 전환합니다.

평소에는 모든 트래픽이 엔드포인트 하나와 같은 IPS 경로를 지나므로 대칭이 유지됩니다. AZ에 이상이 생길 때만 경로가 통째로 옮겨 갑니다. AZ를 전환하는 순간 진행 중이던 연결은 새 경로의 IPS에 세션 기록이 없으므로 끊길 수 있다는 점도 감안해야 합니다.

지금 다시 설계한다면 먼저 볼 AWS 기능

이 구성은 당시 요건과 제약 안에서 고른 방법입니다. 같은 문제를 다시 다룬다면 아래 기능이 구조에 맞는지 먼저 확인해 볼 만합니다.

  • VGW 인그레스 라우팅의 GWLB 엔드포인트 지정: 2023년 8월부터 가상 프라이빗 게이트웨이(VGW) 라우팅 테이블에서 GWLB 엔드포인트를 다음 홉으로 지정할 수 있습니다. DX나 Site-to-Site VPN으로 들어오는 트래픽을 VGW 경계에서 바로 검사 경로로 보낼 수 있습니다.
  • TGW appliance mode: 트래픽이 TGW의 VPC 연결을 거쳐 검사 VPC로 간다면 이 기능을 먼저 봅니다. TGW가 한 연결의 왕복 트래픽을 같은 AZ의 연결로 보내 비대칭을 막아 줍니다.
  • GWLB 교차 영역 부하 분산: 기본값은 꺼져 있고, 켜면 다른 AZ의 어플라이언스로도 트래픽을 보낼 수 있습니다. AZ 하나에 엔드포인트가 놓이는 제약은 그래도 남습니다.

정리

GWLB는 패킷 검사 어플라이언스를 AWS 트래픽 경로에 넣으려면 사실상 필수인 구성 요소입니다. IPS 검사를 우회하거나 루프를 도는 트래픽이 라우팅 변경 하나로도 생길 수 있어, 흐름 전체를 신중하게 검토해야 했습니다.

이번 AWS GWLB 구성에서 얻은 가장 큰 교훈은 대칭성입니다. IPS 같은 상태 기반 장비를 쓰는 한, 가용성을 위해 경로를 나누는 순간 왕복 경로가 갈라질 수 있습니다. AZ 이중화 같은 가용성 설계는 대칭을 깨지 않는 범위 안에서 해야 했습니다.

참고

관련 포스트:

참고 문서: AWS Gateway Load Balancer 개요 · GWLB 속성(교차 영역 부하 분산) · VGW 인그레스 라우팅의 GWLB 지원 · GWLB와 TGW 중앙 검사 아키텍처(appliance mode) · Cisco Threat Defense의 GWLB·Geneve 구성