K8s Gateway API TLS Passthrough란?
TLS Passthrough는 게이트웨이가 TLS를 종료(terminate)하지 않고 암호화된 트래픽을 백엔드 Pod까지 그대로 전달하는 라우팅 모드입니다. Gateway API의 TLSRoute 리소스로 구현하며, 백엔드 서비스가 자체 인증서로 직접 TLS 핸드셰이크를 수행합니다. 금융·의료 등 엔드투엔드 암호화가 필수인 환경에서 핵심 패턴입니다.
TLS 모드 비교
| 모드 | TLS 종료 위치 | 게이트웨이 역할 | Route 리소스 |
|---|---|---|---|
| Terminate | 게이트웨이 | 복호화 → HTTP 라우팅 | HTTPRoute |
| Passthrough | 백엔드 Pod | SNI 기반 L4 라우팅 | TLSRoute |
| Re-encrypt | 게이트웨이 + 백엔드 | 복호화 → 재암호화 | HTTPRoute + BackendTLSPolicy |
Passthrough에서 게이트웨이는 TLS 핸드셰이크의 SNI(Server Name Indication) 필드만 읽어 라우팅을 결정합니다. 페이로드는 복호화하지 않으므로 HTTP 헤더 기반 라우팅, 경로 매칭, 헤더 수정 등은 불가능합니다.
Gateway + TLSRoute 기본 설정
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy-gateway
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: tls-gateway
namespace: gateway-system
spec:
gatewayClassName: envoy-gateway
listeners:
# HTTPS Terminate (일반 웹 트래픽)
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-cert
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "true"
# TLS Passthrough (엔드투엔드 암호화)
- name: tls-passthrough
protocol: TLS
port: 8443
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
tls-passthrough: "true"
kinds:
- kind: TLSRoute
TLSRoute: SNI 기반 라우팅
SNI 호스트네임으로 트래픽을 서로 다른 백엔드 서비스로 분기합니다.
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: payment-tls
namespace: payment
spec:
parentRefs:
- name: tls-gateway
namespace: gateway-system
sectionName: tls-passthrough
hostnames:
- "payment.internal.example.com"
rules:
- backendRefs:
- name: payment-service
port: 8443
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: vault-tls
namespace: vault
spec:
parentRefs:
- name: tls-gateway
namespace: gateway-system
sectionName: tls-passthrough
hostnames:
- "vault.internal.example.com"
rules:
- backendRefs:
- name: vault-service
port: 8200
---
# 여러 호스트네임을 같은 백엔드로 라우팅
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: database-tls
namespace: database
spec:
parentRefs:
- name: tls-gateway
namespace: gateway-system
sectionName: tls-passthrough
hostnames:
- "primary.db.example.com"
- "replica.db.example.com"
rules:
- backendRefs:
- name: postgres-service
port: 5432
백엔드 Pod TLS 설정: cert-manager 연동
백엔드 Pod이 자체 인증서로 TLS를 제공하도록 cert-manager와 연동합니다.
# 내부 CA로 백엔드 인증서 발급
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: internal-ca
namespace: payment
spec:
ca:
secretName: internal-ca-keypair
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: payment-tls-cert
namespace: payment
spec:
secretName: payment-tls-secret
issuerRef:
name: internal-ca
dnsNames:
- "payment.internal.example.com"
- "payment-service.payment.svc.cluster.local"
duration: 720h # 30일
renewBefore: 168h # 7일 전 갱신
---
# Deployment: 인증서를 Pod에 마운트
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: payment
spec:
template:
spec:
containers:
- name: app
image: payment-service:latest
ports:
- containerPort: 8443
env:
- name: TLS_CERT_PATH
value: /certs/tls.crt
- name: TLS_KEY_PATH
value: /certs/tls.key
volumeMounts:
- name: tls-certs
mountPath: /certs
readOnly: true
volumes:
- name: tls-certs
secret:
secretName: payment-tls-secret
// Spring Boot — 자체 TLS 설정
// application.yml
server:
port: 8443
ssl:
enabled: true
certificate: file:/certs/tls.crt
certificate-private-key: file:/certs/tls.key
client-auth: want # mTLS: 클라이언트 인증서 요청
// NestJS — 자체 TLS 설정
// main.ts
const httpsOptions = {
cert: fs.readFileSync('/certs/tls.crt'),
key: fs.readFileSync('/certs/tls.key'),
ca: fs.readFileSync('/certs/ca.crt'), // mTLS용 CA
requestCert: true, // mTLS 활성화
rejectUnauthorized: true,
};
const app = await NestFactory.create(AppModule, { httpsOptions });
mTLS: 상호 인증서 검증
TLS Passthrough와 mTLS를 결합하면 게이트웨이를 거쳐도 클라이언트-서버 간 상호 인증이 유지됩니다.
# 클라이언트 인증서 발급 (서비스 간 mTLS)
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: order-client-cert
namespace: order
spec:
secretName: order-client-tls
issuerRef:
name: internal-ca
commonName: "order-service"
dnsNames:
- "order-service.order.svc.cluster.local"
usages:
- client auth
- digital signature
duration: 720h
---
# Order 서비스 → Payment 서비스 mTLS 호출
# Order Pod에 클라이언트 인증서 마운트
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: order
spec:
template:
spec:
containers:
- name: app
volumeMounts:
- name: client-certs
mountPath: /client-certs
readOnly: true
- name: ca-cert
mountPath: /ca
readOnly: true
volumes:
- name: client-certs
secret:
secretName: order-client-tls
- name: ca-cert
secret:
secretName: internal-ca-cert
// Spring Boot — mTLS 클라이언트 설정
@Configuration
public class MtlsRestClientConfig {
@Bean
public RestClient paymentClient() throws Exception {
SSLContext sslContext = SSLContextBuilder.create()
.loadKeyMaterial(
new File("/client-certs/tls.key"),
new File("/client-certs/tls.crt"))
.loadTrustMaterial(new File("/ca/ca.crt"))
.build();
HttpClient httpClient = HttpClient.create()
.secure(spec -> spec.sslContext(
SslContextBuilder.forClient()
.keyManager(/* cert, key */)
.trustManager(/* ca */)
.build()));
return RestClient.builder()
.baseUrl("https://payment.internal.example.com:8443")
.requestFactory(new ReactorClientHttpRequestFactory(httpClient))
.build();
}
}
BackendTLSPolicy: Re-encrypt 패턴
게이트웨이에서 TLS를 종료하되 백엔드와 다시 TLS로 통신하는 Re-encrypt 패턴입니다.
apiVersion: gateway.networking.k8s.io/v1alpha3
kind: BackendTLSPolicy
metadata:
name: payment-backend-tls
namespace: payment
spec:
targetRefs:
- group: ""
kind: Service
name: payment-service
validation:
caCertificateRefs:
- name: internal-ca-cert
group: ""
kind: ConfigMap
hostname: payment-service.payment.svc.cluster.local
---
# HTTPRoute로 L7 라우팅 (경로, 헤더 매칭 가능)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: payment-api
namespace: payment
spec:
parentRefs:
- name: tls-gateway
sectionName: https
hostnames:
- "payment.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api/v1/payments
backendRefs:
- name: payment-service
port: 8443 # 백엔드도 TLS
Re-encrypt vs Passthrough 선택 기준:
- Passthrough: L7 라우팅 불필요, 최소 레이턴시, 게이트웨이 인증서 불필요
- Re-encrypt: L7 라우팅 필요 (경로/헤더 매칭), WAF 적용 필요, 인증서 분리 관리
트러블슈팅
# TLSRoute 상태 확인
kubectl get tlsroute -A -o wide
kubectl describe tlsroute payment-tls -n payment
# Gateway 리스너 상태 확인
kubectl describe gateway tls-gateway -n gateway-system
# Status.Listeners[].Conditions에서 Accepted/Programmed 확인
# SNI 테스트 (curl로 TLS Passthrough 검증)
curl -v --resolve payment.internal.example.com:8443:GATEWAY_IP
https://payment.internal.example.com:8443/health
# openssl로 인증서 체인 확인
openssl s_client -connect GATEWAY_IP:8443
-servername payment.internal.example.com
-showcerts 2>/dev/null | openssl x509 -noout -subject -issuer
# 일반적인 문제:
# 1. "no route matched" → hostnames의 SNI와 실제 요청 불일치
# 2. "connection reset" → 백엔드 Pod의 TLS 설정 오류
# 3. "certificate verify failed" → CA 인증서 미신뢰
# 4. Gateway 리스너 Not Programmed → GatewayClass가 TLS Passthrough 미지원
프로덕션 체크리스트
- GatewayClass 지원 확인: 모든 구현체가 TLSRoute를 지원하지는 않음. Envoy Gateway, Istio, Cilium 등 확인 필수
- 인증서 자동 갱신: cert-manager의
renewBefore와 Pod 재시작 전략 연계 - CA 인증서 배포: 모든 관련 서비스에 내부 CA 인증서를 ConfigMap/Secret으로 배포
- 헬스체크 포트 분리: TLS 포트와 별도로 HTTP 헬스체크 포트 운영 권장
- 모니터링: 인증서 만료일, TLS 핸드셰이크 에러율, SNI 라우팅 실패 메트릭 수집
- 네임스페이스 격리:
allowedRoutes.namespaces로 TLSRoute 생성 권한을 필요한 네임스페이스로 제한
TLS Passthrough는 제로트러스트 네트워크의 핵심 구성 요소입니다. 게이트웨이가 트래픽을 복호화하지 않으므로, 민감 데이터가 중간에 노출될 위험이 원천 차단됩니다. Gateway API 기본 라우팅은 K8s Gateway API 실전 가이드를, cert-manager 설정은 K8s cert-manager TLS 자동화 글을 참고하세요.