SNI를 사용한 트래픽 라우팅

2025. 2. 23. 01:48Devops/Istio

728x90
반응형

SNI(Server Name Indication)는 TLS 핸드셰이크 과정에서 클라이언트가 접속하려는 도메인 이름을 명시하는 기능입니다.

Istio에서는 이를 활용하여 도메인 기반의 트래픽 라우팅을 설정할 수 있습니다.

(SNI에 대한 관련사항은 자세히 다루지는 않습니다.. 참고를 https://www.cloudflare.com/ko-kr/learning/ssl/what-is-sni/ 하면 좋을듯합니다.. )

 

기본적으로 Istio의 Gateway는 Host 헤더 기반의 HTTP 라우팅을 수행하지만, TLS 트래픽의 경우 SNI를 활용하여 특정 서비스로 라우팅할 수도 있습니다. 이는 멀티 테넌트 환경에서 특정 도메인별로 트래픽을 라우팅하거나, 특정 클러스터로 트래픽을 분산할 때 유용합니다. 또한 이런 방식은 한 IP에 여러 호스트를 매칭시키기 때문에 참여의 어플리케이션 개수도 늘릴수 있습니다.

한마디로 한 IP와 특정 TCP PORT로 오는 HOST를 Routing 한다 생각하면 쉬울수도..?

 

그리하여 이번엔 SNI를 관련된 트래픽 라우팅에 대해 인증서 만들고 파드도 배포하며 직접 확인해봅니다.

0. 예시 인증서 만들기

인증서는 selfsigned로 만든 CA인증서를 활용합니다. 그후 그 CA인증서로
swlee.example.com과 swlee2.example.com 두개의 인증서를 만들어서 진행합니다.  

  • ca인증서 만들기
# CA 개인키 생성
openssl genpkey -algorithm RSA -out ca.key -pkeyopt rsa_keygen_bits:2048
 
# CA 인증서 생성 (자체서명) 10년으로 만듬
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj "/CN=MySelfSignedCA"

$ ls
ca.crt ca.key

 

  • 서버용 인증서 만들기 (도메인용)
# swlee.example.com 인증서만들기
## 개인키생성
$ openssl genpkey -algorithm RSA -out swlee.key -pkeyopt rsa_keygen_bits:2048
## 인증서 서명
$ openssl req -new -key swlee.key -out swlee.csr -subj "/CN=swlee.example.com"
## 자체서명 인증서 만들기
$ openssl x509 -req -in swlee.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out swlee.crt -days 365 -sha256


# swlee2.example.com 인증서만들기
## 개인키생성
$ openssl genpkey -algorithm RSA -out swlee2.key -pkeyopt rsa_keygen_bits:2048
## 인증서 서명
$ openssl req -new -key swlee2.key -out swlee2.csr -subj "/CN=swlee2.example.com"
## 자체서명 인증서 만들기
$ openssl x509 -req -in swlee2.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out swlee2.crt -days 365 -sha256
$ ls
ca.crt ca.key swlee.crt  swlee.csr  swlee.key  swlee2.crt  swlee2.csr  swlee2.key

 

 

  • 인증서 체인 만들기 

클라이언트가 루트 CA는 신뢰하지만 서버 인증서와는 직접 연결하지않기때문에 중간 CA를 만들어줘야합니다. 

브라우저에서 만약에 신뢰 할 수 없는 인증서 오류를 방지하려면 ca-chain.crt( 중간 + 루트 ) 인증서를 함께 제공해야 합니다. 인증서 체인을 만드는이유는 curl을 통한 TEST를 하기 위해 만듭니다.. 

# swlee.example.com 용 chain 생성
$ cat swlee.crt ca.crt > ca-chain.cert

# swlee2.example.com 용 chain 생성
$ cat swlee2.crt ca.crt > ca-chain2.cert

 

이후 TLS 커넥션을 마지막으로 종료할 Service에 배포할 Secret을 배포해줘야합니다.

$ kubectl create secret tls swlee.example.com \
--key ./swlee.key  \
--cert ./swlee.crt
secret/swlee.example.com created
$ kubectl get secrets
NAME                TYPE                DATA   AGE
swlee.example.com   kubernetes.io/tls   2      3s


$ kubectl create secret tls swlee2.example.com \
--key ./swlee2.key  \
--cert ./swlee2.crt
secret/swlee2.example.com created
$ kubectl get secrets
NAME                 TYPE                DATA   AGE
swlee.example.com    kubernetes.io/tls   2      63s
swlee2.example.com   kubernetes.io/tls   2      18s

 

 

1. Istio에서 SNI 기반 트래픽 라우팅 적용 방법

1.0.   application 설치와 Ingerss Gateway에 TCP추가하기

먼저 Test를 위해 간단한 application을 배포합니다. 앱은 swlee.example.com과 swlee2.example.com에 사용할 앱 2개를 배포하여 진행합니다.

여기서 중요한것은 SNI에 대한 TLS 종료는 백엔드에서 한다는 점이기에 백엔드로 배포되는 서비스에 위에서만든 인증서를 꼭 마운트를 해줘야 한다는 점입니다.

  • swlee.example.com 배포
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: swlee-example-1
  name: swlee-example-1
spec:
  ports:
  - name: https
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: swlee-example-1
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: swlee-example-1
  name: swlee-example-1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: swlee-example-1
  template:
    metadata:
      labels:
        app: swlee-example-1
    spec:
      containers:
      - env:
        - name: "LISTEN_ADDR"
          value: "0.0.0.0:8080"
        - name: "SERVER_TYPE"
          value: "http"
        - name: "TLS_CERT_LOCATION"
          value: "/etc/certs/tls.crt"
        - name: "TLS_KEY_LOCATION"
          value: "/etc/certs/tls.key"
        - name: "NAME"
          value: "swlee-example-1"
        - name: "MESSAGE"
          value: "Hello from swlee-example-1!!!"
        - name: KUBERNETES_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        image: nicholasjackson/fake-service:v0.14.1
        imagePullPolicy: IfNotPresent
        name: swlee-example
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        securityContext:
          privileged: false
        volumeMounts:
        - mountPath: /etc/certs
          name: tls-certs
      volumes:
      - name: tls-certs
        secret:
          secretName: swlee.example.com

 

  • swlee2.example.com 배포
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: swlee-example-2
  name: swlee-example-2
spec:
  ports:
  - name: https
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: swlee-example-2
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: swlee-example-2
  name: swlee-example-2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: swlee-example-2
  template:
    metadata:
      labels:
        app: swlee-example-2
    spec:
      containers:
      - env:
        - name: "LISTEN_ADDR"
          value: "0.0.0.0:8080"
        - name: "SERVER_TYPE"
          value: "http"
        - name: "TLS_CERT_LOCATION"
          value: "/etc/certs/tls.crt"
        - name: "TLS_KEY_LOCATION"
          value: "/etc/certs/tls.key"
        - name: "NAME"
          value: "swlee-example-2"
        - name: "MESSAGE"
          value: "Hello from swlee-example-2!!!"
        - name: KUBERNETES_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        image: nicholasjackson/fake-service:v0.14.1
        imagePullPolicy: IfNotPresent
        name: swlee-example
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        securityContext:
          privileged: false
        volumeMounts:
        - mountPath: /etc/certs
          name: tls-certs
      volumes:
      - name: tls-certs
        secret:
          secretName: swlee2.example.com

 

해당 yaml을 kubernete에 배포합니다. 꼭 tls인증서를 매칭하여 배포해야 정상작동됩니다!!

이후 배포가완료되면 TCP라우팅을 위한 ingress-gateway에 TCP를 추가합니다.

이번 글에서는 31000을 오픈하여 SNI 호스트네임에 따른 TCP라우팅이 되도록 합니다.

 

이후..  patch명령을 통해 Istio IngressGateway service에 TCP port를 추가해줍니다.

( ※ 참고 : NodePort를 사용시에는 Nodeport도 명시하여 노출시켜줘야합니다.)

# 31000 포트 추가
$ kubectl patch svc istio-ingressgateway -n istio-system --type='json' -p '[
  {
    "op": "add",
    "path": "/spec/ports/0",
    "value": {
      "name": "tcp-31000",
      "protocol": "TCP",
      "port": 31000,
      "targetPort": 31000
    }
  }
]'
service/istio-ingressgateway patched
# 31000포트 확인
$ k get svc -n istio-system istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name =="tcp-31000")]}'
{"name":"tcp-31000","nodePort":32287,"port":31000,"protocol":"TCP","targetPort":31000}

 


