태그 보관물: AWS

Keycloak LDAPS Fails After an Amazon Linux 2023 Update: Trusting the Right AD Root CA

dnf upgrade 뒤 LDAPS만 실패한 Keycloak

Amazon Linux 2023 서버에 2026년 11월 2일 dnf upgrade를 적용한 뒤, Windows AD와 연동하던 Keycloak LDAPS 연결이 실패한 기록입니다. LDAP(389)로 바꾸면 연결이 정상이었고, 636 포트가 열려 있는 것도 확인한 상태였습니다.

점검하면서 걸린 곳은 두 군데였습니다. OS 신뢰 저장소에 AD의 Root CA가 없었고, 처음 등록한 파일은 CA가 아니라 서버 인증서였습니다. OS에서 검증이 통과한 뒤에도 Keycloak은 자신의 truststore에 Root CA를 넣고 재시작한 뒤에야 연결됐습니다.

업그레이드가 정확히 무엇을 바꿨는지는 확인하지 못했습니다. 그 부분은 글 끝에 따로 적었습니다.

환경

구성 요소 내용
OS Amazon Linux 2023 (2026-11-02 dnf upgrade 적용)
Keycloak 26.3.3, 서버에 직접 설치(컨테이너 아님)
LDAP 서버 Windows Active Directory, AD의 사설 CA(example-DC01-CA)가 발급한 서버 인증서
Keycloak LDAP 설정 ldaps://DC01.example.local:636, StartTLS Off, Use Truststore SPI Always

포트가 열려 있어도 LDAPS는 따로 확인해야 하는 이유

389는 평문 LDAP입니다. LDAPS(636)는 연결 직후 TLS 핸드셰이크와 인증서 검증을 한 번 더 거칩니다. TCP 연결과 389 동작은 그 앞 단계까지만 보증합니다.

Keycloak 설정을 건드리기 전에, Keycloak이 동작하는 서버에서 OpenSSL로 TLS 협상과 인증서 검증을 나눠 보았습니다.

1. OpenSSL 첫 검증: 협상은 성공, 검증은 오류 21

openssl s_client \
  -connect dc01.example.local:636 \
  -servername dc01.example.local \
  -showcerts </dev/null
# 출력 일부 발췌
verify error:num=20:unable to get local issuer certificate
verify error:num=21:unable to verify the first certificate
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Verify return code: 21 (unable to verify the first certificate)

TLS 1.3과 TLS_AES_256_GCM_SHA384 협상은 성공했으므로 프로토콜이나 cipher 호환성 문제는 아니었습니다. 서버 인증서는 CN=DC01.example.local이고 발급자는 사설 CA인 example-DC01-CA였습니다. 2026-09-24부터 2027-09-24까지인 유효기간도 문제가 없었습니다.

AD는 체인 없이 서버 인증서 한 장만 보내고 있었습니다. Keycloak 서버가 발급자 CA를 미리 알고 있지 않으면 검증을 끝낼 수 없는 구조입니다.

2. 이름은 ca.cer인데 서버 인증서였다

AD에서 내보낸 파일 두 개 중 example-dc01-ca.cer를 먼저 OS 신뢰 저장소에 넣었습니다. update-ca-trust extract를 실행한 뒤에도 오류 20(unable to get local issuer certificate)은 그대로였습니다. 두 파일의 subject와 issuer를 비교하자 이유가 드러났습니다.

$ openssl x509 -in example-dc01-ca.cer -noout -subject -issuer
subject=CN=DC01.example.local
issuer=DC=local, DC=example, CN=example-DC01-CA

$ openssl x509 -in example-dc01-root.cer -noout -subject -issuer
subject=DC=local, DC=example, CN=example-DC01-CA
issuer=DC=local, DC=example, CN=example-DC01-CA

