Teleport TLS Passthrough — Gateway API에서 자체 TLS 서비스 라우팅
Teleport의 자체 TLS 인증서 요구사항을 Gateway API TLSRoute로 해결한 과정을 정리합니다.
배경
Homelab Kubernetes 클러스터에서 Keycloak → Authentik IdP 전환 작업 중, Teleport를 신규 배포하고 Gateway API(Cilium)를 통해 외부에 노출해야 했습니다.
기존 모든 서비스(Grafana, Mattermost 등)는 Cilium Gateway의 HTTPS Terminate 리스너를 통해 라우팅되고 있었습니다:
Client → TLS → Gateway (TLS 종료) → HTTP → Backend ServiceTeleport도 동일한 방식(HTTPRoute)으로 라우팅을 시도했으나, 접속 시 즉시 연결이 끊어졌습니다.
upstream connect error or disconnect/reset before headers.reset reason: connection termination원인 분석
Teleport의 TLS 아키텍처
Teleport는 proxyListenerMode: multiplex 설정으로 단일 포트(443)에서 모든 프로토콜을 처리합니다:
| 프로토콜 | 용도 |
|---|---|
| HTTPS | Web UI, OIDC 콜백 |
| gRPC | Auth 서버 통신 |
| SSH | tsh CLI 접속 |
| Kubernetes API | kubectl 프록시 |
이를 위해 Teleport Proxy는 자체 TLS를 반드시 유지합니다. 클라이언트의 TLS ClientHello에서 ALPN(Application-Layer Protocol Negotiation)을 분석하여 프로토콜을 식별하기 때문입니다.
HTTPRoute의 한계
Cilium Gateway의 HTTPS 리스너는 TLS Terminate 모드로 동작합니다:
Client --TLS--> Gateway (TLS 종료, 인증서: wildcard.heeho.net) | +--HTTP--> Backend (평문)Gateway가 TLS를 종료한 후 백엔드에 평문 HTTP로 전달하지만, Teleport 백엔드는 TLS 연결만 수락합니다. Gateway가 평문 HTTP로 연결을 시도하면 Teleport가 즉시 연결을 끊습니다.
Gateway --HTTP--> Teleport:3080 (TLS 기대) → connection reset이것이 reset reason: connection termination 에러의 근본 원인입니다.
시도한 접근법과 실패 이유
LoadBalancer 서비스로 전환: Gateway를 우회하여 Teleport에 직접 접근하는 방법입니다. LoadBalancer가 내부 IP(172.30.1.8)를 할당받지만, 외부에서 접근하려면 공유기에 별도 포트 포워딩이 필요합니다. 기존 인프라 구조(모든 서비스가 Gateway 단일 진입점)를 깨뜨리는 방식이라 부적합했습니다.
proxyListenerMode: separate로 변경: Teleport가 프로토콜별로 별도 포트를 열어 웹 UI만 HTTP로 서빙하는 방식입니다. 그러나 separate 모드에서도 웹 포트(3080)는 여전히 TLS를 사용합니다. Teleport는 설계상 평문 HTTP 리스너를 제공하지 않습니다.
해결
Gateway API TLS Passthrough
Gateway API 스펙은 TLS를 종료하지 않고 그대로 백엔드에 전달하는 Passthrough 모드를 지원합니다:
Client --TLS--> Gateway (TLS 미종료, SNI만 확인) --TLS--> Backend[!NOTE] **SNI(Server Name Indication)**는 TLS 핸드셰이크의 ClientHello 메시지에 포함되는 확장 필드로, 클라이언트가 접속하려는 호스트명을 평문으로 전달합니다. TLS 암호화가 시작되기 전에 전송되므로, Gateway가 TLS를 복호화하지 않고도 목적지를 판별할 수 있습니다.
Gateway는 이 SNI만 읽어서 라우팅하고, TLS 세션을 그대로 백엔드에 전달합니다. 이렇게 하면:
- Teleport가 직접 TLS를 처리하여 ALPN 기반 프로토콜 멀티플렉싱 유지
- 클라이언트 ↔ Teleport 간 End-to-End 암호화 보존
- tsh CLI, 웹 UI, SSH 등 모든 프로토콜이 단일 포트에서 정상 동작
TLSRoute CRD 설치
Cilium은 TLSRoute를 지원하지만, CRD가 Gateway API 기본 채널에 포함되지 않아 별도 설치가 필요합니다:
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.1/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yamlCRD를 런타임에 추가한 경우, Cilium이 새 CRD를 감시하도록 Cilium operator와 agent를 재시작해야 합니다:
kubectl -n kube-system rollout restart deployment cilium-operatorkubectl -n kube-system rollout restart daemonset ciliumGateway에 TLS Passthrough 리스너 추가
기존 HTTPS Terminate 리스너(포트 443)와 같은 포트에 hostname으로 구분하여 Passthrough 리스너를 추가합니다:
spec: listeners: # 기존 — 일반 서비스용 (TLS 종료) - name: https protocol: HTTPS port: 443 tls: mode: Terminate certificateRefs: - kind: Secret name: wildcard.heeho.net namespace: cilium-secrets allowedRoutes: namespaces: from: All
# 추가 — Teleport용 (TLS Passthrough) - name: tls-passthrough protocol: TLS port: 443 hostname: "teleport.heeho.net" tls: mode: Passthrough allowedRoutes: namespaces: from: All핵심 차이:
| 설정 | HTTPS (Terminate) | TLS (Passthrough) |
|---|---|---|
protocol | HTTPS | TLS |
tls.mode | Terminate | Passthrough |
certificateRefs | 필요 (wildcard 인증서) | 불필요 (백엔드가 자체 인증서 사용) |
hostname | 미지정 (모든 호스트) | teleport.heeho.net (특정 호스트) |
같은 포트 443에서 SNI hostname으로 리스너를 분리합니다. teleport.heeho.net으로 들어오는 TLS 연결은 Passthrough 리스너가 처리하고, 나머지는 기존 Terminate 리스너가 처리합니다.
HTTPRoute를 TLSRoute로 교체
기존 HTTPRoute를 삭제하고 TLSRoute를 생성합니다:
apiVersion: gateway.networking.k8s.io/v1alpha2kind: TLSRoutemetadata: name: teleport namespace: teleportspec: parentRefs: - name: cilium-gateway namespace: kube-system sectionName: tls-passthrough # Gateway의 특정 리스너 지정 hostnames: - "teleport.heeho.net" rules: - backendRefs: - name: teleport port: 443sectionName으로 Gateway의 tls-passthrough 리스너에 명시적으로 바인딩합니다.
결과
Gateway 리스너 상태를 확인하면 tls-passthrough 리스너가 정상적으로 프로그래밍되어 있습니다:
# Gateway 리스너 상태 확인kubectl get gateway cilium-gateway -n kube-system -o json | jq '.status.listeners[]'
# tls-passthrough 리스너: Programmed=True, attachedRoutes=1트래픽 흐름 (변경 전 vs 후):
[변경 전 — 실패]Client --TLS--> Gateway (TLS 종료) --HTTP--> Teleport:3080 → connection reset
[변경 후 — 성공]Client --TLS--> Gateway (SNI 확인만) --TLS--> Teleport:3080 → 정상 처리변경 후 검증한 항목들입니다:
- Web UI: 브라우저에서
teleport.heeho.net접속 시 로그인 페이지 정상 렌더링 - tsh CLI:
tsh login --proxy=teleport.heeho.net으로 SSH 세션 정상 수립 - 인증서: 브라우저 인증서 정보에서 Gateway의 wildcard가 아닌 Teleport 자체 발급 인증서가 표시되는 것을 확인 — TLS가 종료되지 않고 Passthrough된 증거입니다
교훈
Gateway API는 TLS Passthrough를 표준화했습니다. Ingress에서는 TLS Passthrough가 구현체에 종속되어 있었습니다. nginx-ingress는 nginx.ingress.kubernetes.io/ssl-passthrough: "true" 어노테이션, Traefik은 IngressRoute CRD의 tls.passthrough: true — 구현체를 바꾸면 설정을 다시 써야 합니다. Gateway API의 TLSRoute는 이를 표준 스펙으로 통합했습니다. “HTTPRoute로 안 되면 불가능하다”고 판단하기 전에 TLSRoute, TCPRoute 등 다른 Route 타입을 검토해야 합니다.
자체 TLS를 사용하는 서비스는 Passthrough가 필수입니다. ALPN 멀티플렉싱, mTLS 인증 등 백엔드가 TLS 세션을 직접 제어해야 하는 서비스를 Gateway 뒤에 배치할 때는 처음부터 TLS Passthrough를 고려해야 합니다.
CRD 런타임 추가 시 컨트롤러 재시작이 필요합니다. TLSRoute CRD를 설치한 후 Cilium이 이를 감시하려면 operator와 agent를 재시작해야 합니다. CRD 추가 → 컨트롤러 재시작 → Route 생성 순서를 지켜야 attachedRoutes가 정상 반영됩니다.
관련 콘텐츠
Gateway API, Ingress를 대체하는 Kubernetes 표준
SIG-Network이 4년에 걸쳐 만든 Gateway API의 핵심 리소스 3가지와 그 관계를 이해하고, Ingress와 무엇이 달라졌는지 정리한다.
KubernetesNGINX Ingress EOL 대응: OCI에서 Envoy Gateway로 마이그레이션
NGINX Ingress Controller 지원 종료에 대비해 OCI Always Free 클러스터에서 Envoy Gateway로 마이그레이션한 경험을 공유합니다.
KubernetesGateway API 전환기 (1) - Cilium을 Kubespray에서 Helm으로
Kubespray로 설치한 Cilium을 Helm 관리로 전환하는 과정에서 겪은 트러블슈팅과 교훈을 공유합니다.