1.1. Istio Gateway 에 설정

이제부터 Gateway을 통해 SNI를 설정합니다.

Gateway에서는  SNI 헤더를 살펴보고 트래픽을 특정 백엔드로 라우팅 하는 일을 합니다. 라우팅 메커니즘은 passthrough를 사용합니다. 여기서 중요한것은.. TLS의 종료는 백엔드에서 해야한다는 점입니다. 그렇기에 원래라면 TLS설정은 Gateway에 하지만.. SNI는 Gateway에 TLS설정을 하지않습니다.  먼저 Gateway를 배포해봅니다.

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: sni-passthrough-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 31000
      name: tcp-sni-1
      protocol: TLS
    hosts:
    - "swlee.example.com"
    tls:
      mode: PASSTHROUGH
  - port:
      number: 31000
      name: tcp-sni-2
      protocol: TLS
    hosts:
    - "swlee2.example.com"
    tls:
      mode: PASSTHROUGH

 

 

1.2. Virtualservcie 배포

마지막으로 virtualservice을 배포하여 트래픽 라우팅을 진행해봅시다.

virtualservice은 위와 달리 두가지로 (swlee.example.com 과 swlee2.example.com) 배포해봅니다. 두가지로 하는 이유는 gateway를 하나로 배포했기 때문입니다. 

  • swlee.example.com (virutalservice 배포)
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: swlee.example.com
spec:
  hosts:
  - "swlee.example.com"
  gateways:
  - sni-passthrough-gateway
  tls:
  - match:
    - port: 31000
      sniHosts:
      - swlee.example.com
    route:
    - destination:
        host: swlee-example-1
        port:
          number: 80
  • swlee2.example.com (virutalservice 배포)
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: swlee2.example.com
spec:
  hosts:
  - "swlee2.example.com"
  gateways:
  - sni-passthrough-gateway
  tls:
  - match:
    - port: 31400
      sniHosts:
      - swlee2.example.com
    route:
    - destination:
        host: swlee-example-2
        port:
          number: 80

 