example-dc01-ca.cer는 이름과 달리 DC01의 서버 인증서였습니다. 실제 CA는 subject와 issuer가 같은 자체 서명 인증서, 즉 example-dc01-root.cer 쪽이었습니다. 신뢰 저장소에는 서버 인증서가 아니라 그것을 발급한 CA를 넣어야 합니다.

CA 파일을 등록하기 전에 두 가지를 확인해 두면 같은 혼동을 피할 수 있습니다. Basic Constraints가 CA:TRUE인지 보고, 그 CA로 서버 인증서가 실제로 검증되는지 시험합니다.

openssl x509 -in example-dc01-root.cer -noout -text | grep -A1 'Basic Constraints'
openssl verify -CAfile example-dc01-root.cer example-dc01-ca.cer

3. Root CA를 넣자 OS 검증은 통과

sudo cp example-dc01-root.cer /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract

openssl s_client \
  -connect dc01.example.local:636 \
  -servername dc01.example.local \
  -verify_hostname dc01.example.local \
  -verify_return_error </dev/null
# 출력 일부 발췌
Verified peername: DC01.example.local
Verify return code: 0 (ok)

OpenSSL 기준으로는 인증서 체인과 호스트명 검증이 모두 통과했습니다. 앞서 잘못 넣은 서버 인증서 파일은 anchors 디렉터리에서 빼 두는 편이 나중에 덜 헷갈립니다.

4. OS는 통과했는데 Keycloak은 여전히 실패

Keycloak 관리 콘솔의 User Federation(LDAP) 화면에서 Test connection과 Test authentication이 모두 실패했습니다. Keycloak은 Java로 동작하므로 OpenSSL과 같은 저장소를 보지 않습니다.

Keycloak 공식 문서에 따르면 Keycloak은 Java 기본 truststore의 인증서를 항상 신뢰합니다. 추가 인증서는 conf/truststores 디렉터리나 truststore-paths 옵션으로 넣습니다. JDK 패키지에 따라 Java 기본 truststore(cacerts)가 OS 신뢰 저장소와 별개의 파일일 수 있어서, OS 저장소를 고쳐도 JVM에 반영된다는 보장이 없습니다.

Keycloak이 쓰는 Java의 cacerts가 어디를 가리키는지는 아래처럼 확인할 수 있습니다. 서비스가 별도의 JAVA_HOME을 쓴다면 그 경로의 java로 바꿔서 봐야 합니다.

JAVA_BIN=$(readlink -f "$(command -v java)")
readlink -f "$(dirname "$JAVA_BIN")/../lib/security/cacerts"

결과가 /etc/pki/ 아래를 가리키면 OS 저장소를 따르는 것이고, JDK 디렉터리 안의 파일이면 별도 저장소입니다. JVM은 시작할 때 truststore를 읽으므로 어느 쪽을 바꾸든 Keycloak을 재시작해야 반영됩니다.

Keycloak LDAPS 복구 과정 개념도: OpenSSL 검증 오류 21과 20을 거쳐 Root CA 등록으로 OS 검증이 통과한 뒤, Keycloak truststore 등록과 재시작으로 LDAP 연동이 복구되기까지의 단계와 세 신뢰 저장소의 역할
Keycloak LDAPS 연결이 복구되기까지 거친 네 단계와, OS·JVM·Keycloak 신뢰 저장소가 각각 무엇을 검증하는지 정리한 그림입니다.

5. Keycloak truststore에 Root CA를 등록하고 재시작

Keycloak 26.3.3은 Quarkus 기반이라 추가로 신뢰할 인증서를 Keycloak 설정으로 관리합니다. 등록 방법은 두 가지이고, 이번에는 둘 다 적용했습니다. Root CA를 conf/truststores에 PEM으로 두고, keycloak.conf의 truststore-paths에도 같은 파일을 지정한 뒤 재시작했습니다. LDAPS 연결은 그 뒤에 복구됐습니다.

