networking Teleport Gateway API TLS Cilium

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 Service

Teleport도 동일한 방식(HTTPRoute)으로 라우팅을 시도했으나, 접속 시 즉시 연결이 끊어졌습니다.

upstream connect error or disconnect/reset before headers.
reset reason: connection termination

원인 분석

Teleport의 TLS 아키텍처

Teleport는 proxyListenerMode: multiplex 설정으로 단일 포트(443)에서 모든 프로토콜을 처리합니다:

프로토콜용도
HTTPSWeb UI, OIDC 콜백
gRPCAuth 서버 통신
SSHtsh CLI 접속
Kubernetes APIkubectl 프록시

이를 위해 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 기본 채널에 포함되지 않아 별도 설치가 필요합니다:

Terminal window
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.2.1/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml

CRD를 런타임에 추가한 경우, Cilium이 새 CRD를 감시하도록 Cilium operator와 agent를 재시작해야 합니다:

Terminal window
kubectl -n kube-system rollout restart deployment cilium-operator
kubectl -n kube-system rollout restart daemonset cilium

Gateway에 TLS Passthrough 리스너 추가

기존 HTTPS Terminate 리스너(포트 443)와 같은 포트에 hostname으로 구분하여 Passthrough 리스너를 추가합니다:

cilium/1.18.2/gateway.yaml
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)
protocolHTTPSTLS
tls.modeTerminatePassthrough
certificateRefs필요 (wildcard 인증서)불필요 (백엔드가 자체 인증서 사용)
hostname미지정 (모든 호스트)teleport.heeho.net (특정 호스트)

같은 포트 443에서 SNI hostname으로 리스너를 분리합니다. teleport.heeho.net으로 들어오는 TLS 연결은 Passthrough 리스너가 처리하고, 나머지는 기존 Terminate 리스너가 처리합니다.

HTTPRoute를 TLSRoute로 교체

기존 HTTPRoute를 삭제하고 TLSRoute를 생성합니다:

tool-set/security/teleport/18.6.4/tlsroute.yaml
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: teleport
namespace: teleport
spec:
parentRefs:
- name: cilium-gateway
namespace: kube-system
sectionName: tls-passthrough # Gateway의 특정 리스너 지정
hostnames:
- "teleport.heeho.net"
rules:
- backendRefs:
- name: teleport
port: 443

sectionName으로 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가 정상 반영됩니다.

관련 콘텐츠

댓글