Istio Hands-on Study [1기]

[5주차] 마이크로서비스 통신 보안 : 자동 mTLS

구구달스 2025. 4. 22. 10:28

🔐 Istio로 자동 mTLS 쉽게 이해하기


🔒 9.2 자동 mTLS: 서비스 간 안전한 통신

  • Istio에서는 sidecar proxy가 주입된 서비스 간 트래픽이 기본적으로 암호화되고 상호 인증(Mutual Authentication)됩니다. 이는 mTLS를 통해 구현되는데, mTLS는 두 서비스가 서로 신뢰할 수 있는 인증서를 제시신원을 확인하고 데이터를 암호화하는 방식입니다.
  • 과거에는 인증서를 수동으로 발급하고 갱신하는 과정에서 오류가 자주 발생해 서비스 장애로 이어졌습니다. Istio는 이 과정을 자동화해 오류를 줄이고 보안을 강화합니다. Istio의 컨트롤 플레인은 서비스에 필요한 인증서를 발급하고 주기적으로 갱신합니다.

  • 하지만 Istio가 "기본적으로 안전하다" 해도, 완전한 보안을 위해 추가 설정이 필요합니다:
    • 상호 인증된 트래픽만 허용: 기본적으로 Istio는 암호화되지 않은 트래픽도 허용해 서비스 마이그레이션을 쉽게 하지만, 이를 제한해 보안을 강화할 수 있습니다.
    • 최소 권한 원칙(Principle of Least Privilege): 각 서비스에 필요한 최소한의 접근 권한만 부여해, 인증서가 유출되더라도 피해를 최소화합니다.

Istio는 mTLS를 통해 서비스 간 통신을 암호화하고 상호 인증을 자동화하며,
최소 권한 원칙으로 보안을 강화합니다.


🛠️ 9.2.1 환경 설정하기

mTLS의 기능을 보여주기 위해 간단한 환경을 설정하겠습니다. 이 환경에는 세 가지 서비스가 포함됩니다:

  • webapp: 웹 애플리케이션 서비스.
  • catalog: 데이터 카탈로그 서비스.
  • sleep: sidecar proxy가 없는 레거시 워크로드로, mTLS를 지원하지 않습니다.

  • 그 다음, 서비스를 설치합니다:
(⎈|kind-myk8s:N/A) $ kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction
serviceaccount/catalog created
service/catalog created
deployment.apps/catalog created

(⎈|kind-myk8s:N/A) $ kubectl apply -f services/webapp/kubernetes/webapp.yaml -n istioinaction
serviceaccount/webapp created
service/webapp created
deployment.apps/webapp created

(⎈|kind-myk8s:N/A) $ kubectl apply -f services/webapp/istio/webapp-catalog-gw-vs.yaml -n istioinaction
gateway.networking.istio.io/coolstore-gateway created
virtualservice.networking.istio.io/webapp-virtualservice created
  • default 네임스페이스에 sleep 앱 배포
(⎈|kind-myk8s:N/A) $ cat ch9/sleep.yaml
...
    spec:
      serviceAccountName: sleep
      containers:
      - name: sleep
        image: governmentpaas/curl-ssl
        command: ["/bin/sleep", "3650d"]
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - mountPath: /etc/sleep/tls
          name: secret-volume
      volumes:
      - name: secret-volume
        secret:
          secretName: sleep-secret
          optional: true
---

(⎈|kind-myk8s:N/A) $ kubectl apply -f ch9/sleep.yaml -n default
serviceaccount/sleep created
service/sleep created
deployment.apps/sleep created
  • 설치가 완료되었는지 확인하기 위해, sleep 서비스에서 webapp 서비스로 암호화되지 않은(clear-text) 요청을 보냅니다:
(⎈|kind-myk8s:N/A) $ kubectl exec deploy/sleep -c sleep -- curl -s webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
200
  • 결과: 200
    • 서비스가 정상적으로 설정되었고, webapp이 암호화되지 않은 요청을 수락했음을 보여줍니다.
      Istio는 기본적으로 암호화되지 않은 트래픽을 허용해 서비스 마이그레이션을 용이하게 하지만, 이를 제한하려면 PeerAuthentication 리소스를 사용해 mTLS를 강제할 수 있습니다.

테스트 환경에서 webapp, catalog, sleep 서비스를 설정하고,
Istio의 기본 설정이 암호화되지 않은 트래픽을 허용함을 확인했습니다.


📌 핵심 요약

  • Istio는 mTLS를 통해 서비스 간 트래픽을 자동으로 암호화하고 상호 인증합니다.
  • 컨트롤 플레인이 인증서를 발급 및 갱신해 수동 관리의 오류를 줄입니다.
  • 최소 권한 원칙을 적용해 인증서 유출 시 피해를 최소화합니다.
  • 기본적으로 암호화되지 않은 트래픽이 허용되지만, PeerAuthentication 리소스로 이를 제한할 수 있습니다.
  • 테스트 환경에서 webapp, catalog, sleep 서비스를 설정해 mTLS 동작을 확인했습니다.

🔐 Istio PeerAuthentication 쉽게 이해하기

PeerAuthentication은 서비스 간 트래픽의 보안을 설정하는 강력한 도구로, mTLS(Mutual TLS)를 강제하거나 암호화되지 않은 트래픽을 허용하는 방식으로 서비스 메시의 보안을 관리합니다.


🔒 9.2.2 PeerAuthentication 리소스 이해하기

  • PeerAuthentication 리소스는 서비스가 mTLS를 엄격하게 요구할지(STRICT 모드), 아니면 암호화되지 않은(clear-text) 트래픽도 허용할지(PERMISSIVE 모드)를 설정합니다. 이 설정은 다음 세 가지 범위로 적용할 수 있습니다:
    • Mesh-wide: 서비스 메시 전체 워크로드에 적용.
    • Namespace-wide: 특정 네임스페이스의 모든 워크로드에 적용.
    • Workload-specific: 특정 워크로드에만 적용.

PeerAuthentication은 mTLS 설정을 통해 서비스 간 트래픽 보안을 관리하며,
메시, 네임스페이스, 워크로드 단위로 적용 가능합니다.


🛡️ 모든 비인증 트래픽 차단: Mesh-wide 정책

  • 서비스 메시의 보안을 강화하려면 암호화되지 않은 트래픽을 차단하는 Mesh-wide PeerAuthentication 정책을 설정할 수 있습니다. 이 정책은 STRICT 모드를 사용해 모든 트래픽이 mTLS로 인증되도록 강제합니다.
  • Mesh-wide 정책은 다음 두 조건을 만족해야 합니다:
    • Istio가 설치된 네임스페이스 에 적용.
    • 정책 이름은 default로 설정

  • 다음은 Mesh-wide 정책의 예제입니다:
(⎈|kind-myk8s:N/A) $ cat ch9/meshwide-strict-peer-authn.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication" #PeerAuthentication은 Istio에서 서비스 간의 인증 정책을 설정하는 리소스
metadata:
  name: "default"
  namespace: "istio-system" #리소스가 "istio-system" 네임스페이스에 생성됨
spec:
  mtls: #상호 TLS(mTLS) 설정
    mode: STRICT #모든 서비스 간 통신이 반드시 mTLS를 사용해야 한다
  • 이 정책을 클러스터에 적용합니다:
$ kubectl apply -f ch9/meshwide-strict-peer-authn.yaml -n istio-system
peerauthentication.security.istio.io/default created
  • 이제 sleep 서비스(레거시 워크로드, sidecar proxy 없음)에서 webapp으로 암호화되지 않은 요청을 보내 확인해봅니다:
$ kubectl exec deploy/sleep -c sleep -- curl -s http://webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
000
command terminated with exit code 56
  • command terminated with exit code 56
    • 요청이 거부된 것을 확인할 수 있습니다. 이는 STRICT 모드가 암호화되지 않은 트래픽을 차단했기 때문입니다.
      하지만, 모든 서비스를 한 번에 STRICT 모드로 전환하는 것은 대규모 프로젝트에서 팀 간 협업이 필요할 수 있습니다. 따라서 점진적으로 보안을 강화하는 PERMISSIVE 모드를 사용할 수 있습니다.

Mesh-wide PeerAuthentication 정책은 STRICT 모드로 암호화되지 않은 트래픽을 차단하며,
istio-system 네임스페이스에 default 이름으로 적용됩니다.

 


🌐 비인증 트래픽 허용: Namespace-wide 정책

  • Namespace-wide PeerAuthentication 정책은 특정 네임스페이스의 모든 워크로드에 적용됩니다.
    예를 들어, istioinaction 네임스페이스에서 PERMISSIVE 모드를 설정하면, 레거시 워크로드(sleep) 같은 비인증 트래픽도 허용됩니다. 이는 Mesh-wide STRICT 정책을 덮어씌웁니다.
  • 다음은 Namespace-wide 정책 예제입니다:
$ cat << EOF | kubectl apply -f -
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "default"
  namespace: "istioinaction"
spec:
  mtls:
    mode: PERMISSIVE
EOF
peerauthentication.security.istio.io/default created
  • 요청 실행
$ kubectl exec deploy/sleep -c sleep -- curl -s http://webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
200
  • 하지만, 모든 워크로드에 PERMISSIVE 모드를 적용하면 보안 취약점이 커질 수 있습니다. 대신, 특정 워크로드(예: webapp)에만 PERMISSIVE 모드를 적용하는 것이 더 안전합니다.
  • 다음 실습을 위해 삭제
$ kubectl delete pa default -n istioinaction
peerauthentication.security.istio.io "default" deleted

Namespace-wide 정책은 네임스페이스 단위로 PERMISSIVE 모드를 설정해 비인증 트래픽을 허용하지만,
보안 강화를 위해 특정 워크로드에만 적용하는 것이 좋습니다.


🎯 워크로드별 PeerAuthentication 정책 적용

  • 더 세밀한 제어를 위해 Workload-specific PeerAuthentication 정책을 사용해 특정 워크로드에만 PERMISSIVE 모드를 적용할 수 있습니다. 예를 들어, webapp 워크로드에만 비인증 트래픽을 허용하고, catalog 워크로드는 STRICT 모드를 유지합니다.
  • 다음은 webapp에 적용하는 정책 예제입니다:
$ cat ch9/workload-permissive-peer-authn.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "webapp"
  namespace: "istioinaction"
spec:
  selector:
    matchLabels:
      app: webapp #app=webapp 라벨이 있는 워크로드에만 이 정책을 적용
  mtls:
    mode: PERMISSIVE #mTLS 모드를 "허용(PERMISSIVE)"으로 설정
    #PERMISSIVE 모드는 mTLS와 일반 텍스트 트래픽을 모두 허용합니다. 
    #즉, TLS로 암호화된 연결과 암호화되지 않은 평문 연결 모두 수락합니다.
  • 이 정책을 적용합니다:
$ kubectl apply -f ch9/workload-permissive-peer-authn.yamlsive-peer-authn.yaml
peerauthentication.security.istio.io/webapp created
  • 이제 sleep 서비스에서 webapp으로 요청을 다시 보냅니다:
$ kubectl exec deploy/sleep -c sleep -- curl -s http://webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
200

$ kubectl -n default exec deploy/sleep -c sleep --   curl -s webapp.istioinaction/api/catalog | jq
[
  {
    "id": 1,
    "color": "amber",
    "department": "Eyewear",
    "name": "Elinor Glasses",
    "price": "282.00"
  },
...
  • 요청이 성공적으로 처리되었습니다! 이는 webapp이 PERMISSIVE 모드로 설정되어 비인증 트래픽을 허용했기 때문입니다. 반면, catalog는 여전히 STRICT 모드를 유지해 보안을 강화합니다.
$ kubectl exec deploy/sleep -c sleep -- curl -s http://catalog.istioinaction/api/items -o /dev/null -w "%{http_code}\n"
000
command terminated with exit code 56

Workload-specific 정책은 특정 워크로드(예: webapp)에만 PERMISSIVE 모드를 적용해
보안과 유연성을 균형 있게 관리합니다.


⚙️ 추가 mTLS 모드

  • STRICTPERMISSIVE 외에 두 가지 추가 모드가 있습니다:
    • UNSET: 상위 정책(Mesh-wide 또는 Namespace-wide)을 상속합니다.
    • DISABLE: 트래픽을 프록시를 거치지 않고 서비스로 직접 보냅니다.
  • 이 모드들은 특정 상황에서 유용하지만, 대부분 STRICT 또는 PERMISSIVE를 사용합니다.

UNSET과 DISABLE 모드는 특정 상황에서 유연성을 제공하지만,
STRICT와 PERMISSIVE가 주로 사용됩니다.


🔍 서비스 간 트래픽 도청: tcpdump 사용

  • Istio 프록시에는 tcpdump 유틸리티가 포함되어 네트워크 트래픽을 캡처하고 분석할 수 있습니다. 하지만 tcpdump은 권한이 필요하므로, Istio 설치 시 privileged 모드를 활성화해야 합니다:
demo 프로파일 컨트롤 플레인 배포 시 istio-proxy 에 privileged 설정 >> 이미 설정 되어 있음
  • webapp Pod이 준비되면, 트래픽을 캡처합니다:
    • sudo tcpdump: 루트 권한으로 tcpdump 도구를 실행합니다.
      -l: 라인 버퍼링 모드를 사용하여 출력을 즉시 표시합니다.
      --immediate-mode: 패킷을 즉시 처리하고 표시합니다.
      -vv: 매우 상세한(very verbose) 출력을 제공합니다.
      -s 0: 패킷의 전체 내용을 캡처합니다(스냅샷 길이 제한 없음).
    • (ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0:  "페이로드가 있는 TCP 패킷"만 캡처하는 필터
      • ip[2:2]: IP 헤더의 3번째와 4번째 바이트를 가져옵니다; IP 패킷의 전체 길이 
        ((ip[0]&0xf)<<2): 첫 번째 바이트의 하위 4비트만 추출하고 왼쪽으로 2비트 시프트하여 바이트 단위로 변환 ; IP 헤더의 길이
        ((tcp[12]&0xf0)>>2):  TCP 헤더의 13번째 바이트를 가져옵니다. > 상위 4비트만 추출 > 오른쪽으로 2비트 시프트; TCP 헤더의 길이
        패킷 전체 길이 - IP 헤더 길이 - TCP 헤더 길이 = 데이터 페이로드 길이
        계산된 페이로드 길이가 0이 아닌지 확인

        이 세 값을 사용하여 패킷의 페이로드 길이를 계산하고, 페이로드가 있는 패킷만 캡처합니다.
      • and not (port 53): DNS 트래픽(포트 53)은 제외합니다.
$ kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy \
  -- sudo tcpdump -l --immediate-mode -vv -s 0 '(((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0) and not (port 53)'
  • 다른 터미널에서 sleep 서비스에서 webapp으로 요청을 보냅니다:
$ kubectl exec deploy/sleep -c sleep -- curl -s webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
200
  • 첫 번째 터미널에서 캡처된 트래픽을 확인하면, 데이터가 clear-text로 전송된 것을 볼 수 있습니다:
# sleep -> webapp 호출 HTTP
01:10:00.107105 IP (tos 0x0, ttl 63, id 8987, offset 0, flags [DF], proto TCP (6), length 146)
    10-10-0-14.sleep.default.svc.cluster.local.53950 > webapp-7685bcb84-tp5fd.http-alt: Flags [P.], cksum 0x14b3 (incorrect -> 0xd6a4), seq 3310570377:3310570471, ack 3138045211, win 502, options [nop,nop,TS val 1463167787 ecr 460000615], length 94: HTTP, length: 94
        GET /api/catalog HTTP/1.1
        Host: webapp.istioinaction
        User-Agent: curl/8.5.0
        Accept: */*
        
# webapp -> catalog 호출 HTTPS
01:10:00.110830 IP (tos 0x0, ttl 64, id 19518, offset 0, flags [DF], proto TCP (6), length 1304)
    webapp-7685bcb84-tp5fd.59800 > 10-10-0-12.catalog.istioinaction.svc.cluster.local.3000: Flags [P.], cksum 0x1937 (incorrect -> 0xa3c3), seq 3859477536:3859478788, ack 594120, win 501, options [nop,nop,TS val 388368956 ecr 1794695550], length 1252
    
# catalog -> webapp 응답 HTTPS
01:10:00.113592 IP (tos 0x0, ttl 63, id 55101, offset 0, flags [DF], proto TCP (6), length 1788)
    10-10-0-12.catalog.istioinaction.svc.cluster.local.3000 > webapp-7685bcb84-tp5fd.59800: Flags [P.], cksum 0x1b1b (incorrect -> 0xb29e), seq 1:1737, ack 1252, win 501, options [nop,nop,TS val 1794807257 ecr 388368956], length 1736

# webapp -> sleep 응답 HTTP
01:10:00.114496 IP (tos 0x0, ttl 64, id 26730, offset 0, flags [DF], proto TCP (6), length 662)
    webapp-7685bcb84-tp5fd.http-alt > 10-10-0-14.sleep.default.svc.cluster.local.53950: Flags [P.], cksum 0x16b7 (incorrect -> 0x886e), seq 1:611, ack 94, win 509, options [nop,nop,TS val 460000622 ecr 1463167787], length 610: HTTP, length: 610
        HTTP/1.1 200 OK
        content-length: 357
        content-type: application/json; charset=utf-8
        date: Wed, 07 May 2025 01:10:00 GMT
        x-envoy-upstream-service-time: 5
        server: istio-envoy
        x-envoy-decorator-operation: webapp.istioinaction.svc.cluster.local:80/*

        [{"id":1,"color":"amber","department":"Eyewear","name":"Elinor 
        ...

mTLS를 사용하면 트래픽이 암호화되어 도청이 불가능하지만,
레거시 워크로드는 clear-text로 데이터를 노출할 수 있습니다.


🛡️ 워크로드 신원 확인: SVID 인증서 검증

  • 마지막으로, Istio가 발급한 인증서가 유효한 SVID(SPIFFE Verifiable Identity Document) 인지, 그리고 워크로드의 서비스 계정과 일치하는지 확인합니다.
    openssl 명령어를 사용해 catalog 서비스의 X.509 인증서를 확인합니다:
$ kubectl -n istioinaction exec deploy/webapp -c istio-proxy \
  -- openssl s_client -showcerts \
  -connect catalog.istioinaction.svc.cluster.local:80 \
  -CAfile /var/run/secrets/istio/root-cert.pem | \
  openssl x509 -in /dev/stdin -text -noout
depth=1 O = cluster.local
verify return:1
depth=0
verify return:1
40C757AB307F0000:error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required:../ssl/record/rec_layer_s3.c:1584:SSL alert number 116
Certificate:
...
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Authority Key Identifier:
                A2:42:16:BF:6E:00:5D:D4:75:F9:72:6E:9D:F9:08:1D:CC:E2:2C:F6
            X509v3 Subject Alternative Name: critical
                URI:spiffe://cluster.local/ns/istioinaction/sa/catalog
...
  • SPIFFE ID가 catalog 워크로드의 서비스 계정과 일치함을 확인할 수 있습니다.
            X509v3 Subject Alternative Name: critical
                URI:spiffe://cluster.local/ns/istioinaction/sa/catalog
  •  이제 인증서의 유효성을 검증합니다: (Pod 내부에서 다음 명령어를 실행)
$ kubectl -n istioinaction exec -it deploy/webapp -c istio-proxy -- /bin/bash

istio-proxy@webapp-7685bcb84-tp5fd:/$ openssl verify -CAfile /var/run/secrets/istio/root-cert.pem \
  <(openssl s_client -connect \
  catalog.istioinaction.svc.cluster.local:80 -showcerts 2>/dev/null)
/dev/fd/63: OK

 

  • 결과: /dev/fd/63: OK =>  Istio CA가 발급한 인증서가 유효함을 보여줍니다.

Istio가 발급한 SVID 인증서는 워크로드의 서비스 계정과 연결되며,
유효성 검증을 통해 신뢰할 수 있음을 확인할 수 있습니다.


📌 핵심 요약

  • PeerAuthentication 리소스는 mTLS를 설정해 서비스 간 트래픽 보안을 관리하며, Mesh-wide, Namespace-wide, Workload-specific 범위로 적용됩니다.
  • STRICT 모드는 모든 트래픽에 mTLS를 강제하고, PERMISSIVE 모드는 비인증 트래픽을 허용해 마이그레이션을 지원합니다.
  • Workload-specific 정책은 특정 워크로드(예: webapp)에만 PERMISSIVE 모드를 적용해 보안과 유연성을 균형 있게 관리합니다.
  • tcpdump로 트래픽을 분석하면, mTLS는 데이터를 암호화하지만 레거시 워크로드는 clear-text로 노출될 수 있습니다.
  • SVID 인증서는 SPIFFE ID를 포함하며, openssl로 유효성을 검증해 워크로드 신원을 확인할 수 있습니다.