# 1) conf/truststores 디렉터리에 PEM으로 두기 (설치 경로는 예시)
sudo mkdir -p /opt/keycloak/conf/truststores
sudo cp example-dc01-root.cer /opt/keycloak/conf/truststores/example-dc01-root.pem
# 2) keycloak.conf에도 같은 파일을 지정
truststore-paths=/opt/keycloak/conf/truststores/example-dc01-root.pem
# 3) 두 설정을 반영하도록 재시작 (서비스 이름은 예시)
sudo systemctl restart keycloak

문서에 적힌 형식은 PEM과 PKCS12(.p12, .pfx, .pkcs12)입니다. 확장자가 .cer여도 내용이 PEM이면 그대로 쓸 수 있습니다. DER 형식이라면 openssl x509 -inform DER -in example-dc01-root.cer -out example-dc01-root.pem으로 바꿔 둡니다.

이미 truststore-paths를 쓰고 있다면 기존 값을 덮어쓰지 말고 경로를 추가해야 합니다. 서비스 이름과 설치 경로는 환경에 맞게 바꿔야 합니다.

두 방법을 함께 적용했기 때문에 어느 쪽이 결정적이었는지는 구분되지 않습니다. Keycloak 문서에 따르면 conf/truststores에 둔 인증서도 신뢰 대상이 됩니다. 그러니 같은 파일을 truststore-paths로 다시 지정한 것은 중복일 수 있습니다. 한쪽만 남겨도 연결이 유지되는지는 다음 점검 때 확인해 볼 만합니다.

업그레이드가 무엇을 바꿨는지는 확인하지 못했다

장애는 dnf upgrade 직후에 시작됐지만, 그 트랜잭션에 어떤 패키지가 들어 있었는지는 확인하지 않았습니다. 그래서 업그레이드가 문제를 만들었는지, 원래 있던 문제를 드러냈을 뿐인지 구분할 수 없습니다. 생각해 볼 수 있는 경로는 아래와 같고, 어느 쪽도 이번에 검증하지는 않았습니다.

  • 예전에 keytool로 JDK의 cacerts에 직접 넣어 둔 CA가, JDK 패키지 업데이트로 파일이 교체되면서 사라졌을 수 있습니다.
  • Keycloak이 업그레이드 과정에서 재시작되면서, 그동안 메모리에 남아 있던 예전 truststore 대신 바뀐 파일을 새로 읽었을 수 있습니다.

AWS 보안 공지(CVE-2026-113470)에는 AL2023의 ca-certificates에서 eMudhra 루트를 제거하는 조치가 올라 있었습니다. 이번 문제는 사설 AD CA에 대한 신뢰였으므로 공개 CA 목록 변경과는 직접 관계가 없습니다. 다음에 같은 일이 생기면 업그레이드 트랜잭션부터 확인하려 합니다.

sudo dnf history
sudo dnf history info <ID>
rpm -qa | grep -Ei 'corretto|openjdk|ca-certificates|openssl'

정리

  • 포트와 389는 LDAPS를 보증하지 않습니다. OpenSSL로 TLS 협상과 인증서 검증을 나눠 보면, 원인이 프로토콜인지 신뢰 체인인지 바로 갈립니다.
  • 파일 이름보다 subject와 issuer를 봅니다. Root CA는 subject와 issuer가 같은 자체 서명 인증서입니다.
  • OS, JVM, Keycloak의 신뢰 저장소는 서로 다릅니다. Keycloak LDAPS 연결을 위해서는 Keycloak truststore에 Root CA를 두고 재시작해야 했습니다. Keycloak을 새 버전으로 옮길 때는 conf/truststores도 함께 옮겨야 합니다.
  • 389로 돌아가거나 검증을 끄는 것은 해결이 아닙니다. StartTLS 없이 389를 쓰면 바인드 계정의 비밀번호가 평문으로 오갑니다.

참고

관련 포스트:

참고 문서: Keycloak Configuring trusted certificates · OpenSSL s_client · AWS ALAS CVE-2026-113470

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 구성