LabHub
배우기 러닝패스 코스

Envoy 내부 구조 · 한 포트가 여러 얼굴을 가질 때 · 이론

체인은 순서가 아니라 구체성으로 이긴다

LabHub 에서 이어서 보기

한 줄 요약

리스너는 '주소와 포트 하나' 이고, 그 안의 필터 체인이 실제로 연결을 받는다. 어느 체인이 받을지는 연결의 성질(SNI · 전송 프로토콜 · ALPN · 출발지)로 정해지며, 그 판정은 여덟 단계의 고정된 순서로 좁혀진다. 설정에 쓴 순서는 아무 상관이 없다.

왜 이게 필요했나

인그레스 게이트웨이 한 대가 도메인 수십 개를 받는다. 도메인마다 인증서가 다르고, 어떤 도메인은 HTTP/2 로 받아야 하고, 내부 헬스 체크는 평문으로 들어온다. 이걸 포트마다 프로세스를 따로 띄워 푸는 방법도 있지만, 그러면 주소를 그만큼 나눠 주어야 하고 설정도 그만큼 흩어진다.

Envoy 는 다른 길을 택했다. 포트는 하나로 두고, 그 포트에 들어온 연결의 성질을 보고 다른 체인으로 보낸다. 그래서 443 하나가 도메인마다 다른 인증서를 내보내고, 같은 포트가 평문 요청도 받을 수 있다.

이 구조를 모르면 두 가지 증상 앞에서 막힌다. 하나는 "인증서를 분명히 넣었는데 이 도메인만 연결이 그냥 끊긴다" 이고, 다른 하나는 "체인을 위로 올렸는데 여전히 아래 것이 받는다" 이다. 둘 다 원인이 같다 — 체인 선택 규칙을 순서로 오해한 것이다.

어떻게 동작하나

연결이 들어오면 리스너 필터가 먼저 돈다. 이 필터들은 아직 바이트를 읽기만 하고 아무것도 바꾸지 않는다. 그중 tls_inspector 가 하는 일이 핵심이다. TLS 핸드셰이크의 첫 메시지(ClientHello)는 평문이라 복호화 없이도 읽을 수 있고, 거기에 SNI(접속하려는 이름)와 ALPN(쓰고 싶은 프로토콜 목록)이 들어 있다. 인스펙터는 그 둘을 꺼내 연결에 붙여 두고, 덤으로 "이 연결은 TLS인가 아닌가" 도 판정해 transport_protocoltls 또는 raw_buffer 로 세운다.

이 필터를 켜지 않으면 server_namesapplication_protocols 조건은 아무 연결에도 맞지 않는다. 설정은 통과하는데 동작만 안 하므로 가장 찾기 어려운 부류의 실수다.

그다음 매칭이 돈다. 공식 문서가 적어 둔 순서는 이렇다.

1. 목적지 포트        2. 목적지 IP3. 서버 이름(SNI)     4. 전송 프로토콜5. 애플리케이션 프로토콜(ALPN)6. 직접 연결된 출발지 IP   7. 출발지 유형8. 출발지 IP          9. 출발지 포트

각 단계에서 가장 구체적으로 맞는 체인만 다음 단계로 넘어간다. 조건을 적지 않은 체인은 '아무거나 받겠다' 는 뜻이라 살아남지만, 조건을 적었는데 틀린 체인은 그 자리에서 탈락한다. 모든 단계를 지나면 남는 체인은 최대 하나임이 보장된다.

여기서 나오는 결론 하나가 실무에서 자주 발을 건다. SNI 조건이 ALPN 조건보다 앞이다. 그래서 server_names: ["a.example.com"] 만 적은 체인과 application_protocols: ["h2"] 만 적은 체인이 있을 때, a.example.com 으로 HTTP/2 요청이 오면 SNI 쪽이 받는다. "h2 는 저쪽 체인으로 보내려고 했는데" 라는 말이 나오는 자리가 바로 여기다.

와일드카드도 구체성으로 비교한다. SNI 가 www.example.com 일 때 우선순위는 www.example.com*.example.com*.com → 조건 없음 순이다.

현장에서 만나는 모습

"이 도메인만 연결이 끊깁니다." 응답 코드가 없다는 것이 단서다. 404 나 503 은 HTTP 계층까지 갔다는 뜻인데, 맞는 체인이 없으면 그 앞에서 연결이 닫히므로 클라이언트에는 TLS 핸드셰이크 실패로만 보인다. 새 도메인의 인증서를 넣으면서 server_names 에 이름을 추가하는 것을 잊으면 정확히 이 모양이 된다.

"체인 순서를 바꿔도 결과가 같습니다." 당연하다. 순서는 매칭에 쓰이지 않는다. 바꿔야 하는 것은 조건의 구체성이다.

평문과 TLS 를 한 포트로 받는 구성. 쿠버네티스 안에서 헬스 체크는 평문으로, 밖에서 오는 트래픽은 TLS 로 받고 싶을 때 transport_protocol: raw_buffer 체인 하나를 더해 해결한다. 포트를 하나 더 열고 방화벽 규칙을 하나 더 만드는 것보다 훨씬 간단하다.

공식 문서: [Listeners](https://www.envoyproxy.io/docs/envoy/v1.38.3/intro/arch_overview/listeners/listeners) · [FilterChainMatch](https://www.envoyproxy.io/docs/envoy/v1.38.3/api-v3/config/listener/v3/listener_components.proto) · [TLS Inspector](https://www.envoyproxy.io/docs/envoy/v1.38.3/configuration/listeners/listener_filters/tls_inspector)

다음 실습에서 할 것

인증서 두 장을 직접 구워 SNI 로 체인을 가르고, 같은 포트에서 평문과 TLS 를 함께 받고, SNI 와 ALPN 이 동시에 맞을 때 무엇이 이기는지를 직접 확인한다. 마지막에는 어느 체인에도 맞지 않는 요청을 보내 '응답이 아니라 침묵' 을 눈으로 본다.