gateways는 위에 배포한 sni-passthrough-gateway로 명시해준다.

그리고 hosts는 각각 virtauservice에 할당되는 호스트를 하나씩 입력해줘야한다. 

마지막으로 sniHosts는 hosts에 명시된 Name을 넣어줘야한다. 

 

2. 결과 확인해보기

2-0. 배포된 서비스 확인

# gateway
$ k get gateways.networking.istio.io
NAME                      AGE
sni-passthrough-gateway   8m40s

# virtualService
$ k get virtualservices.networking.istio.io
NAME                  GATEWAYS                      HOSTS                    AGE
swlee.example.com     ["sni-passthrough-gateway"]   ["swlee.example.com"]    3s
swlee2.example.com    ["sni-passthrough-gateway"]   ["swlee2.example.com"]   2m50s

 

2-1. 서비스 호출

호출전에 swlee.example.com과 swlee.example.com 을 /etc/hosts에 ingressgateway 의 IP를 넣어두 되고

없다면 --resolve를 사용하여 호출하여도 됩니다.

 

  • swlee.example.com 호출 결과
$ curl -H "Host: swlee.example.com" https://swlee.example.com:31000  \
--cacert ca-chain.cert\
--resolve swlee.example.com:31000:localhost

 

 

 

  • swlee2.example.com 호출 결과
$ curl -H "Host: swlee2.example.com" https://swlee2.example.com:31000  \
--cacert ca-chain2.cert\
--resolve swlee.example2.com:31000:localhost

 

결과는 이와같다.. 한 IP와 PORT를 통해 SNI를 활용하여 특정 서비스로 라우팅이 되는걸 볼수 있습니다.

 

3. 정리

http를통한 tls은 gateway에 인증서를 넣고 앞에서 처리한다면  SNI는 back단에서 인증처리를 종료해야한다는 점을 알았다. sni를 사실 잘사용해본적이 없었는데 이번기회에 알게되어서 나중에  기회 될 때는 잘 쓸 수 있을거같다.

728x90
반응형