K8s TLS Passthrough·mTLS 심화

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 미지원

프로덕션 체크리스트

  1. GatewayClass 지원 확인: 모든 구현체가 TLSRoute를 지원하지는 않음. Envoy Gateway, Istio, Cilium 등 확인 필수
  2. 인증서 자동 갱신: cert-manager의 renewBefore와 Pod 재시작 전략 연계
  3. CA 인증서 배포: 모든 관련 서비스에 내부 CA 인증서를 ConfigMap/Secret으로 배포
  4. 헬스체크 포트 분리: TLS 포트와 별도로 HTTP 헬스체크 포트 운영 권장
  5. 모니터링: 인증서 만료일, TLS 핸드셰이크 에러율, SNI 라우팅 실패 메트릭 수집
  6. 네임스페이스 격리: allowedRoutes.namespaces로 TLSRoute 생성 권한을 필요한 네임스페이스로 제한

TLS Passthrough는 제로트러스트 네트워크의 핵심 구성 요소입니다. 게이트웨이가 트래픽을 복호화하지 않으므로, 민감 데이터가 중간에 노출될 위험이 원천 차단됩니다. Gateway API 기본 라우팅은 K8s Gateway API 실전 가이드를, cert-manager 설정은 K8s cert-manager TLS 자동화 글을 참고하세요.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux