10장 데이터 플레인 트러블 슈팅하기

이스티오는 네트워크 통신 장애 발생 시 복원력 기능(타임아웃·재시도 등)을 통해 애플리케이션의 자동 대응을 지원한다. 데이터 플레인 동기화를 담당하는 istiod, 트래픽 허용을 위한 인그레스 게이트웨이, 트래픽 제어를 수행하는 서비스 프록시, 실제 요청을 처리하는 애플리케이션이 협력해 요청 흐름을 관리한다. 서비스 프록시의 비정상 동작 시 전체 시스템에 영향을 줄 수 있으므로 각 구성 요소의 정상 작동이 중요하다.

 

10.1 가장 흔한 실수: 잘못 설정한 데이터 플레인

이스티오는 VirtualService, DestinationRule 같은 CRD로 프록시 설정을 관리한다.
이 설정들은 엔보이 설정으로 변환되어 데이터 플레인에 적용된다.
DestinationRule이 없으면 부분집합 정의가 없어 인그레스 게이트웨이에서 모든 요청이 실패한다.

# 샘플 애플리케이션 배포
kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction # catalog v1 배포
kubectl apply -f ch10/catalog-deployment-v2.yaml -n istioinaction # catalog v2 배포
kubectl apply -f ch10/catalog-gateway.yaml -n istioinaction # catalog-gateway 배포
kubectl apply -f ch10/catalog-virtualservice-subsets-v1-v2.yaml -n istioinaction

# Gateway 
cat ch10/catalog-gateway.yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: catalog-gateway
  namespace: istioinaction
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - "catalog.istioinaction.io"
    port:
      number: 80
      name: http
      protocol: HTTP

# VirtualService
cat ch10/catalog-virtualservice-subsets-v1-v2.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog-v1-v2
  namespace: istioinaction
spec:
  hosts:
  - "catalog.istioinaction.io"
  gateways:
  - "catalog-gateway"
  http:
  - route:
    - destination:
        host: catalog.istioinaction.svc.cluster.local
        subset: version-v1
        port:
          number: 80
      weight: 20
    - destination:
        host: catalog.istioinaction.svc.cluster.local
        subset: version-v2
        port:
          number: 80
      weight: 80

# 확인
kubectl get deploy,svc -n istioinaction
kubectl get gw,vs -n istioinaction

 

10.2 데이터 플레인 문제 식별하기

데이터 플레인 문제 해결 시 컨트롤 플레인 동기화 상태를 먼저 확인해야 한다. 데이터 플레인 설정은 궁극적 일관성을 가지므로 환경 변화가 즉시 반영되지 않는다. 예를 들어 파드 장애 시 쿠버네티스가 비정상 엔드포인트를 감지하고 데이터 플레인에서 제거하는 데 지연이 발생할 수 있다. 컨트롤 플레인은 지속적으로 최신 설정을 데이터 플레인에 동기화하여 일관성을 복원한다. 이 과정에서 그림 10.3과 같은 이벤트 흐름을 통해 데이터 플레인 업데이트가 시각화된다.

  • SYNCED : istiod가 보낸 마지막 설정을 엔보이가 확인했다.
  • NOT SENT : istiod가 아무것도 엔보이로 보내지 않았다. 보통은 istiod가 보낼 것이 없기 때문이다.
  • STALE : istiod가 엔보이에 업데이트를 보냈지만 확인받지 못했다. 이는 다음 중 하나를 나타낸다.
    • istiod가 과부하됐거나, 엔보이와 istiod 사이의 커넥션 부족 또는 끊김이거나, 이스티오의 버그다.

컨트롤 플레인에 문제가 없으면 데이터 플레인 워크로드 설정 오류를 키알리로 빠르게 검증해야 한다.

 

10.2.2 키알리로 잘못된 설정 발견하기 Discovering misconfigurations with Kiali

 

10.2.3 istioctl로 잘못된 설정 발견하기* Discovering misconfigurations with istioctl

istioctl analyze는 이스티오 설정 오류를 자동으로 감지하고 진단하는 강력한 도구다.
istioctl describe은 특정 파드/서비스에 적용된 라우팅 규칙과 연관된 리소스를 확인해 설정 문제를 식별한다.

istioctl describe는 워크로드별로 적용된 이스티오 설정을 분석해 요약 정보를 제공한다.
이 명령어로 서비스 메시 포함 여부, 적용된 VirtualService/DestinationRule, 상호 인증 요구사항 등을 쉽게 확인할 수 있다.

#
kubectl get pod -n istioinaction -l app=catalog -o jsonpath='{.items[0].metadata.name}'
CATALOG_POD1=$(kubectl get pod -n istioinaction -l app=catalog -o jsonpath='{.items[0].metadata.name}')

# 단축키 : experimental(x), describe(des)
docker exec -it myk8s-control-plane istioctl experimental describe -h
docker exec -it myk8s-control-plane istioctl x des pod -n istioinaction $CATALOG_POD1
Pod: catalog-6cf4b97d-l44zk
   Pod Revision: default
   Pod Ports: 3000 (catalog), 15090 (istio-proxy)
--------------------
Service: catalog
   Port: http 80/HTTP targets pod port 3000
--------------------
Effective PeerAuthentication:
   Workload mTLS mode: PERMISSIVE


Exposed on Ingress Gateway http://172.18.0.2
VirtualService: catalog-v1-v2
   WARNING: No destinations match pod subsets (checked 1 HTTP routes)
      Warning: Route to subset version-v1 but NO DESTINATION RULE defining subsets!
      Warning: Route to subset version-v2 but NO DESTINATION RULE defining subsets!


# 문제 해결 후 확인
cat ch10/catalog-destinationrule-v1-v2.yaml       
kubectl apply -f ch10/catalog-destinationrule-v1-v2.yaml
docker exec -it myk8s-control-plane istioctl x des pod -n istioinaction $CATALOG_POD1
Pod: catalog-6cf4b97d-l44zk
   Pod Revision: default
   Pod Ports: 3000 (catalog), 15090 (istio-proxy)
--------------------
Service: catalog
   Port: http 80/HTTP targets pod port 3000
DestinationRule: catalog for "catalog.istioinaction.svc.cluster.local"
   Matching subsets: version-v1 # 일치하는 부분집합
      (Non-matching subsets version-v2) # 일치하지 않은 부분집합
   No Traffic Policy
--------------------
Effective PeerAuthentication:
   Workload mTLS mode: PERMISSIVE

Exposed on Ingress Gateway http://172.18.0.2
VirtualService: catalog-v1-v2 # 이 파드로 트래픽을 라우팅하는 VirtualService
   Weight 20%

# 다음 점검 방법을 위해 오류 상황으로 원복
kubectl delete -f ch10/catalog-destinationrule-v1-v2.yaml

에러 확인

 

원인 해결 후 정상 확인

analyze와 describe 명령어로 대부분의 설정 오류를 해결할 수 있지만, 추가 진단이 필요한 경우 더 깊은 분석이 필요하다.

 

 

10.3 엔보이 설정에서 수동으로 잘못된 설정 발견하기

엔보이 관리(admin) 인터페이스는 각 서비스 프록시에서 포트 15000으로 접근할 수 있고, 프록시의 설정 전체를 확인하거나 수정하는 데 사용된다.
설정이 많아 가독성이 떨어지기 때문에, istioctl은 출력 결과를 필터링해 필요한 부분만 쉽게 볼 수 있도록 도와준다.
엔보이 관리 인터페이스를 활용하면 자동화 도구로 잡히지 않는 설정 오류를 수동으로 직접 조사할 수 있다.

kubectl port-forward deploy/catalog -n istioinaction 15000:15000
open http://localhost:15000

# 현재 적재한 엔보이 설정 출력 : 데이터양이 많다!
curl -s localhost:15000/config_dump | wc -l
  13952

 

 

 

 

10.3.2 istioctl 로 프록시 설정 쿼리하기 Querying proxy configurations using istioctl

엔보이 API는 프록시의 리스너를 통해 네트워크 설정(IP/포트)을 정의하고, HTTP 필터 체인에서 라우터 필터가 고급 라우팅을 수행한다. 라우트는 가상 호스트와 클러스터를 매칭하는 규칙을 순차적으로 적용하며, 이스티오는 RDS를 통해 동적으로 관리한다. 클러스터는 유사한 워크로드 엔드포인트 그룹을 구성하고, 부분집합으로 세분화된 트래픽 제어가 가능하다. 엔드포인트는 실제 워크로드 IP 주소를 나타내며, 인그레스 게이트웨이 설정 검증 시 리스너·라우트·클러스터·엔드포인트를 종합적으로 확인해야 한다.

 

 

#엔보이 리느서 설정 쿼리하기

#
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway -n istio-system
ADDRESS PORT  MATCH DESTINATION
0.0.0.0 8080  ALL   Route: http.8080 # 8080 포트에 대한 요청은 루트 http.8080에 따라 라우팅하도록 설정된다
0.0.0.0 15021 ALL   Inline Route: /healthz/ready*
0.0.0.0 15090 ALL   Inline Route: /stats/prometheus*
## 리스터는 8080 포트에 설정돼 있다.
## 그 리스너에서 트래픽은 http.8080 이라는 루트에 따라 라우팅된다.

#
kubectl get svc -n istio-system  istio-ingressgateway -o yaml | grep "ports:" -A10
  ports:
  - name: status-port
    nodePort: 30840
    port: 15021
    protocol: TCP
    targetPort: 15021
  - name: http2
    nodePort: 30000
    port: 80
    protocol: TCP
    targetPort: 8080

 

nodePort 30000이나 clusterIP/서비스명으로 유입된 트래픽은 인그레스 게이트웨이 파드의 8080 포트로 전달되고, 해당 포트의 리스너와 http.8080 라우트가 이를 처리한다.

 

 

이스티오는 VirtualServiceDestinationRule로 트래픽 라우팅 규칙을 정의하고, 이 설정들이 엔보이 프록시에 적용된다.
DestinationRule이 없으면 부분집합 정의가 누락되어 라우팅 실패가 발생하며, HTTP 헤더(예: x-istio-cohort)를 활용해 특정 버전(v2)으로 트래픽을 제어할 수 있다.

# 엔보이 루트 설정 쿼리하기 QUERYING THE ENVOY ROUTE CONFIGURATION

# http.8080 루트의 트래픽을 어느 클러스터로 라우팅할지 알아내기 위해 설정을 쿼리
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway -n istio-system --name http.8080
NAME          DOMAINS                      MATCH     VIRTUAL SERVICE
http.8080     catalog.istioinaction.io     /*        catalog-v1-v2.istioinaction
## 호스트 catalog.istioinaction.io 의 트래픽 중 URL이 경로 접두사 /*과 일치하는 것이 istioinaction 네임스페이스의 catalog 서비스에 있는 catalog VirtualService 로 라우팅됨을 보여준다.

# 세부 정보 확인
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway -n istio-system --name http.8080 -o json
...
                "routes": [
                    {
                        "match": {
                            "prefix": "/" # 일치해야 하는 라우팅 규칙
                        },
                        "route": {
                            "weightedClusters": {
                                "clusters": [ # 규칙이 일치할 때 트래픽을 라우팅하는 클러스터
                                    {
                                        "name": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
                                        "weight": 20
                                    },
                                    {
                                        "name": "outbound|80|version-v2|catalog.istioinaction.svc.cluster.local",
                                        "weight": 80
                                    }
                                ],
                                "totalWeight": 100
                            },
...

라우팅 규칙에 따라 트래픽은 outbound|80|version-v1 또는 version-v2 부분집합의 catalog.istioinaction.svc.cluster.local 클러스터로 분산된다.

 

 

엔보이 클러스터 설정은 백엔드 서비스 라우팅을 정의하며, 각 클러스터는 여러 엔드포인트로 부하를 분산한다.
istioctl proxy-config clusters 명령어로 특정 클러스터를 필터링(direction/fqdn/port/subset)해 확인할 수 있다.
예를 들어 outbound|80|version-v1|catalog.istioinaction.svc.cluster.local 클러스터는 v1 버전의 catalog 서비스 트래픽을 처리한다.

#
docker exec -it myk8s-control-plane istioctl proxy-config clusters deploy/istio-ingressgateway -n istio-system \
--fqdn catalog.istioinaction.svc.cluster.local --port 80
SERVICE FQDN                                PORT     SUBSET     DIRECTION     TYPE     DESTINATION RULE
catalog.istioinaction.svc.cluster.local     80       -          outbound      EDS  

#
docker exec -it myk8s-control-plane istioctl proxy-config clusters deploy/istio-ingressgateway -n istio-system \
--fqdn catalog.istioinaction.svc.cluster.local --port 80 --subset version-v1


# 해당 파일이 없을 경우 'copy & paste'로 작성 후 진행 하자
docker exec -it myk8s-control-plane cat /istiobook/ch10/catalog-destinationrule-v1-v2.yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: catalog
  namespace: istioinaction
spec:
  host: catalog.istioinaction.svc.cluster.local
  subsets:
  - name: version-v1
    labels:
      version: v1
  - name: version-v2
    labels:
      version: v2

# istioctl analyze 명령어를 사용해서, 설정할 yaml 파일이 식별한 서비스 메시 오류를 고칠 수 있는지 확인
docker exec -it myk8s-control-plane istioctl analyze /istiobook/ch10/catalog-destinationrule-v1-v2.yaml -n istioinaction
✔ No validation issues found when analyzing /istiobook/ch10/catalog-destinationrule-v1-v2.yaml.




# 문제 해결
cat ch10/catalog-destinationrule-v1-v2.yaml
kubectl apply -f ch10/catalog-destinationrule-v1-v2.yaml

# 확인
docker exec -it myk8s-control-plane istioctl proxy-config clusters deploy/istio-ingressgateway -n istio-system \
--fqdn catalog.istioinaction.svc.cluster.local --port 80
SERVICE FQDN                                PORT     SUBSET         DIRECTION     TYPE     DESTINATION RULE
catalog.istioinaction.svc.cluster.local     80       -              outbound      EDS      catalog.istioinaction
catalog.istioinaction.svc.cluster.local     80       version-v1     outbound      EDS      catalog.istioinaction
catalog.istioinaction.svc.cluster.local     80       version-v2     outbound      EDS      catalog.istioinaction

CATALOG_POD1=$(kubectl get pod -n istioinaction -l app=catalog -o jsonpath='{.items[0].metadata.name}')
docker exec -it myk8s-control-plane istioctl x des pod -n istioinaction $CATALOG_POD1
docker exec -it myk8s-control-plane istioctl analyze -n istioinaction

# 호출 확인
curl http://catalog.istioinaction.io:30000/items
curl http://catalog.istioinaction.io:30000/items

 

결과가 없음.

 

적용에 문제가 없음을 확인
문제 해결 후 정상 확인

 

 

엔보이 클러스터는 **동적 서비스 발견(EDS)**과 **Aggregated Discovery Service(ADS)**를 통해 설정된다.
istioctl proxy-config clusters 명령어로 확인 시, 클러스터는 outbound|80|version-v1|catalog.istioinaction.svc.cluster.local 형식으로 정의되며 포트·부분집합·FQDN 정보를 포함한다.
DestinationRule 리소스에서 정의된 부분집합이 클러스터에 반영되며, ADS를 통해 엔드포인트 정보가 실시간으로 동기화되어 트래픽 라우팅이 관리된다.

 

엔보이 클러스터의 엔드포인트 정보는 istioctl proxy-config endpoints 명령어로 확인한다.
예를 들어 --cluster 플래그에 특정 클러스터명을 지정하면 해당 클러스터에 등록된 **엔드포인트 IP와 상태(HEALTHY/UNHEALTHY)**를 출력한다.
출력된 IP로 kubectl get pod를 실행해 실제 워크로드 존재 여부를 검증함으로써 라우팅 설정의 정확성을 확인할 수 있다.

 

 

 

10.3.3 애플리케이션 문제 트러블슈팅하기 Troubleshooting application issues

서비스 프록시의 로그와 메트릭은 마이크로서비스 환경에서 성능 병목, 실패 엔드포인트, 성능 저하 등 다양한 문제를 트러블슈팅하는 데 활용된다.

 

간헐적으로 제한 시간을 초과하는 느린 워크로드 준비하기* SETTING UP AN INTERMITTENTLY SLOW WORKLOAD THAT TIMES OUT

# 신규 터미널
for in in {1..9999}; do curl http://catalog.istioinaction.io:30000/items -w "\nStatus Code %{http_code}\n"; sleep 1; done


# catalog v2 파드 중 첫 번째 파드 이름 변수 지정
CATALOG_POD=$(kubectl get pods -l version=v2 -n istioinaction -o jsonpath={.items..metadata.name} | cut -d ' ' -f1)
echo $CATALOG_POD
catalog-v2-56c97f6db-d74kv

# 해당 파드에 latency (지연) 발생하도록 설정
kubectl -n istioinaction exec -c catalog $CATALOG_POD \
-- curl -s -X POST -H "Content-Type: application/json" \
-d '{"active": true, "type": "latency", "volatile": true}' \
localhost:3000/blowup ;
blowups=[object Object]


# 신규 터미널
for in in {1..9999}; do curl http://catalog.istioinaction.io:30000/items -w "\nStatus Code %{http_code}\n"; sleep 1; done



#
kubectl get vs -n istioinaction
NAME            GATEWAYS              HOSTS                          AGE
catalog-v1-v2   ["catalog-gateway"]   ["catalog.istioinaction.io"]   6h44m

# 타임아웃(0.5s) 적용
kubectl patch vs catalog-v1-v2 -n istioinaction --type json \
-p '[{"op": "add", "path": "/spec/http/0/timeout", "value": "0.5s"}]'

# 적용확인 
kubectl get vs catalog-v1-v2 -n istioinaction -o jsonpath='{.spec.http[?(@.timeout=="0.5s")]}' | jq
...
  "timeout": "0.5s"
}

# 신규 터미널
for in in {1..9999}; do curl http://catalog.istioinaction.io:30000/items -w "\nStatus Code %{http_code}\n"; sleep 1; done
upstream request timeout
Status Code 504
upstream request timeout
Status Code 504
..

#
kubectl logs -n istio-system -l app=istio-ingressgateway -f
[2025-05-09T08:45:41.636Z] "GET /items HTTP/1.1" 504 UT response_timeout - "-" 0 24 501 - "172.18.0.1" "curl/8.7.1" "cb846eff-07ac-902e-9890-7af478c84166" "catalog.istioinaction.io:30000" "10.10.0.13:3000" outbound|80|version-v2|catalog.istioinaction.svc.cluster.local 10.10.0.7:58078 10.10.0.7:8080 172.18.0.1:61108 - -
[2025-05-09T08:45:43.175Z] "GET /items HTTP/1.1" 200 - via_upstream - "-" 0 502 375 374 "172.18.0.1" "curl/8.7.1" "3f2de0c1-5af2-9a33-a6ac-bca08c1ee271" "catalog.istioinaction.io:30000" "10.10.0.13:3000" outbound|80|version-v2|catalog.istioinaction.svc.cluster.local 10.10.0.7:58084 10.10.0.7:8080 172.18.0.1:61118 - -
...

kubectl logs -n istio-system -l app=istio-ingressgateway -f | grep 504
...

#
kubectl logs -n istioinaction -l version=v2 -c istio-proxy -f
[2025-05-09T08:42:38.152Z] "GET /items HTTP/1.1" 0 DC downstream_remote_disconnect - "-" 0 0 500 - "172.18.0.1" "curl/8.7.1" "69fef43c-2fea-9e51-b33d-a0375b382d86" "catalog.istioinaction.io:30000" "10.10.0.13:3000" inbound|3000|| 127.0.0.6:36535 10.10.0.13:3000 172.18.0.1:0 outbound_.80_.version-v2_.catalog.istioinaction.svc.cluster.local default
...

 

 

 

엔보이 액세스 로그 이해하기 + 엔보이 액세스 로그 형식 바꾸기

이스티오 프록시 로그는 기본적으로 TEXT 형식이지만, JSON 형식으로 설정하면 각 값의 의미를 쉽게 파악할 수 있다.

# 형식 설정 전 로그 확인
kubectl logs -n istio-system -l app=istio-ingressgateway -f | grep 504
...

# MeshConfig 설정 수정
KUBE_EDITOR="nano" kubectl edit -n istio-system cm istio
...
  mesh: |-
    accessLogFile: /dev/stdout # 기존 설정되어 있음
    accessLogEncoding: JSON # 추가
...

# 형식 설정 후 로그 확인
kubectl logs -n istio-system -l app=istio-ingressgateway -f | jq
...
{
  "upstream_host": "10.10.0.13:3000", # 요청을 받는 업스트림 호스트
  "bytes_received": 0,
  "upstream_service_time": null,
  "response_code_details": "response_timeout",
  "upstream_cluster": "outbound|80|version-v2|catalog.istioinaction.svc.cluster.local",
  "duration": 501, # 500ms 인 제한 시간 초과
  "response_code": 504,
  "path": "/items",
  "protocol": "HTTP/1.1",
  "upstream_transport_failure_reason": null,
  "connection_termination_details": null,
  "method": "GET",
  "requested_server_name": null,
  "start_time": "2025-05-09T08:56:38.988Z",
  "downstream_remote_address": "172.18.0.1:59052",
  "upstream_local_address": "10.10.0.7:57154",
  "downstream_local_address": "10.10.0.7:8080",
  "bytes_sent": 24,
  "authority": "catalog.istioinaction.io:30000",
  "x_forwarded_for": "172.18.0.1",
  "request_id": "062ad02a-ff36-9dcc-8a7d-68eabb01bbb5",
  "route_name": null,
  "response_flags": "UT", # 엔보이 응답 플래그, UT(Upstream request Timeout)로 중단됨, '업스트림 요청 제한 시간 초과'
  "user_agent": "curl/8.7.1"
}
...

# slow 동작되는 파드 IP로 느린 동작 파드 확인!
CATALOG_POD=$(kubectl get pods -l version=v2 -n istioinaction -o jsonpath={.items..metadata.name} | cut -d ' ' -f1)
kubectl get pod -n istioinaction $CATALOG_POD -owide
NAME                         READY   STATUS    RESTARTS   AGE     IP           NODE                  NOMINATED NODE   READINESS GATES
catalog-v2-56c97f6db-d74kv   2/2     Running   0          7h11m   10.10.0.13   myk8s-control-plane   <none>           <none>

 

 

 

엔보이 게이트웨이의 로깅 수준 높이기 INCREASING THE LOGGING LEVEL FOR THE INGRESS GATEWAY

엔보이는 none, error, warning, info, debug 등 다양한 로깅 수준을 범위별(connection, http, router, pool 등)로 설정해 필요한 영역의 로그만 상세히 확인할 수 있다.

#
docker exec -it myk8s-control-plane istioctl proxy-config log deploy/istio-ingressgateway -n istio-system \
--level http:debug,router:debug,connection:debug,pool:debug

# 로그 확인
kubectl logs -n istio-system -l app=istio-ingressgateway -f
k logs -n istio-system -l app=istio-ingressgateway -f > istio-igw-log.txt # 편집기로 열어서 보기
...

 

#로그 내용

# 504 검색
2025-05-09T09:17:17.762027Z	debug	envoy http external/envoy/source/common/http/filter_manager.cc:967	[C18119][S12425904214070917868] Sending local reply with details response_timeout	thread=38
2025-05-09T09:17:17.762072Z	debug	envoy http external/envoy/source/common/http/conn_manager_impl.cc:1687	[C18119][S12425904214070917868] encoding headers via codec (end_stream=false):
':status', '504'
'content-length', '24'
'content-type', 'text/plain'
'date', 'Fri, 09 May 2025 09:17:17 GMT'
'server', 'istio-envoy'
	thread=38

# 커넥션 ID(C18119)로 다시 검색

## [C18119] new stream  # 시작
2025-05-09T09:17:17.262341Z	debug	envoy http external/envoy/source/common/http/conn_manager_impl.cc:329	[C18119] new stream	thread=38
2025-05-09T09:17:17.262425Z	debug	envoy http external/envoy/source/common/http/conn_manager_impl.cc:1049	[C18119][S12425904214070917868] request headers complete (end_stream=true):
':authority', 'catalog.istioinaction.io:30000'
':path', '/items'
':method', 'GET'
'user-agent', 'curl/8.7.1'
'accept', '*/*'
	thread=38

## /items 요청이 cluster로 매칭됨
2025-05-09T09:17:17.262445Z	debug	envoy http external/envoy/source/common/http/conn_manager_impl.cc:1032	[C18119][S12425904214070917868] request end stream	thread=38
2025-05-09T09:17:17.262468Z	debug	envoy connection external/envoy/source/common/network/connection_impl.h:92	[C18119] current connecting state: false	thread=38
025-05-09T09:17:17.262603Z	debug	envoy router external/envoy/source/common/router/router.cc:470	[C18119][S12425904214070917868] cluster 'outbound|80|version-v2|catalog.istioinaction.svc.cluster.local' match for URL '/items'	thread=38
2025-05-09T09:17:17.262683Z	debug	envoy router external/envoy/source/common/router/router.cc:678	[C18119][S12425904214070917868] router decoding headers:
':authority', 'catalog.istioinaction.io:30000'
':path', '/items'
':method', 'GET'
':scheme', 'http'
'user-agent', 'curl/8.7.1'
'accept', '*/*'
'x-forwarded-for', '172.18.0.1'
'x-forwarded-proto', 'http'
'x-envoy-internal', 'true'
'x-request-id', 'a6bc39e7-9215-950f-96ea-4cb5f6b12deb'
'x-envoy-decorator-operation', 'catalog-v1-v2:80/*'
'x-envoy-peer-metadata', 'ChQKDkFQUF9DT05UQUlORVJTEgIaAAoaCgpDTFVTVEVSX0lEEgwaCkt1YmVybmV0ZXMKGwoMSU5TVEFOQ0VfSVBTEgsaCTEwLjEwLjAuNwoZCg1JU1RJT19WRVJTSU9OEggaBjEuMTcuOAqcAwoGTEFCRUxTEpEDKo4DCh0KA2FwcBIWGhRpc3Rpby1pbmdyZXNzZ2F0ZXdheQoTCgVjaGFydBIKGghnYXRld2F5cwoUCghoZXJpdGFnZRIIGgZUaWxsZXIKNgopaW5zdGFsbC5vcGVyYXRvci5pc3Rpby5pby9vd25pbmctcmVzb3VyY2USCRoHdW5rbm93bgoZCgVpc3RpbxIQGg5pbmdyZXNzZ2F0ZXdheQoZCgxpc3Rpby5pby9yZXYSCRoHZGVmYXVsdAowChtvcGVyYXRvci5pc3Rpby5pby9jb21wb25lbnQSERoPSW5ncmVzc0dhdGV3YXlzChIKB3JlbGVhc2USBxoFaXN0aW8KOQofc2VydmljZS5pc3Rpby5pby9jYW5vbmljYWwtbmFtZRIWGhRpc3Rpby1pbmdyZXNzZ2F0ZXdheQovCiNzZXJ2aWNlLmlzdGlvLmlvL2Nhbm9uaWNhbC1yZXZpc2lvbhIIGgZsYXRlc3QKIgoXc2lkZWNhci5pc3Rpby5pby9pbmplY3QSBxoFZmFsc2UKGgoHTUVTSF9JRBIPGg1jbHVzdGVyLmxvY2FsCi8KBE5BTUUSJxolaXN0aW8taW5ncmVzc2dhdGV3YXktNmJiOGZiNjU0OS1oY2RuYwobCglOQU1FU1BBQ0USDhoMaXN0aW8tc3lzdGVtCl0KBU9XTkVSElQaUmt1YmVybmV0ZXM6Ly9hcGlzL2FwcHMvdjEvbmFtZXNwYWNlcy9pc3Rpby1zeXN0ZW0vZGVwbG95bWVudHMvaXN0aW8taW5ncmVzc2dhdGV3YXkKFwoRUExBVEZPUk1fTUVUQURBVEESAioACicKDVdPUktMT0FEX05BTUUSFhoUaXN0aW8taW5ncmVzc2dhdGV3YXk='
'x-envoy-peer-metadata-id', 'router~10.10.0.7~istio-ingressgateway-6bb8fb6549-hcdnc.istio-system~istio-system.svc.cluster.local'
'x-envoy-expected-rq-timeout-ms', '500'
'x-envoy-attempt-count', '1'
	thread=38

## upstream timeout 으로 client 에서 끊음 (disconnect)
2025-05-09T09:17:17.262701Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:265	[C17947] using existing fully connected connection	thread=38
2025-05-09T09:17:17.262710Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:182	[C17947] creating stream	thread=38
2025-05-09T09:17:17.262736Z	debug	envoy router external/envoy/source/common/router/upstream_request.cc:581	[C18119][S12425904214070917868] pool ready	thread=38
2025-05-09T09:17:17.761697Z	debug	envoy router external/envoy/source/common/router/router.cc:947	[C18119][S12425904214070917868] upstream timeout	thread=38 # 업스트림 서버가 설정된 타임아웃 내에 응답하지 않아 요청이 실패
2025-05-09T09:17:17.761762Z	debug	envoy router external/envoy/source/common/router/upstream_request.cc:500	[C18119][S12425904214070917868] resetting pool request	thread=38
2025-05-09T09:17:17.761776Z	debug	envoy connection external/envoy/source/common/network/connection_impl.cc:139	[C17947] closing data_to_write=0 type=1	thread=38
2025-05-09T09:17:17.761779Z	debug	envoy connection external/envoy/source/common/network/connection_impl.cc:250	[C17947] closing socket: 1	thread=38
2025-05-09T09:17:17.761920Z	debug	envoy connection external/envoy/source/extensions/transport_sockets/tls/ssl_socket.cc:320	[C17947] SSL shutdown: rc=0	thread=38
2025-05-09T09:17:17.761982Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:484	[C17947] client disconnected, failure reason: 	thread=38
2025-05-09T09:17:17.761997Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:454	invoking idle callbacks - is_draining_for_deletion_=false	thread=38

## 504 응답
2025-05-09T09:17:17.762027Z	debug	envoy http external/envoy/source/common/http/filter_manager.cc:967	[C18119][S12425904214070917868] Sending local reply with details response_timeout	thread=38
2025-05-09T09:17:17.762072Z	debug	envoy http external/envoy/source/common/http/conn_manager_impl.cc:1687	[C18119][S12425904214070917868] encoding headers via codec (end_stream=false):
':status', '504'
'content-length', '24'
'content-type', 'text/plain'
'date', 'Fri, 09 May 2025 09:17:17 GMT'
'server', 'istio-envoy'
	thread=38
2025-05-09T09:17:17.762253Z	debug	envoy pool external/envoy/source/common/conn_pool/conn_pool_base.cc:215	[C17947] destroying stream: 0 remaining	thread=38
2025-05-09T09:17:17.763718Z	debug	envoy connection external/envoy/source/common/network/connection_impl.cc:656	[C18119] remote close	thread=38
2025-05-09T09:17:17.763731Z	debug	envoy connection external/envoy/source/common/network/connection_impl.cc:250	[C18119] closing socket: 0	thread=38

응답이 느린 업스트림의 IP가 액세스 로그와 일치해, 특정 인스턴스만 오동작함을 확인했다.
로그에서 클라이언트(프록시)가 업스트림 커넥션을 종료한 것도 확인되어, 제한 시간 초과로 인한 종료라는 예상과 일치한다.
엔보이 로거를 통해 프록시의 동작 원인과 문제 인스턴스를 정확히 파악할 수 있다.

 

10.3.4 tcpdump로 네트워크 트래픽 검사* Inspect network traffic with ksniff

특정 파드에서 tcpdump 후 wireshark 로 불러오기

# slow 파드 정보 확인
CATALOG_POD=$(kubectl get pods -l version=v2 -n istioinaction -o jsonpath={.items..metadata.name} | cut -d ' ' -f1)
kubectl get pod -n istioinaction $CATALOG_POD -owide

# catalog 서비스 정보 확인
kubectl get svc,ep -n istioinaction
NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/catalog   ClusterIP   10.200.1.178   <none>        80/TCP    10h

NAME                ENDPOINTS                                         AGE
endpoints/catalog   10.10.0.12:3000,10.10.0.13:3000,10.10.0.14:3000   10h

# istio-proxy 에서 기본 정보 확인
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- sudo whoami
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- tcpdump -h
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- ip -c addr
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- ip add show dev eth0
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- ip add show dev lo

# istio-proxy 에 eth0 에서 패킷 덤프
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- sudo tcpdump -i eth0 tcp port 3000 -nnq
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- sudo tcpdump -i eth0 tcp port 3000 -nn
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- sudo tcpdump -i eth0 tcp port 3000

# istio-proxy 에 lo 에서 패킷 덤프
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- sudo tcpdump -i lo -nnq

# istio-proxy 에 tcp port 3000 에서 패킷 덤프
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- sudo tcpdump -i any tcp port 3000 -nnq
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- sudo tcpdump -i any tcp port 3000 -nn

#
kubectl describe pod -n istioinaction $CATALOG_POD
...
    Mounts:
      /etc/istio/pod from istio-podinfo (rw)
      /etc/istio/proxy from istio-envoy (rw)
      /var/lib/istio/data from istio-data (rw)
      /var/run/secrets/credential-uds from credential-socket (rw)
      /var/run/secrets/istio from istiod-ca-cert (rw)
      /var/run/secrets/tokens from istio-token (rw)
      /var/run/secrets/workload-spiffe-credentials from workload-certs (rw)
      /var/run/secrets/workload-spiffe-uds from workload-socket (rw)
...

# istio-proxy 에 tcp port 3000 에서 패킷 덤프에 출력 결과를 파일로 저장 
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- sudo tcpdump -i any tcp port 3000 -w /var/lib/istio/data/dump.pcap
kubectl exec -it -n istioinaction $CATALOG_POD -c istio-proxy -- ls -l /var/lib/istio/data/

# 출력 결과 파일을 로컬로 다운로드
kubectl cp -n istioinaction -c istio-proxy $CATALOG_POD:var/lib/istio/data/dump.pcap ./dump.pcap

# 로컬로 다운 받은 파일을 wireshark 로 불러오기
wireshark dump.pcap

 

 

RST, FIN 을 사이드카에서 주는게 아니라 istio GW에서 준다.

 

그라파나를 통한 Istio 실패 요청 비율 분석

  1. 클라이언트 측 성공률 70% (30% 실패) → 주요 원인은 504 Gateway Timeout
  2. 서버 측 성공률 100% → 실제 서버 문제는 없으나, Envoy 프록시가 응답 코드 0으로 처리
  3. 인그레스 게이트웨이 응답 플래그 UT(Upstream Timeout) vs catalog v2 플래그 DC(Downstream Connection 종료)
  4. 타임아웃 차이 → 클라이언트(istio-ingress)의 0.5초 타임아웃 설정이 서버(catalog) 응답보다 짧아 발생
  5. 트러블슈팅 포인트 : 워크로드(v1/v2)별 상세 메트릭 추적 필요 → PromQL로 파드 단위 분할 분석 권장

 

중간중간 끊기면서 뭔가 제대로 출력이 안된듯..

 

 

프로메테우스로 Istio 문제 파드 진단 핵심

목적 : 그라파나 한계 보완 → 파드 단위 실패 요청 집중 분석

쿼리 전략 : istio_requests_total 메트릭에 DC 응답 플래그 필터링 → 클라이언트 강제 종료 사례 추적

진단 결과 : catalog v2 파드에서만 응답 코드 0 집중 발생 → 타임아웃 설정 불일치 확인

sort_desc( # 가장 높은 값부터 내림차순 정렬
  sum( # irate 값들을 집계
    irate( #  요청 수 초당 증가율
      istio_requests_total {
        reporter="destination",   # 서버(destination) 측에서 보고한 메트릭만 필터링
        destination_service=~"catalog.istioinaction.svc.cluster.local",   # catalog 가 서버(destination)측인 메트릭만 필터링
        response_flags="DC"       # DC (다운스트림 커넥션 종료)로 끝난 메트릭만 필터링
      }[5m]
    )
  )by(response_code, pod, version) # 응답 코드(response_code), 대상 pod, 버전(version) 별로 분리 => sum.. 합산
)

# 쿼리1
istio_requests_total
istio_requests_total{reporter="destination", destination_service=~"catalog.istioinaction.svc.cluster.local"}
istio_requests_total{reporter="destination", destination_service=~"catalog.istioinaction.svc.cluster.local",response_flags="DC"}

# 쿼리2
istio_requests_total{reporter="destination", destination_service=~"catalog.istioinaction.svc.cluster.local",response_flags="DC"}[5m]
irate(istio_requests_total{reporter="destination", destination_service=~"catalog.istioinaction.svc.cluster.local",response_flags="DC"}[5m])
sum(irate(istio_requests_total{reporter="destination", destination_service=~"catalog.istioinaction.svc.cluster.local",response_flags="DC"}[5m]))

# 쿼리3
sum(irate(istio_requests_total{reporter="destination", destination_service=~"catalog.istioinaction.svc.cluster.local",response_flags="DC"}[5m])) by(response_code, pod, version)
sort_desc(sum(irate(istio_requests_total{reporter="destination", destination_service=~"catalog.istioinaction.svc.cluster.local",response_flags="DC"}[5m]))by(response_code, pod, version))

 

 

## PromQL 쿼리별 분석

### **쿼리1: 기본 메트릭 필터링**
```promql
istio_requests_total
istio_requests_total{reporter="destination", destination_service=~"catalog.istioinaction.svc.cluster.local"}
istio_requests_total{reporter="destination", destination_service=~"catalog.istioinaction.svc.cluster.local",response_flags="DC"}
```
- **목적**: 단계별 필터링을 통한 문제 범위 축소
  1. **전체 메트릭 확인**: `istio_requests_total` (모든 요청)
  2. **서버 측 리포트 필터**: `reporter="destination"` + `destination_service`(catalog 서비스 대상 요청)
  3. **이슈 신호 포착**: `response_flags="DC"` (Downstream Connection 종료 이벤트)

---

### **쿼리2: 실시간 트래픽 패턴 분석**
```promql
istio_requests_total{...}[5m]
irate(...[5m])
sum(irate(...))
```
- **동작 원리**:
  - `[5m]`: **5분 간 데이터 범위** 지정 → 단기 트렌드 파악
  - `irate()`: **초당 요청 증가율** 계산 → 급증/감소 추이 감지
  - `sum()`: 모든 레이블 값 **통합 집계** → 전체 실패율 산출

---

### **쿼리3: 근본 원인 진단**
```promql
sum(...) by(response_code, pod, version)
sort_desc(...)
```
- **핵심 기능**:
  - `by(response_code, pod, version)`: **파드/버전별** 실패 요청 분할 분석
  - `sort_desc()`: **실패율 상위 항목** 우선 표시 → 문제 있는 워크로드 신속 식별

---

### **실무 적용 시나리오**
```python
# 문제 파악 프로세스 예시
if "response_flags=DC" in query_results:
    identify_affected_pods()  # 쿼리3 실행 → catalog-v2 파드 발견
    check_timeout_config()    # istio-ingressgateway 타임아웃 0.5초 확인
    adjust_virtual_service()  # 타임아웃 1초로 조정
```

---

### **요약 표**

| 쿼리 단계 | 주요 연산자 | 출력 예시 | 용도 |
|---------|------------|----------|-----|
| 쿼리1 | `{}` 필터 | `{response_flags="DC", pod="catalog-v2"}` | 이슈 후보군 추출 |
| 쿼리2 | `irate()` | `0.35 req/sec` | 실시간 트래픽 강도 측정 |
| 쿼리3 | `sort_desc()` | `catalog-v2 (70%) → catalog-v1 (5%)` | 문제 파드 우선순위 결정 |

---

 

 

부록 D 이스티오 구성 요소 트러블 슈팅하기

이스티오 사이드카는 헬스체크, 메트릭 수집·노출, DNS 해석, 트래픽 라우팅 등 다양한 기능을 제공한다. 프록시가 트래픽을 처리하기 전에 설정 수신 및 ID 할당 등 추가적인 준비 상태 확인이 필요하다. 메트릭은 애플리케이션, 에이전트, 엔보이 프록시에서 생성되며, 에이전트가 이를 집계해 노출한다.

 

  • 서비스용 포트 Ports facing other services
    • 15020 : (파일럿 에이전트 프로세스) 여러 기능 제공!
      • 메트릭을 집계하고 노출하며, 이때 메트릭에는 엔보이 프록시의 15090 포트에 쿼리한 메트릭, 애플리케이션 메트릭(설정한 경우), 자체 메트릭이 있다.
      • 엔보이 및 DNS 프록시를 헬스 체크. 이 엔드포인트에서 애플리케이션도 헬스 체크하도록 프록시를 설정할 수 있지만, 보통은 가상머신과 같이 쿠버네티스가 아닌 워크로드에만 사용한다.
      • 이스티오 개발 팀에 유용한 파일럿 에이전트 디버깅용 엔드포인트로, 메모리 정보, CPU 프로파일링 등과 같은 정보를 노출한다.
    • 15021 : (엔보이 프로세스) 사이드카 주입된 파드는 이 포트에서 트래픽을 받을 준비가 됐는지 확인하도록 설정된다. Pods with the sidecar injected are configured to check their readiness to receive traffic on this port.
      • 앞서 설명한 것처럼 엔보이 프록시헬스 체크를 15020 포트의 파일럿 에이전트로 라우팅하며, 실제 헬스 체크는 여기서 일어난다. the Envoy proxy routes the health checks to the Pilot agent on port 15020, where the actual healthchecking occurs.
    • 15053 : (파일럿 에이전트 프로세스) 쿠버네티스 DNS 해석이 충분하지 않은 에지 케이스를 해결하기 위해 istiod가 구성한 로컬 DNS 프록시 Local DNS proxy configured by istiod to resolve edge cases where Kubernetes DNS resolution doesn’t suffice.
    • 15001 : (엔보이 프로세스) 애플리케이션에서 나가는 트래픽은 Iptable 규칙에 의해 일단 이 포트로 리다이렉트되며, 이후 프록시가 트래픽을 서비스로 라우팅한다.
    • 15006 : (엔보이 프로세스) 애플리케이션으로 들어오는 트래픽은 Iptable 규칙에 의해 일단 이 포트로 리다이렉트되며, 여기서 로컬 애플리케이션 라우팅된다.
  • 에이전트 디버깅 및 내부 상태 조사에 유용한 포트 useful for debugging and introspecting the agent
    • 15000 : (엔보이 프로세스) 엔보이 프록시 관리 인터페이스
    • 15090 : (엔보이 프로세스) 엔보이 프록시 메트릭을 노출 (xDS 통계, 커넥션 통계, HTTP 통계, 이상값 outlier 통계, 헬스 체크 통계, 서킷 브레이커 통계 등)
    • 15004 : (파일럿 에이전트 프로세스) 에이전트를 통해 이스티오 파일럿 디버그 엔드포인트를 노출. 파일럿과의 연결 문제를 디버깅에 유용.
    • 15020 : (파일럿 에이전트 프로세스) 파일럿 에이전트 디버기용 엔드포인트들을 노출.
#
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: liveness-http
  namespace: istioinaction
spec:
  selector:
    matchLabels:
      app: liveness-http
      version: v1
  template:
    metadata:
      labels:
        app: liveness-http
        version: v1
    spec:
      containers:
      - name: liveness-http
        image: docker.io/istio/health:example
        ports:
        - containerPort: 8001
        livenessProbe:
          httpGet:
            path: /foo
            port: 8001
          initialDelaySeconds: 5
          periodSeconds: 5
EOF

#
kubectl get pod -n istioinaction -l app=liveness-http
kubectl describe pod -n istioinaction -l app=liveness-http
...
Containers:
  liveness-http:
    Container ID:   containerd://edaf01bff5d553e03290b3d44f60bb26958319e615a27a9b38309aad9b2df477
    Image:          docker.io/istio/health:example
    Image ID:       docker.io/istio/health@sha256:d8a2ff91d87f800b4661bec5aaadf73d33de296d618081fa36a0d1cbfb45d3d5
    Port:           8001/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Sat, 10 May 2025 16:58:35 +0900
    Ready:          True
    Restart Count:  0
    Liveness:       http-get http://:15020/app-health/liveness-http/livez delay=5s timeout=1s period=5s #success=1 #failure=3
    ...
  istio-proxy:
    Container ID:  containerd://d4b0955372bdb7b3e1490eb3f290c6c6f5a9f2691eabea4cebafaafa8be85fc9
    Image:         docker.io/istio/proxyv2:1.17.8
    Image ID:      docker.io/istio/proxyv2@sha256:d33fd90e25c59f4f7378d1b9dd0eebbb756e03520ab09cf303a43b51b5cb01b8
    Port:          15090/TCP
    ...
    Readiness:  http-get http://:15021/healthz/ready delay=1s timeout=3s period=2s #success=1 #failure=30
    Environment:
      ...                          
      ISTIO_META_POD_PORTS:          [
                                         {"containerPort":8001,"protocol":"TCP"}
                                     ]
      ISTIO_META_APP_CONTAINERS:     liveness-http
      ISTIO_META_CLUSTER_ID:         Kubernetes
      ISTIO_META_NODE_NAME:           (v1:spec.nodeName)
      ISTIO_META_INTERCEPTION_MODE:  REDIRECT
      ISTIO_META_WORKLOAD_NAME:      liveness-http
      ISTIO_META_OWNER:              kubernetes://apis/apps/v1/namespaces/istioinaction/deployments/liveness-http
      ISTIO_META_MESH_ID:            cluster.local
      TRUST_DOMAIN:                  cluster.local
      ISTIO_KUBE_APP_PROBERS:        {"/app-health/liveness-http/livez":{"httpGet":{"path":"/foo","port":8001,"scheme":"HTTP"},"timeoutSeconds":1}}


kubectl get pod -n istioinaction -l app=liveness-http -o json | jq '.items[0].spec.containers[0].livenessProbe.httpGet'
{
  "path": "/app-health/liveness-http/livez",
  "port": 15020,
  "scheme": "HTTP"
}

# 헬스체크 확인
kubectl exec -n istioinaction deploy/liveness-http -c istio-proxy -- curl -s localhost:15020/app-health/liveness-http/livez -v

# 실습 확인 후 삭제
kubectl delete deploy liveness-http -n istioinaction


#
kubectl exec -n istioinaction deploy/webapp -c istio-proxy -- curl -s localhost:15020/healthz/ready -v

# webapp 워크로드의 병합된 통계 확인 : istio_agent로 시작하는 메트릭(에이전트에서 온 것) + envoy로 시작하는 메트릭(프록시에서 온 것)
kubectl exec -n istioinaction deploy/webapp -c istio-proxy -- curl -s localhost:15020/stats/prometheus
## 응답에서는 istio_agent로 시작하는 메트릭(에이전트에서 온 것)과 envoy로 시작하는 메트릭(프록시에서 온 것)을 볼 수 있는데,
## 이는 이 둘이 병합됐음을 보여준다.

#
kubectl exec -n istioinaction deploy/webapp -c istio-proxy -- curl -s localhost:15020/quitquitquit

#
kubectl exec -n istioinaction deploy/webapp -c istio-proxy -- curl -s localhost:15020/debug/ndsz

#
kubectl port-forward deploy/webapp -n istioinaction 15020:15020
open http://localhost:15020/debug/pprof # 혹은 웹 브라우저에서 열기

 

15020 포트에는 이스티오 에이전트 트러블슈팅을 위한 여러 엔드포인트가 존재한다.
/healthz/ready는 엔보이와 DNS 프록시의 상태를 검사해 워크로드가 트래픽을 받을 준비가 됐는지 확인한다.
/stats/prometheus는 엔보이 및 애플리케이션 메트릭을 병합해 노출하고, /quitquitquit는 파일럿 에이전트 프로세스를 종료하며, /app-health/는 애플리케이션의 쿠버네티스 프로브를 프록시가 대신 처리한다

 

 

 

#
kubectl exec -n istioinaction deploy/webapp -c istio-proxy -- curl -s localhost:15004/debug/syncz -v
kubectl exec -n istioinaction deploy/webapp -c istio-proxy -- curl -s localhost:15004/debug/syncz | jq
...
      "@type": "type.googleapis.com/envoy.service.status.v3.ClientConfig",
      "node": {
        "id": "catalog-6cf4b97d-fbftr.istioinaction", # 워크로드 ID
        "metadata": {
          "CLUSTER_ID": "Kubernetes"
        }
      },
      "genericXdsConfigs": [
        {
          "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
          "configStatus": "SYNCED" # xDS API는 최신 상태로 동기화됬다
        },
        {
          "typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
          "configStatus": "SYNCED" # xDS API는 최신 상태로 동기화됬다
        },
        {
          "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
          "configStatus": "SYNCED" # xDS API는 최신 상태로 동기화됬다
        },
        {
          "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
          "configStatus": "SYNCED" # xDS API는 최신 상태로 동기화됬다
        },
...

# 하위 명령 출력 내용과 동일
docker exec -it myk8s-control-plane istioctl x internal-debug -h
docker exec -it myk8s-control-plane istioctl x internal-debug syncz

이스티오 파일럿은 디버그 엔드포인트를 통해 서비스 메시의 구성 및 상태 정보를 노출한다. 주요 엔드포인트로는 클러스터/라우트/리스너 설정을 확인하는 /debug/adsz, 엔드포인트 정보를 제공하는 /debug/edsz, 전체 구성을 조회하는 /debug/configz 등이 있다.

istioctl x internal-debug 명령어는 파일럿 디버그 엔드포인트에 접근해 동기화 상태(syncz), 구성 차이(diff), 엔보이 설정 등을 직접 확인할 수 있다. 인-클러스터/아웃-오브-클러스터 배포 환경에서 보안 옵션(--cert-dir, --xds-address)을 활용해 안전하게 데이터를 수집할 수 있으며, 다중 컨트롤 플레인 환경에서는 --xds-label로 특정 인스턴스를 대상으로 진단이 가능하다.

이 도구들은 Envoy 설정 동기화 문제, 엔드포인트 누락, 라우팅 규칙 오작동 등을 효과적으로 트러블슈팅하는 데 활용된다. 예를 들어 istioctl x internal-debug syncz로 전체 메시의 동기화 상태를 한 번에 확인하거나, 특정 파드의 설정 차이를 비교해 문제 원인을 식별할 수 있다.

 

#
kubectl -n istio-system exec -it deploy/istiod -- netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 127.0.0.1:9876          0.0.0.0:*               LISTEN     
tcp6       0      0 :::15017                :::*                    LISTEN     
tcp6       0      0 :::15014                :::*                    LISTEN     
tcp6       0      0 :::15012                :::*                    LISTEN     
tcp6       0      0 :::15010                :::*                    LISTEN     
tcp6       0      0 :::8080                 :::*                    LISTEN 

# pilot-discovery 프로세스 확인
kubectl -n istio-system exec -it deploy/istiod -- ss -tnlp
State          Recv-Q         Send-Q                 Local Address:Port                  Peer Address:Port         Process                                          
LISTEN         0              4096                       127.0.0.1:9876                       0.0.0.0:*             users:(("pilot-discovery",pid=1,fd=8))          
LISTEN         0              4096                               *:15017                            *:*             users:(("pilot-discovery",pid=1,fd=12))         
LISTEN         0              4096                               *:15014                            *:*             users:(("pilot-discovery",pid=1,fd=9))          
LISTEN         0              4096                               *:15012                            *:*             users:(("pilot-discovery",pid=1,fd=10))         
LISTEN         0              4096                               *:15010                            *:*             users:(("pilot-discovery",pid=1,fd=11))         
LISTEN         0              4096                               *:8080                             *:*             users:(("pilot-discovery",pid=1,fd=3)) 

#
kubectl describe pod -n istio-system -l app=istiod
...
Containers:
  discovery:
    Container ID:  containerd://f13d7ad8a32cc0cecf47392ef426ea4687ce12d1abf64b5a6d2a60c2f8934e04
    Image:         docker.io/istio/pilot:1.17.8
    Image ID:      docker.io/istio/pilot@sha256:cb9e7b1b1c7b8dcea37d5173b87c40f38a5ae7b44799adfdcf8574c57a52ad2c
    Ports:         8080/TCP, 15010/TCP, 15017/TCP
    Host Ports:    0/TCP, 0/TCP, 0/TCP
    Args:
      discovery
      --monitoringAddr=:15014
      --log_output_level=default:info
      --domain
      cluster.local
      --keepaliveMaxServerConnectionAge
      30m
    ...
    Readiness:  http-get http://:8080/ready delay=1s timeout=5s period=3s #success=1 #failure=3
    Environment:
      REVISION:                                     default
      JWT_POLICY:                                   third-party-jwt
      PILOT_CERT_PROVIDER:                          istiod
      POD_NAME:                                     istiod-8d74787f-ltkhs (v1:metadata.name)
      POD_NAMESPACE:                                istio-system (v1:metadata.namespace)
      SERVICE_ACCOUNT:                               (v1:spec.serviceAccountName)
      KUBECONFIG:                                   /var/run/secrets/remote/config
      PILOT_TRACE_SAMPLING:                         100
      PILOT_ENABLE_PROTOCOL_SNIFFING_FOR_OUTBOUND:  true
      PILOT_ENABLE_PROTOCOL_SNIFFING_FOR_INBOUND:   true
      ISTIOD_ADDR:                                  istiod.istio-system.svc:15012
      PILOT_ENABLE_ANALYSIS:                        false
      CLUSTER_ID:                                   Kubernetes
...

 

  • 서비스용 포트
    • 15010 : xDS API 및 인증서 발급을 평문으로 노출한다. 트래픽을 스니핑할 수 있으므로 이 포트는 사용하지 않는 것이 좋다.
    • 15012 : 15010 포트와 노출하는 정보는 같지만 보안을 적용한다. 이 포트는 TLS를 사용해 ID를 발급하여, 후속 요청은 상호 인증된다.
    • 15014 : 11장에서 다룬 것과 같은 컨트롤 플레인 메트릭을 노출한다.
    • 15017 : 쿠버네티스 API 서버가 호출하는 웹훅 서버를 노출한다.
      • 쿠버네티스 API 서버는 새로 만들어진 파드에 사이드카를 주입하고, Gateway나 VirtualServie 같은 이스티오 리소스를 검증하기 위해 호출한다.
  • 디버깅 및 검사 포트
    • 8080 : 이스티오 파일럿 디버그 엔드포인트를 노출한다.
    • 9876 : istiod 프로세스에 대한 검사 정보를 노출한다.
#
kubectl -n istio-system port-forward deploy/istiod 8080
open http://localhost:8080/debug

# 파일럿이 알고 있는 서비스 메시 상태
## 클러스터, 루트, 리스너 설정
curl -s http://localhost:8080/debug/adsz | jq

## 이 파일럿이 관리하는 모든 프록시에 대한 푸시를 트리거한다.
curl -s http://localhost:8080/debug/adsz?push=true
Pushed to 4 servers

## /debug/edsz=proxyID=<pod>.<namespace> : 프록시가 알고 있는 엔드포인트들
curl -s http://localhost:8080/debug/edsz=proxyID=webapp.istioninaction

## /debug/authorizationz : 네임스페이스에 적용되는 인가 정책 목록
curl -s http://localhost:8080/debug/authorizationz | jq


# 파일럿이 알고 있는 데이터 플레인 설정을 나타내는 엔드포인트
## 이 파일럿 인스턴스에 연결된 모든 엔보이의 버전 상태 : 현재 비활성화되어 있음
curl -s http://localhost:8080/debug/config_distribution
Pilot Version tracking is disabled. It may be enabled by setting the PILOT_ENABLE_CONFIG_DISTRIBUTION_TRACKING environment variable to true

## 이스티오 파일럿의 현재 알려진 상태에 따라 엔보이 설정을 생성한다.
curl -s http://localhost:8080/debug/config_dump?=proxyID=webapp.istioninaction

## 이 파일럿이 관리하는 프록시들을 표시한다.
curl -s http://localhost:8080/debug/syncz | jq
...
  {
    "cluster_id": "Kubernetes",
    "proxy": "webapp-7685bcb84-lwsvj.istioinaction",
    "istio_version": "1.17.8",
    "cluster_sent": "ff5e6b2c-e857-4e12-b17e-46ad968567f4",
    "cluster_acked": "ff5e6b2c-e857-4e12-b17e-46ad968567f4",
    "listener_sent": "7280c908-010d-4788-807f-7138e74fe72e",
    "listener_acked": "7280c908-010d-4788-807f-7138e74fe72e",
    "route_sent": "2a1916c3-9c05-4ce5-8cfa-d777105b9205",
    "route_acked": "2a1916c3-9c05-4ce5-8cfa-d777105b9205",
    "endpoint_sent": "dffacd32-2674-4e39-8e76-17016ff32514",
    "endpoint_acked": "dffacd32-2674-4e39-8e76-17016ff32514"
  },
...

이스티오 파일럿은 /debug/adsz, /debug/edsz, /debug/authorizationz로 서비스 메시 상태(클러스터·라우트·엔드포인트·인가 정책)를 확인한다.
/debug/syncz는 프록시 동기화 상태와 **논스(nonce)**를 비교해 설정 최신 여부를 판단하며, /debug/config_dump는 특정 프록시의 엔보이 설정을 생성한다.
istioctl proxy-status 같은 도구가 엔드포인트를 활용하지만, 복잡한 문제 시 직접 접근해 세부 진단이 가능하다.

 

 

 

 

 

9.3 서비스 간 트래픽 인가하기

이스티오의 AuthorizationPolicy 리소스는 인증된 주체에 대해 서비스 메시 내에서 메시, 네임스페이스, 워크로드 단위로 세분화된 접근 제어 정책을 선언적으로 정의할 수 있게 해준다. 

각 서비스에 배포된 프록시는 인가 집행 엔진 역할을 하며, AuthorizationPolicy 리소스에 정의된 정책을 바탕으로 요청을 직접 허용하거나 거부한다.
이처럼 접근 제어가 프록시에서 직접 이루어지기 때문에 이스티오의 인가 처리 방식은 매우 효율적이다.

 

#예시 정책

# cat ch9/allow-catalog-requests-in-web-app.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "AuthorizationPolicy"
metadata:
  name: "allow-catalog-requests-in-web-app"
  namespace: istioinaction
spec:
  selector:
    matchLabels:
      app: webapp
  rules:
  - to:
    - operation:
        paths: ["/api/catalog*"]
  action: ALLOW
  • AuthorizationPolicy 리소스는 워크로드에 적용할 인가 정책을 선언적으로 정의하며, selector(적용 대상), action(허용/거부/커스텀), rules(규칙) 세 가지 주요 필드를 가진다.
  • selector는 정책이 적용될 워크로드(예: 특정 레이블)를 지정한다.
  • rules는 요청의 출처(from: principals, namespaces, ipBlocks)와 작업(to: 경로, 메서드 등), 조건(when)을 조합해 세부 인가 규칙을 정의한다.
  • action 필드는 규칙과 일치하는 요청에 대해 허용(ALLOW), 거부(DENY), 커스텀(CUSTOM) 중 어떤 처리를 할지 결정한다.
  • 정책이 적용되면, istiod가 프록시 설정을 갱신해 해당 워크로드의 프록시가 요청을 실시간으로 허용 또는 거부한다.

 

실습환경 구성

# 9.2.1 에서 이미 배포함
kubectl -n istioinaction apply -f services/catalog/kubernetes/catalog.yaml
kubectl -n istioinaction apply -f services/webapp/kubernetes/webapp.yaml
kubectl -n istioinaction apply -f services/webapp/istio/webapp-catalog-gw-vs.yaml
kubectl -n default apply -f ch9/sleep.yaml

# gw,vs 확인 
kubectl -n istioinaction get gw,vs


# PeerAuthentication 설정 : 앞에서 이미 설정함
cat ch9/meshwide-strict-peer-authn.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "default"
  namespace: "istio-system"
spec:
  mtls:
    mode: STRICT
    
kubectl -n istio-system apply -f ch9/meshwide-strict-peer-authn.yaml
kubectl get peerauthentication -n istio-system

cat ch9/workload-permissive-peer-authn.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "webapp"
  namespace: "istioinaction"
spec:
  selector:
    matchLabels:
      app: webapp 
  mtls:
    mode: PERMISSIVE

kubectl -n istioinaction apply -f ch9/workload-permissive-peer-authn.yaml
kubectl get peerauthentication -n istioinaction

 

 

 

워크로드에 정책 적용 시 동작 확인

워크로드에 ALLOW 인가 정책이 하나라도 적용되면, 명시적으로 허용된 트래픽만 접근할 수 있고 그 외의 모든 트래픽은 기본적으로 거부된다.

 

# cat ch9/allow-catalog-requests-in-web-app.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "AuthorizationPolicy"
metadata:
  name: "allow-catalog-requests-in-web-app"
  namespace: istioinaction
spec:
  selector:
    matchLabels:
      app: webapp # 워크로드용 셀렉터 Selector for workloads
  rules:
  - to:
    - operation:
        paths: ["/api/catalog*"] # 요청을 경로 /api/catalog 와 비교한다 Matches requests with the path /api/catalog
  action: ALLOW # 일치하면 허용한다 If a match, ALLOW
  
  
  
  # 로그
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f

# 적용 전 확인 
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/hello/world # 404 리턴

# AuthorizationPolicy 리소스 적용
kubectl apply -f ch9/allow-catalog-requests-in-web-app.yaml
kubectl get authorizationpolicy -n istioinaction

# 
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/webapp.istioinaction --port 15006
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/webapp.istioinaction --port 15006 -o json > webapp-listener.json
...
          {
              "name": "envoy.filters.http.rbac",
              "typedConfig": {
                  "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
                  "rules": {
                      "policies": {
                          "ns[istioinaction]-policy[allow-catalog-requests-in-web-app]-rule[0]": {
                              "permissions": [
                                  {
                                      "andRules": {
                                          "rules": [
                                              {
                                                  "orRules": {
                                                      "rules": [
                                                          {
                                                              "urlPath": {
                                                                  "path": {
                                                                      "prefix": "/api/catalog"
                                                                  }
                                                              }
                                                          }
                                                      ]
                                                  }
                                              }
                                          ]
                                      }
                                  }
                              ],
                              "principals": [
                                  {
                                      "andIds": {
                                          "ids": [
                                              {
                                                  "any": true
                                              }
                                          ]
                                      }
                                  }
                              ]
                          }
                      }
                  },
                  "shadowRulesStatPrefix": "istio_dry_run_allow_" #  실제로 차단하지 않고, 정책이 적용됐을 때 통계만 수집 , istio_dry_run_allow_로 prefix된 메트릭 생성됨
              }
          },
...

# 로그 : 403 리턴 체크!
docker exec -it myk8s-control-plane istioctl proxy-config log deploy/webapp -n istioinaction --level rbac:debug
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
[2025-05-03T10:08:52.918Z] "GET /hello/world HTTP/1.1" 403 - rbac_access_denied_matched_policy[none] - "-" 0 19 0 - "-" "curl/8.5.0" "b272b991-7a79-9581-bb14-55a6ee705311" "webapp.istioinaction" "-" inbound|8080|| - 10.10.0.3:8080 10.10.0.13:50172 - -

# 적용 후 확인
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/hello/world # 403 리턴
RBAC: access denied

# 다음 실습을 위해 정책 삭제
kubectl delete -f ch9/allow-catalog-requests-in-web-app.yaml

 

정책이 있을때와 없을때의 차이. 403/404

 

전체 정책으로 기본적으로 모든 요청 거부하기

보안성을 증가시키고 과정을 단순화하기 위해, ALLOW 정책을 명시적으로 지정하지 않은 모든 요청을 거부하는 메시 범위 정책을 정의해보자.

# cat ch9/policy-deny-all-mesh.yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: deny-all
  namespace: istio-system # 이스티오를 설치한 네임스페이스의 정책은 메시의 모든 워크로드에 적용된다
spec: {} # spec 이 비어있는 정책은 모든 요청을 거부한다


# 적용 전 확인 
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog
curl -s http://webapp.istioinaction.io:30000/api/catalog


# 정책 적용
kubectl apply -f ch9/policy-deny-all-mesh.yaml
kubectl get authorizationpolicy -A

# 적용 후 확인 1
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog
...
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
[2025-05-03T14:45:31.051Z] "GET /api/catalog HTTP/1.1" 403 - rbac_access_denied_matched_policy[none] - "-" 0 19 0 - "-" "curl/8.5.0" "f1ec493b-cc39-9573-b3ad-e37095bbfaeb" "webapp.istioinaction" "-" inbound|8080|| - 10.10.0.3:8080 10.10.0.13:60780 - -

# 적용 후 확인 2
curl -s http://webapp.istioinaction.io:30000/api/catalog
...
kubectl logs -n istio-system -l app=istio-ingressgateway -f
...

 

access denied가 출력됨.

 

특정 네임스페이스에서 온 요청 허용하기

종종 특정 네임스페이스에서 시작한, 모든 서비스에 대한 트래픽을 허용하고 싶을 것이다.

이는 source.namespace 속성으로 할 수 있다.

# 
cat << EOF | kubectl apply -f -
apiVersion: "security.istio.io/v1beta1"
kind: "AuthorizationPolicy"
metadata:
  name: "webapp-allow-view-default-ns"
  namespace: istioinaction # istioinaction의 워크로드
spec:
  rules:
  - from: # default 네임스페이스에서 시작한
    - source:
        namespaces: ["default"]
    to:   # HTTP GET 요청에만 적용 
    - operation:
        methods: ["GET"]
EOF

#
kubectl get AuthorizationPolicy -A
NAMESPACE       NAME                           AGE
istio-system    deny-all                       11h
istioinaction   webapp-allow-view-default-ns   11h

docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/webapp.istioinaction --port 15006 -o json
...
                {
                    "name": "envoy.filters.http.rbac",
                    "typedConfig": {
                        "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
                        "rules": {
                            "policies": {
                                "ns[istio-system]-policy[deny-all]-rule[0]": {
                                    "permissions": [
                                        {
                                            "notRule": {
                                                "any": true
                                            }
                                        }
                                    ],
                                    "principals": [
                                        {
                                            "notId": {
                                                "any": true
                                            }
                                        }
                                    ]
                                },
                                "ns[istioinaction]-policy[webapp-allow-view-default-ns]-rule[0]": {
                                    "permissions": [
                                        {
                                            "andRules": {
                                                "rules": [
                                                    {
                                                        "orRules": {
                                                            "rules": [
                                                                {
                                                                    "header": {
                                                                        "name": ":method",
                                                                        "exactMatch": "GET"
                                                                    }
                                                                }
                                                            ]
                                                        }
                                                    }
                                                ]
                                            }
                                        }
                                    ],
                                    "principals": [
                                        {
                                            "andIds": {
                                                "ids": [
                                                    {
                                                        "orIds": {
                                                            "ids": [
                                                                {
                                                                    "filterState": {
                                                                        "key": "io.istio.peer_principal",
                                                                        "stringMatch": {
                                                                            "safeRegex": {
                                                                                "regex": ".*/ns/default/.*"
...

#
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f

# 호출 테스트
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog
...

sleep 서비스는 레거시 워크로드로, 사이드카 프록시가 없어 ID가 부여되지 않는다. 이로 인해 webapp 프록시는 sleep 서비스의 요청 출처(네임스페이스 등)를 확인할 수 없다. 해결책은 sleep 서비스에 프록시를 주입해 ID와 상호 인증을 지원하는 것이며, 이 방식이 권장된다.

부득이한 경우에는 webapp에서 미인증 요청도 허용하는 인가 정책을 적용할 수 있지만, 이는 보안상 덜 안전하다.

 

#
kubectl label ns default istio-injection=enabled
kubectl delete pod -l app=sleep

#
docker exec -it myk8s-control-plane istioctl proxy-status
NAME                                                   CLUSTER        CDS        LDS        EDS        RDS          ECDS         ISTIOD                    VERSION
sleep-6f8cfb8c8f-wncwh.default                         Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED       NOT SENT     istiod-8d74787f-n4c7b     1.17.8
...

# 호출 테스트 : webapp
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction # default -> webapp 은 성공
...

kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog 
error calling Catalog service

docker exec -it myk8s-control-plane istioctl proxy-config log deploy/webapp -n istioinaction --level rbac:debug
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f # webapp -> catalog 는 deny-all 로 거부됨
[2025-05-04T02:36:49.857Z] "GET /items HTTP/1.1" 403 - via_upstream - "-" 0 19 0 0 "-" "beegoServer" "669eb3d6-f59a-99e8-80cb-f1ff6c0faf99" "catalog.istioinaction:80" "10.10.0.16:3000" outbound|80||catalog.istioinaction.svc.cluster.local 10.10.0.14:33066 10.200.1.46:80 10.10.0.14:48794 - default
[2025-05-04T02:36:49.856Z] "GET /api/catalog HTTP/1.1" 500 - via_upstream - "-" 0 29 1 1 "-" "curl/8.5.0" "669eb3d6-f59a-99e8-80cb-f1ff6c0faf99" "webapp.istioinaction" "10.10.0.14:8080" inbound|8080|| 127.0.0.6:38191 10.10.0.14:8080 10.10.0.17:59998 outbound_.80_._.webapp.istioinaction.svc.cluster.local default


# 호출 테스트 : catalog
kubectl logs -n istioinaction -l app=catalog -c istio-proxy -f
kubectl exec deploy/sleep -- curl -sSL catalog.istioinaction/items # default -> catalog 은 성공


# 다음 실습을 위해 default 네임스페이스 원복
kubectl label ns default istio-injection-
kubectl rollout restart deploy/sleep

docker exec -it myk8s-control-plane istioctl proxy-status
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction # 거부 확인

 

 

미인증 레거시 워크로드에서 온 요청 허용하기

미인증 워크로드의 요청을 허용하려면 AuthorizationPolicy에서 from 필드를 제거하고, app: webapp 셀렉터로 webapp에만 적용하면 된다.

# cat ch9/allow-unauthenticated-view-default-ns.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "AuthorizationPolicy"
metadata:
  name: "webapp-allow-unauthenticated-view-default-ns"
  namespace: istioinaction
spec:
  selector:
    matchLabels:
      app: webapp
  rules:
    - to:
      - operation:
          methods: ["GET"]
          
          
          
#
kubectl apply -f ch9/allow-unauthenticated-view-default-ns.yaml
kubectl get AuthorizationPolicy -A
NAMESPACE       NAME                                           AGE
istio-system    deny-all                                       12h
istioinaction   webapp-allow-unauthenticated-view-default-ns   14s
istioinaction   webapp-allow-view-default-ns                   11h

# 여러개의 정책이 적용 시에 우선순위는?
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/webapp.istioinaction --port 15006 -o json | jq
...
         "name": "envoy.filters.http.rbac",
                        "typedConfig": {
                            "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
                            "rules": {
                                "policies": {
                                    "ns[istio-system]-policy[deny-all]-rule[0]": {
                                        "permissions": [
                                            {
                                                "notRule": {
                                                    "any": true
                                                }
                                            }
                                        ],
                                        "principals": [
                                            {
                                                "notId": {
                                                    "any": true
                                                }
                                            }
                                        ]
                                    },
                                    "ns[istioinaction]-policy[webapp-allow-unauthenticated-view-default-ns]-rule[0]": {
                                        "permissions": [
                                            {
                                                "andRules": {
                                                    "rules": [
                                                        {
                                                            "orRules": {
                                                                "rules": [
                                                                    {
                                                                        "header": {
                                                                            "name": ":method",
                                                                            "exactMatch": "GET"
                                                                        }
                                                                    }
                                                                ]
                                                            }
                                                        }
                                                    ]
                                                }
                                            }
                                        ],
                                        "principals": [
                                            {
                                                "andIds": {
                                                    "ids": [
                                                        {
                                                            "any": true
                                                        }
                                                    ]
                                                }
                                            }
                                        ]
                                    },
                                    "ns[istioinaction]-policy[webapp-allow-view-default-ns]-rule[0]": {
                                        "permissions": [
                                            {
                                                "andRules": {
                                                    "rules": [
                                                        {
                                                            "orRules": {
                                                                "rules": [
                                                                    {
                                                                        "header": {
                                                                            "name": ":method",
                                                                            "exactMatch": "GET"
                                                                        }
                                                                    }
                                                                ]
                                                            }
                                                        }
                                                    ]
                                                }
                                            }
                                        ],
                                        "principals": [
                                            {
                                                "andIds": {
                                                    "ids": [
                                                        {
                                                            "orIds": {
                                                                "ids": [
                                                                    {
                                                                        "filterState": {
                                                                            "key": "io.istio.peer_principal",
                                                                            "stringMatch": {
                                                                                "safeRegex": {
                                                                                    "regex": ".*/ns/default/.*"
                                                                                }
...

# 호출 테스트 : webapp
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction # default -> webapp 은 성공
...

kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f # webapp -> catalog 는 deny-all 로 거부됨
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog 
error calling Catalog service

# (옵션) 호출 테스트 : catalog
kubectl logs -n istioinaction -l app=catalog -c istio-proxy -f
kubectl exec deploy/sleep -- curl -sSL catalog.istioinaction/items

 

 

 

 

특정 서비스 어카운트에서 온 요청 허용하기

서비스 어카운트 정보가 webapp인 워크로드에서 온 트래픽만 허용하려면, AuthorizationPolicy에서 필터 메타데이터의 서비스 어카운트 값을 기준으로 인가 규칙을 설정하면 된다.

 

# cat ch9/catalog-viewer-policy.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "AuthorizationPolicy"
metadata:
  name: "catalog-viewer"
  namespace: istioinaction
spec:
  selector:
    matchLabels:
      app: catalog
  rules:
  - from:
    - source: 
        principals: ["cluster.local/ns/istioinaction/sa/webapp"] # Allows requests with the identity of webapp
    to:
    - operation:
        methods: ["GET"]
        
        
        
        
#
kubectl apply -f ch9/catalog-viewer-policy.yaml
kubectl get AuthorizationPolicy -A
NAMESPACE       NAME                                           AGE
istio-system    deny-all                                       13h
istioinaction   catalog-viewer                                 10s
istioinaction   webapp-allow-unauthenticated-view-default-ns   61m
istioinaction   webapp-allow-view-default-ns                   12h

#
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/catalog.istioinaction --port 15006 -o json
...
            "principals": [
                {
                    "andIds": {
                        "ids": [
                            {
                                "orIds": {
                                    "ids": [
                                        {
                                            "filterState": {
                                                "key": "io.istio.peer_principal",
                                                "stringMatch": {
                                                    "exact": "spiffe://cluster.local/ns/istioinaction/sa/webapp"
                                                }
...

# 호출 테스트 : sleep --(미인증 레거시 허용)--> webapp --(principals webapp 허용)--> catalog
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction
kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog 
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
kubectl logs -n istioinaction -l app=catalog -c istio-proxy -f

# (옵션) 호출 테스트 : catalog
kubectl exec deploy/sleep -- curl -sSL catalog.istioinaction/items
...

 

 

정책의 조건부 적용

특정 조건(예: 사용자가 관리자일 때)에만 인가 정책을 적용하려면, AuthorizationPolicy의 when 속성을 활용하면 된다.

apiVersion: "security.istio.io/v1beta1"
kind: "AuthorizationPolicy"
metadata:
  name: "allow-mesh-all-ops-admin"
  namespace: istio-system
spec:
  rules:
  - from:
    - source:
        requestPrincipals: ["auth@istioinaction.io/*"]
    when:
    - key: request.auth.claims[groups] # 이스티오 속성을 지정한다
      values: ["admin"] # 반드시 일치해야 하는 값의 목록을 지정한다
  • 이 정책은 다음 두 조건이 모두 충족될 때만 요청을 허용한다.
    • 첫째, 토큰은 요청 주체 auth@istioinaction.io/* 가 발급한 것이어야 한다.
    • 둘째, JWT에 값이 ‘admin’인 group 클레임 claim이 포함돼 있어야 한다.
  • 또는 notValues 속성을 사용해 이 정책을 적용하지 않아야 하는 값들을 정의할 수도 있다.
  • 조건에서 사용할 수 있는 이스티오 속성 전체 목록은 이스티오 문서에서 찾을 수 있다.
  • https://istio.io/latest/docs/reference/config/security/conditions/

 

인가 정책 평가 순서 

  1. CUSTOM 정책이 있으면 가장 먼저 평가
    • 외부 인가 서버 등과 연동된 CUSTOM 정책이 요청을 거부(Deny)하면, 즉시 요청이 거부
  2. DENY 정책이 그다음 평가
    • CUSTOM 정책이 없거나 통과하면, DENY 정책이 요청과 일치하는지 확인
    • 일치하는 DENY 정책이 있으면 요청이 거부
  3. ALLOW 정책이 마지막으로 평가
    • DENY 정책도 없거나 통과하면, ALLOW 정책이 있는지 확인
    • ALLOW 정책이 하나라도 요청과 일치하면 요청이 허용
  4. ALLOW 정책이 있지만, 일치하는 것이 없으면 요청은 거부
    • ALLOW 정책이 아예 없으면 요청은 허용(기본 허용).
    • 하지만 ALLOW 정책이 있는데 아무것도 일치하지 않으면 요청은 거부(기본 거부).

CUSTOM(DENY) → DENY → ALLOW 순서로 평가

 

 

9.4 최종 사용자 인증 및 인가

JWT란

JWT(JSON Web Token)는 클라이언트가 서버에 인증할 때 사용하는 간결한 클레임(정보) 표현 방식이다. 헤더, 페이로드, 서명 세 부분으로 구성되며, 각각은 점(.)으로 구분되고 Base64 URL로 인코딩된다.

헤더에는 토큰 유형과 서명 알고리즘 정보가 담기고, 페이로드에는 사용자 정보 및 클레임이 포함된다. 서명은 JWT의 진위와 무결성을 검증하는 데 사용되며, 서버의 비밀키 또는 공개키/개인키 쌍으로 생성된다.

이 구조 덕분에 JWT는 HTTP 요청에 쉽게 포함할 수 있고, 서버는 별도 세션 저장 없이 토큰만으로 사용자를 인증·인가할 수 있다.

 

#
cat ./ch9/enduser/user.jwt

# 디코딩 방법 1
jwt decode $(cat ./ch9/enduser/user.jwt)

# 디코딩 방법 2
cat ./ch9/enduser/user.jwt | cut -d '.' -f1 | base64 --decode | sed 's/$/}/'  | jq
cat ./ch9/enduser/user.jwt | cut -d '.' -f2 | base64 --decode | sed 's/$/"}/' | jq
{
  "exp": 4745145038, # 만료 시간 Expiration time
  "group": "user",   # 'group' 클레임
  "iat": 1591545038, # 발행 시각 Issue time
  "iss": "auth@istioinaction.io", # 토큰 발행자 Token issuer
  "sub": "9b792b56-7dfa-4e4b-a83f-e20679115d79" # 토큰의 주체 Subject or principal of the token
}

 

JWT의 클레임은 클라이언트의 신원과 인가 정보를 서비스에 제공하며, 이 정보를 신뢰하려면 토큰이 반드시 검증 가능해야 한다.

  • JWT는 인증 서버에서 비밀키로 서명되어 발급되며, 검증을 위해 공개키가 JWKS(JSON Web Key Set) 형태로 HTTP 엔드포인트에 제공된다.
  • 서비스(리소스 서버)는 이 JWKS 엔드포인트에서 공개키를 가져와 JWT의 서명을 검증하고, 토큰의 발급자(issuer), 만료(exp), 대상(audience) 등 클레임 값도 함께 확인한다.
  • 서명과 클레임 검증을 모두 통과하면, 서비스는 토큰의 진위와 신뢰성을 인정하고 클레임 정보를 인가에 활용한다.

1. 인증서버의 역할

  • 인증서버는 두 가지 키를 가진다.
    • private key(비공개 키): JWT 토큰에 서명할 때 사용
    • public key(공개 키): JWT 토큰의 서명을 검증할 때 사용

2. JWT 토큰 발급

  • 사용자가 인증서버에 로그인하면, 인증서버는 private key로 서명된 JWT 토큰을 발급

3. 공개 키(JWKS) 제공

  • 인증서버는 자신의 public key를 JWKS(JSON Web Key Set)라는 형식으로 HTTP 엔드포인트를 통해 제공
  • 서비스 서버는 이 엔드포인트에서 public key를 가져올 수 있다.

4. 서비스 서버의 검증 과정

  • 사용자는 JWT 토큰을 서비스 서버에 보낸다
  • 서비스 서버는 인증서버의 JWKS 엔드포인트에서 public key를 가져와, 토큰의 서명을 검증

5. 서명 검증 방식

  • public key로 JWT의 서명을 복호화해서 해시값을 얻고
  • JWT 토큰 안의 데이터로 다시 해시값을 계산
  • 두 해시값이 같으면, 토큰 데이터(클레임)가 변조되지 않았음을 확인
  • 해시값이 동일하다면, 토큰이 위조되지 않았음을 보장하므로 신뢰한다.

 

인그레스 게이트웨이에서의 최종 사용자 인증 및 인가

Istio 워크로드는 ID 제공자에게 인증받아 토큰을 발급받은 최종 사용자의 JWT를 통해 요청을 인증하고 인가할 수 있으며, 일반적으로 인그레스 게이트웨이에서 이를 수행해 유효하지 않은 요청을 조기에 차단한다. 이 과정에서 JWT를 요청에서 제거해, 후속 서비스에서 토큰이 유출되거나 재전송 공격(replay attack)에 악용되는 것을 방지한다.

 

실습환경 전 기존 실습 삭제

#
kubectl delete virtualservice,deployment,service,\
destinationrule,gateway,peerauthentication,authorizationpolicy --all -n istioinaction

#
kubectl delete peerauthentication,authorizationpolicy -n istio-system --all

# 삭제 확인
kubectl get gw,vs,dr,peerauthentication,authorizationpolicy -A


# 실습 환경 배포
kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction
kubectl apply -f services/webapp/kubernetes/webapp.yaml -n istioinaction
cat ch9/enduser/ingress-gw-for-webapp.yaml
kubectl apply -f ch9/enduser/ingress-gw-for-webapp.yaml -n istioinaction

 

 

RequestAuthentication으로 JWT 검증하기

RequestAuthentication 리소스는 JWT 토큰을 검증하고, 유효한 토큰의 클레임을 추출해 필터 메타데이터에 저장한다. 이 필터 메타데이터는 인가 정책(AuthorizationPolicy)이 요청 허용/거부를 결정할 때 근거로 사용된다.

JWT가 있는 요청은 클레임이 메타데이터에 저장되지만, JWT가 없는 요청은 클레임 정보가 없다.

RequestAuthentication 리소스는 인가를 직접 강제하지 않으며, 인가를 위해서는 별도의 AuthorizationPolicy가 필요하다. 즉, 인증(토큰 검증 및 클레임 추출)과 인가(정책 적용)는 분리되어 있으며, 둘 다 설정해야 효과적으로 보안을 적용할 수 있다.

 

RequestAuthentication 리소스 만들기

다음 RequestAuthentication 리소스는 이스티오의 인그레스 게이트웨이에 적용된다. 이는 인그레스 게이트웨이가 auth@istioinaction.io 에서 발급한 토큰을 검증하도록 설정한다.

# cat ch9/enduser/jwt-token-request-authn.yaml 
apiVersion: "security.istio.io/v1beta1"
kind: "RequestAuthentication"
metadata:
  name: "jwt-token-request-authn"
  namespace: istio-system # 적용할 네임스페이스
spec:
  selector:
    matchLabels:
      app: istio-ingressgateway
  jwtRules:
  - issuer: "auth@istioinaction.io" # 발급자 Expected issuer
    jwks: | # 특정 JWKS로 검증
      { "keys":[ {"e":"AQAB","kid":"CU-ADJJEbH9bXl0tpsQWYuo4EwlkxFUHbeJ4ckkakCM","kty":"RSA","n":"zl9VRDbmVvyXNdyoGJ5uhuTSRA2653KHEi3XqITfJISvedYHVNGoZZxUCoiSEumxqrPY_Du7IMKzmT4bAuPnEalbY8rafuJNXnxVmqjTrQovPIerkGW5h59iUXIz6vCznO7F61RvJsUEyw5X291-3Z3r-9RcQD9sYy7-8fTNmcXcdG_nNgYCnduZUJ3vFVhmQCwHFG1idwni8PJo9NH6aTZ3mN730S6Y1g_lJfObju7lwYWT8j2Sjrwt6EES55oGimkZHzktKjDYjRx1rN4dJ5PR5zhlQ4kORWg1PtllWy1s5TSpOUv84OPjEohEoOWH0-g238zIOYA83gozgbJfmQ"}]}
      
#
kubectl apply -f ch9/enduser/jwt-token-request-authn.yaml
kubectl get requestauthentication -A

#
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system --port 8080 -o json
...
        "httpFilters": [
                {
                    "name": "istio.metadata_exchange",
                    "typedConfig": {
                        "@type": "type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm",
                        "config": {
                            "vmConfig": {
                                "runtime": "envoy.wasm.runtime.null",
                                "code": {
                                    "local": {
                                        "inlineString": "envoy.wasm.metadata_exchange"
                                    }
                                }
                            },
                            "configuration": {
                                "@type": "type.googleapis.com/envoy.tcp.metadataexchange.config.MetadataExchange"
                            }
                        }
                    }
                },
                {
                    "name": "envoy.filters.http.jwt_authn",
                    "typedConfig": {
                        "@type": "type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication",
                        "providers": {
                            "origins-0": {
                                "issuer": "auth@istioinaction.io",
                                "localJwks": {
                                    "inlineString": "{ \"keys\":[ {\"e\":\"AQAB\",\"kid\":\"CU-ADJJEbH9bXl0tpsQWYuo4EwlkxFUHbeJ4ckkakCM\",\"kty\":\"RSA\",\"n\":\"zl9VRDbmVvyXNdyoGJ5uhuTSRA2653KHEi3XqITfJISvedYHVNGoZZxUCoiSEumxqrPY_Du7IMKzmT4bAuPnEalbY8rafuJNXnxVmqjTrQovPIerkGW5h59iUXIz6vCznO7F61RvJsUEyw5X291-3Z3r-9RcQD9sYy7-8fTNmcXcdG_nNgYCnduZUJ3vFVhmQCwHFG1idwni8PJo9NH6aTZ3mN730S6Y1g_lJfObju7lwYWT8j2Sjrwt6EES55oGimkZHzktKjDYjRx1rN4dJ5PR5zhlQ4kORWg1PtllWy1s5TSpOUv84OPjEohEoOWH0-g238zIOYA83gozgbJfmQ\"}]}\n"
                                },
                                "payloadInMetadata": "auth@istioinaction.io"
                            }
                        },
                        "rules": [
                            {
                                "match": {
                                    "prefix": "/"
                                },
                                "requires": {
                                    "requiresAny": {
                                        "requirements": [
                                            {
                                                "providerName": "origins-0"
                                            },
                                            {
                                                "allowMissing": {}
                                            }
                                        ]
                                    }
                                }
                            }
                        ],
                        "bypassCorsPreflight": true
                    }
                },
                {
                    "name": "istio_authn",
                    "typedConfig": {
                        "@type": "type.googleapis.com/istio.envoy.config.filter.http.authn.v2alpha1.FilterConfig",
                        "policy": {
                            "origins": [
                                {
                                    "jwt": {
                                        "issuer": "auth@istioinaction.io"
                                    }
                                }
                            ],
                            "originIsOptional": true,
                            "principalBinding": "USE_ORIGIN"
                        },
                        "skipValidateTrustDomain": true
...

 

유효한 발행자의 토큰이 있는 요청은 받아들여진다

#
cat ch9/enduser/user.jwt
USER_TOKEN=$(< ch9/enduser/user.jwt)
jwt decode $USER_TOKEN

# 호출 
curl -H "Authorization: Bearer $USER_TOKEN" \
     -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog

# 로그
docker exec -it myk8s-control-plane istioctl proxy-config log deploy/istio-ingressgateway -n istio-system --level rbac:debug
kubectl logs -n istio-system -l app=istio-ingressgateway -f

 

 

 

유효하지 않은 발행자의 토큰이 있는 요청은 거부된다

#
cat ch9/enduser/not-configured-issuer.jwt
WRONG_ISSUER=$(< ch9/enduser/not-configured-issuer.jwt)
jwt decode $WRONG_ISSUER
...
Token claims
------------
{
  "exp": 4745151548,
  "group": "user",
  "iat": 1591551548,
  "iss": "old-auth@istioinaction.io", # 현재 설정한 정책의 발급자와 다름 issuer: "auth@istioinaction.io" 
  "sub": "79d7506c-b617-46d1-bc1f-f511b5d30ab0"
}
...


# 호출 
curl -H "Authorization: Bearer $WRONG_ISSUER" \
     -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog

# 로그
kubectl logs -n istio-system -l app=istio-ingressgateway -f
[2025-05-04T06:36:22.089Z] "GET /api/catalog HTTP/1.1" 401 - jwt_authn_access_denied{Jwt_issuer_is_not_configured} - "-" 0 28 1 - "172.18.0.1" "curl/8.7.1" "2e183b2e-0968-971d-adbc-6b149171912b" "webapp.istioinaction.io:30000" "-" outbound|80||webapp.istioinaction.svc.cluster.local - 10.10.0.5:8080 172.18.0.1:65436 - -

 

 

토큰이 없는 요청은 클러스터로 받아들여진다

# 호출 
curl -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog

# 로그
kubectl logs -n istio-system -l app=istio-ingressgateway -f

토큰이 없는 요청을 거부하려면, 실제로 다양한 무토큰 요청 상황이 존재하므로 추가적인 처리가 필요하다.

 

 

JWT가 없는 요청 거부하기

JWT가 없는 요청을 거부하려면 requestPrincipals가 없는 source를 명시적으로 거부하는 AuthorizationPolicy를 만들어야 하며, requestPrincipals는 JWT의 issuer와 subject 클레임을 ‘iss/sub’ 형태로 결합해 초기화된다.
이렇게 인증된 클레임은 RequestPrincipals 리소스를 통해 커넥션 메타데이터로 가공되어, AuthorizationPolicy 등에서 활용된다.

 

# cat ch9/enduser/app-gw-requires-jwt.yaml # vi/vim, vscode 에서 포트 30000 추가
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: app-gw-requires-jwt
  namespace: istio-system
spec:
  selector:
    matchLabels:
      app: istio-ingressgateway
  action: DENY
  rules:
  - from:
    - source:
        notRequestPrincipals: ["*"] # 요청 주체에 값이 없는 source는 모두 해당된다
    to:
    - operation:
        hosts: ["webapp.istioinaction.io:30000"] # 이 규칙은 이 특정 호스트에만 적용된다
        ports: ["30000"]

#
kubectl apply -f ch9/enduser/app-gw-requires-jwt.yaml

#
kubectl get AuthorizationPolicy -A
NAMESPACE      NAME                  AGE
istio-system   app-gw-requires-jwt   2m14s

docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system --port 8080 -o json
...
          {
              "name": "envoy.filters.http.rbac",
              "typedConfig": {
                  "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
                  "rules": {
                      "action": "DENY",
                      "policies": {
                          "ns[istio-system]-policy[app-gw-requires-jwt]-rule[0]": {
                              "permissions": [
                                  {
                                      "andRules": {
                                          "rules": [
                                              {
                                                  "orRules": {
                                                      "rules": [
                                                          {
                                                              "header": {
                                                                  "name": ":authority",
                                                                  "stringMatch": {
                                                                      "exact": "webapp.istioinaction.io:30000",
                                                                      "ignoreCase": true
                                                                  }
                                                              }
                                                          }
                                                      ]
                                                  }
                                              }
                                          ]
                                      }
                                  }
                              ],
                              "principals": [
                                  {
                                      "andIds": {
                                          "ids": [
                                              {
                                                  "notId": {
                                                      "orIds": {
                                                          "ids": [
                                                              {
                                                                  "metadata": {
                                                                      "filter": "istio_authn",
                                                                      "path": [
                                                                          {
                                                                              "key": "request.auth.principal"
                                                                          }
                                                                      ],
                                                                      "value": {
                                                                          "stringMatch": {
                                                                              "safeRegex": {
                                                                                  "regex": ".+"
...


# 호출 1
curl -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog
403

# 호출 2
curl -H "Authorization: Bearer $USER_TOKEN" \
     -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog

# 로그
kubectl logs -n istio-system -l app=istio-ingressgateway -f
[2025-05-04T07:04:01.791Z] "GET /api/catalog HTTP/1.1" 403 - rbac_access_denied_matched_policy[ns[istio-system]-policy[app-gw-requires-jwt]-rule[0]] - "-" 0 19 0 - "172.18.0.1" "curl/8.7.1" "41678cf6-6ef8-986e-beb4-4e5af46e7a26" "webapp.istioinaction.io:30000" "-" outbound|80||webapp.istioinaction.svc.cluster.local - 10.10.0.5:8080 172.18.0.1:65424 - -

 

 

 

JWT 클레임에 기반한 다양한 접근 수준

일반 사용자는 API 데이터 읽기만 허용하고, 관리자는 모든 권한을 갖도록 클레임이 다른 토큰 기반 접근 정책을 설정한다.

# 일반 사용자 토큰 : 'group: user' 클레임
jwt decode $(cat ch9/enduser/user.jwt)
...
{
  "exp": 4745145038,
  "group": "user",
  "iat": 1591545038,
  "iss": "auth@istioinaction.io",
  "sub": "9b792b56-7dfa-4e4b-a83f-e20679115d79"
}

# 관리자 토큰 : 'group: admin' 클레임
jwt decode $(cat ch9/enduser/admin.jwt)
...
{
  "exp": 4745145071,
  "group": "admin",
  "iat": 1591545071,
  "iss": "auth@istioinaction.io",
  "sub": "218d3fb9-4628-4d20-943c-124281c80e7b"
}

 

 

#일반 사용자가 webapp 에서 데이터를 읽을 수 있게 허용하도록 AuthorizationPolicy 리소스 설정

# cat ch9/enduser/allow-all-with-jwt-to-webapp.yaml # vi/vim, vscode 에서 포트 30000 추가
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-all-with-jwt-to-webapp
  namespace: istio-system
spec:
  selector:
    matchLabels:
      app: istio-ingressgateway
  action: ALLOW
  rules:
  - from:
    - source:
        requestPrincipals: ["auth@istioinaction.io/*"] # 최종 사용자 요청 주체를 표현 Represents the end-user request principal
    to:
    - operation:
        hosts: ["webapp.istioinaction.io:30000"]
        methods: ["GET"]
        
        
        
        
#관리자에게 모든 작업을 허용하는 AuthorizationPolicy 리소스 설정        
# cat ch9/enduser/allow-mesh-all-ops-admin.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "AuthorizationPolicy"
metadata:
  name: "allow-mesh-all-ops-admin"
  namespace: istio-system
spec:
  selector:
    matchLabels:
      app: istio-ingressgateway
  action: ALLOW
  rules:
  - from:
    - source:
        requestPrincipals: ["auth@istioinaction.io/*"]
    when:
    - key: request.auth.claims[group]
      values: ["admin"] # 이 클레임을 포함한 요청만 허용.

 

 

#실습

#
kubectl apply -f ch9/enduser/allow-all-with-jwt-to-webapp.yaml
kubectl apply -f ch9/enduser/allow-mesh-all-ops-admin.yaml

#
kubectl get authorizationpolicy -A
NAMESPACE      NAME                           AGE
istio-system   allow-all-with-jwt-to-webapp   5s
istio-system   allow-mesh-all-ops-admin       5s
istio-system   app-gw-requires-jwt            34m

#
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system --port 8080 -o json
...
                "policies": {
                    "ns[istio-system]-policy[allow-all-with-jwt-to-webapp]-rule[0]": {
                        "permissions": [
                            {
                                "andRules": {
                                    "rules": [
                                        {
                                            "orRules": {
                                                "rules": [
                                                    {
                                                        "header": {
                                                            "name": ":authority",
                                                            "stringMatch": {
                                                                "exact": "webapp.istioinaction.io:30000",
                                                                "ignoreCase": true
...
                    "ns[istio-system]-policy[allow-mesh-all-ops-admin]-rule[0]": {
                        "permissions": [
                            {
                                "andRules": {
                                    "rules": [
                                        {
                                            "any": true
                                        }
                                    ]
                                }
                            }
                        ],
                        "principals": [
                            {
                                "andIds": {
                                    "ids": [
                                        {
                                            "orIds": {
                                                "ids": [
                                                    {
                                                        "metadata": {
                                                            "filter": "istio_authn",
                                                            "path": [
                                                                {
                                                                    "key": "request.auth.principal"
                                                                }
                                                            ],
                                                            "value": {
                                                                "stringMatch": {
                                                                    "prefix": "auth@istioinaction.io/"
                                                                }
                                                            }
                                                        }
                                                    }
                                                ]
                                            }
                                        },
                                        {
                                            "orIds": {
                                                "ids": [
                                                    {
                                                        "metadata": {
                                                            "filter": "istio_authn",
                                                            "path": [
                                                                {
                                                                    "key": "request.auth.claims"
                                                                },
                                                                {
                                                                    "key": "group"
                                                                }
                                                            ],
                                                            "value": {
                                                                "listMatch": {
                                                                    "oneOf": {
                                                                        "stringMatch": {
                                                                            "exact": "admin"
...

# 수집된 메타데이터를 관찰하고자 서비스 프록시에 rbac 로거 설정
## 기본적으로 envoy rbac 로거는 메타데이터를 로그에 출력하지 않는다. 출력을 위해 로깅 수준을 debug 로 설정하자
docker exec -it myk8s-control-plane istioctl proxy-config log deploy/istio-ingressgateway -n istio-system --level rbac:debug


# 일반유저 : [GET]과 [POST] 호출
USER_TOKEN=$(< ch9/enduser/user.jwt)

curl -H "Authorization: Bearer $USER_TOKEN" \
     -sSl -o /dev/null -w "%{http_code}\n" webapp.istioinaction.io:30000/api/catalog

curl -H "Authorization: Bearer $USER_TOKEN" \
     -XPOST webapp.istioinaction.io:30000/api/catalog \
     --data '{"id": 2, "name": "Shoes", "price": "84.00"}'

# 로그
kubectl logs -n istio-system -l app=istio-ingressgateway -f
...
, dynamicMetadata: filter_metadata {
  key: "envoy.filters.http.jwt_authn"
  value {
    fields {
      key: "auth@istioinaction.io"
      value {
        struct_value {
          fields {
            key: "exp"
            value {
              number_value: 4745145038
            }
          }
          fields {
            key: "group"
            value {
              string_value: "user"
            }
          }
...
[2025-05-04T07:39:27.597Z] "POST /api/catalog HTTP/1.1" 403 - rbac_access_denied_matched_policy[none] - "-" 0 19 1 - "172.18.0.1" "curl/8.7.1" "677a3c73-20a1-935a-b039-e2a8beae9d1b" "webapp.istioinaction.io:30000" "-" outbound|80||webapp.istioinaction.svc.cluster.local - 10.10.0.5:8080 172.18.0.1:57196 - -


# 관리자 : [GET]과 [POST] 호출
ADMIN_TOKEN=$(< ch9/enduser/admin.jwt)

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
     -sSl -o /dev/null -w "%{http_code}\n" webapp.istioinaction.io:30000/api/catalog

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
     -XPOST webapp.istioinaction.io:30000/api/catalog \
     --data '{"id": 2, "name": "Shoes", "price": "84.00"}'

# 로그
kubectl logs -n istio-system -l app=istio-ingressgateway -f

일반 사용자
관리자

 

 

9.5 커스텀 외부 인가 서비스와 통합하기

SPIFFE 기반 인증으로 서비스 신뢰성을 확보한 이스티오는 기본 RBAC 외에, 더 정교한 인가가 필요할 때 프록시가 외부 인가 서비스를 호출하도록 CUSTOM 액션을 활용해 커스텀 인가 메커니즘을 쉽게 확장할 수 있다.

서비스 프록시에 들어온 요청은 프록시가 외부 인가(ExtAuthz) 서비스를 호출하는 동안 잠시 멈추고, 외부 인가 서비스가 허용 또는 거부 메시지를 반환하면 프록시가 인가를 집행한다.

외부 인가 서비스는 애플리케이션 사이드카로 메시 내부에 두거나 메시 외부에 배포할 수 있으며, Envoy의 CheckRequest API를 구현해야 한다.

외부 인가를 사용하면 세밀한 인가 정책을 적용할 수 있지만, 요청 경로에 외부 호출이 추가되어 지연 시간이 늘어나는 트레이드오프가 있으므로 성능 영향도 고려해야 한다.

 

 

외부 인가 실습

# 기존 인증/인가 정책 모두 삭제
kubectl delete authorizationpolicy,peerauthentication,requestauthentication --all -n istio-system

# 실습 애플리케이션 배포
kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction
kubectl apply -f services/webapp/kubernetes/webapp.yaml -n istioinaction
kubectl apply -f services/webapp/istio/webapp-catalog-gw-vs.yaml -n istioinaction
kubectl apply -f ch9/sleep.yaml -n default

# 이스티오 샘플에서 샘플 외부 인가 서비스 배포
docker exec -it myk8s-control-plane bash
-----------------------------------
# 
ls -l istio-$ISTIOV/samples/extauthz/
total 24
-rw-r--r-- 1 root root 4238 Oct 11  2023 README.md
drwxr-xr-x 3 root root 4096 Oct 11  2023 cmd
drwxr-xr-x 2 root root 4096 Oct 11  2023 docker
-rw-r--r-- 1 root root 1330 Oct 11  2023 ext-authz.yaml
-rw-r--r-- 1 root root 2369 Oct 11  2023 local-ext-authz.yaml

cat istio-$ISTIOV/samples/extauthz/ext-authz.yaml
apiVersion: v1
kind: Service
metadata:
  name: ext-authz
  labels:
    app: ext-authz
spec:
  ports:
  - name: http
    port: 8000
    targetPort: 8000
  - name: grpc
    port: 9000
    targetPort: 9000
  selector:
    app: ext-authz
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ext-authz
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ext-authz
  template:
    metadata:
      labels:
        app: ext-authz
    spec:
      containers:
      - image: gcr.io/istio-testing/ext-authz:latest
        imagePullPolicy: IfNotPresent
        name: ext-authz
        ports:
        - containerPort: 8000
        - containerPort: 9000

kubectl apply -f istio-$ISTIOV/samples/extauthz/ext-authz.yaml -n istioinaction

# 빠져나오기
exit
-----------------------------------

# 설치 확인 : ext-authz
kubectl get deploy,svc ext-authz -n istioinaction
NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ext-authz   1/1     1            1           72s

NAME                TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)             AGE
service/ext-authz   ClusterIP   10.200.1.172   <none>        8000/TCP,9000/TCP   72s

# 로그
kubectl logs -n istioinaction -l app=ext-authz -c ext-authz -f

배포한 ext-authz 서비스는 요청에 x-ext-authz: allow 헤더가 있으면 허용하고 없으면 거부하며, 필요에 따라 더 복잡한 외부 인가 로직도 구현할 수 있다.

 

 

이스티오에 외부 인가 설정하기

# includeHeadersInCheck (DEPRECATED)
KUBE_EDITOR="nano" kubectl edit -n istio-system cm istio
--------------------------------------------------------
...
    extensionProviders:
    - name: "sample-ext-authz-http"
      envoyExtAuthzHttp:
        service: "ext-authz.istioinaction.svc.cluster.local"
        port: "8000"
        includeRequestHeadersInCheck: ["x-ext-authz"]
...
--------------------------------------------------------

# 확인
kubectl describe -n istio-system cm istio

이스티오가 새로운 외부 인가 서비스를 인식하도록 하려면, istio-system 네임스페이스의 istio configmap에서 meshConfig의 extensionProviders 항목에 해당 외부 인가 서비스 정보를 추가해야 한다.

예를 들어, sample-ext-authz-http라는 이름으로 HTTP 타입의 외부 인가 서비스를 등록하고, 서비스 주소와 포트, 그리고 전달할 헤더(x-ext-authz)를 명시한다. 이 설정은 kubectl edit -n istio-system cm istio 명령어로 configmap을 수정해 적용할 수 있다.

등록된 extensionProviders는 AuthorizationPolicy 리소스에서 provider 이름으로 참조하여 커스텀 인가 정책을 적용할 수 있다.

마지막으로, 외부 인가 서비스가 실제로 요청에 포함된 헤더를 활용해 인가 결과를 반환하게 되며, 이를 통해 이스티오가 외부 인가를 연동해 동작한다.

 

커스텀 AuthorizationPolicy 리소스 사용하기

action 이 CUSTOMAuthorizationPolicy 를 만들고 정확히 어떤 외부 인가 서비스를 사용할지 지정해본다.

# 아래 AuthorizationPolicy 는 istioinaction 네임스페이스에 webapp 워크로드에 적용되며, 
# sample-ext-authz-http 이라는 외부 인가 서비스에 위임한다.
cat << EOF | kubectl apply -f -
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: ext-authz
  namespace: istioinaction
spec:
  selector:
    matchLabels:
      app: webapp
  action: CUSTOM    # custom action 사용
  provider:
    name: sample-ext-authz-http  # meshconfig 이름과 동일해야 한다
  rules:
  - to:
    - operation:
        paths: ["/*"]  # 인가 정책을 적용할 경로
EOF

#
kubectl get AuthorizationPolicy -A
NAMESPACE       NAME        AGE
istioinaction   ext-authz   98s
#호출확인
docker exec -it myk8s-control-plane istioctl proxy-config log deploy/webapp -n istioinaction --level rbac:debug
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
kubectl logs -n istioinaction -l app=ext-authz -c ext-authz -f


# 헤더 없이 호출
kubectl -n default exec -it deploy/sleep -- curl webapp.istioinaction/api/catalog
denied by ext_authz for not found header `x-ext-authz: allow` in the request

kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
2025-05-04T08:33:04.765006Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:114      checking request: requestedServerName: , sourceIP: 10.10.0.18:55834, directRemoteIP: 10.10.0.18:55834, remoteIP: 10.10.0.18:55834,localAddress: 10.10.0.20:8080, ssl: none, headers: ':authority', 'webapp.istioinaction'
':path', '/api/catalog'
':method', 'GET'
':scheme', 'http'
'user-agent', 'curl/8.5.0'
'accept', '*/*'
'x-forwarded-proto', 'http'
'x-request-id', 'ffd44f00-19ff-96b7-868b-8f6b09bd447d'
, dynamicMetadata:      thread=31
2025-05-04T08:33:04.765109Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:130      shadow denied, matched policy istio-ext-authz-ns[istioinaction]-policy[ext-authz]-rule[0]thread=31
2025-05-04T08:33:04.765170Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:167      no engine, allowed by default      thread=31
[2025-05-04T08:33:04.764Z] "GET /api/catalog HTTP/1.1" 403 UAEX ext_authz_denied - "-" 0 76 5 4 "-" "curl/8.5.0" "ffd44f00-19ff-96b7-868b-8f6b09bd447d" "webapp.istioinaction" "-" inbound|8080|| - 10.10.0.20:8080 10.10.0.18:55834 - -

kubectl logs -n istioinaction -l app=ext-authz -c ext-authz -f
2025/05/04 08:35:26 [HTTP][denied]: GET webapp.istioinaction/api/catalog, headers: map[Content-Length:[0] X-B3-Parentspanid:[58148c96f61496a3] X-B3-Sampled:[1] X-B3-Spanid:[960b8d911e81c217] X-B3-Traceid:[ce6c5622c32fd238a934fbf1aa4a9de0] X-Envoy-Expected-Rq-Timeout-Ms:[600000] X-Envoy-Internal:[true] X-Forwarded-Client-Cert:[By=spiffe://cluster.local/ns/istioinaction/sa/default;Hash=491c5bf23be281a5c0c2e798eba242461dfdb7b178d4a4cd842f9eedb05ae47d;Subject="";URI=spiffe://cluster.local/ns/istioinaction/sa/webapp] X-Forwarded-For:[10.10.0.20] X-Forwarded-Proto:[https] X-Request-Id:[964138e3-d955-97c9-b9a5-dfc88cc7f9c5]], body: []


# 헤더 적용 호출
kubectl -n default exec -it deploy/sleep -- curl -H "x-ext-authz: allow" webapp.istioinaction/api/catalog

kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
2025-05-04T08:37:40.618775Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:114        checking request: requestedServerName: , sourceIP: 10.10.0.18:36150, directRemoteIP: 10.10.0.18:36150, remoteIP: 10.10.0.18:36150,localAddress: 10.10.0.20:8080, ssl: none, headers: ':authority', 'webapp.istioinaction'
':path', '/api/catalog'
':method', 'GET'
':scheme', 'http'
'user-agent', 'curl/8.5.0'
'accept', '*/*'
'x-ext-authz', 'allow'
'x-forwarded-proto', 'http'
'x-request-id', 'b446ddf8-fb2e-9dd7-ba01-6e31fac717da'
, dynamicMetadata:      thread=30
2025-05-04T08:37:40.618804Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:130        shadow denied, matched policy istio-ext-authz-ns[istioinaction]-policy[ext-authz]-rule[0] thread=30
2025-05-04T08:37:40.618816Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:167        no engine, allowed by default     thread=30
[2025-05-04T08:37:40.622Z] "GET /items HTTP/1.1" 200 - via_upstream - "-" 0 502 2 2 "-" "beegoServer" "b446ddf8-fb2e-9dd7-ba01-6e31fac717da" "catalog.istioinaction:80" "10.10.0.19:3000" outbound|80||catalog.istioinaction.svc.cluster.local 10.10.0.20:60848 10.200.1.165:80 10.10.0.20:45874 - default
[2025-05-04T08:37:40.618Z] "GET /api/catalog HTTP/1.1" 200 - via_upstream - "-" 0 357 6 4 "-" "curl/8.5.0" "b446ddf8-fb2e-9dd7-ba01-6e31fac717da" "webapp.istioinaction" "10.10.0.20:8080" inbound|8080|| 127.0.0.6:43721 10.10.0.20:8080 10.10.0.18:36150 - default

kubectl logs -n istioinaction -l app=ext-authz -c ext-authz -f
2025/05/04 08:36:34 [HTTP][allowed]: GET webapp.istioinaction/api/catalog, headers: map[Content-Length:[0] X-B3-Parentspanid:[f9bc85c800aaaa05] X-B3-Sampled:[1] X-B3-Spanid:[bf6cc58161f7ca25] X-B3-Traceid:[af1c826a362ce0382e219cd21afe1fe7] X-Envoy-Expected-Rq-Timeout-Ms:[600000] X-Envoy-Internal:[true] X-Ext-Authz:[allow] X-Forwarded-Client-Cert:[By=spiffe://cluster.local/ns/istioinaction/sa/default;Hash=491c5bf23be281a5c0c2e798eba242461dfdb7b178d4a4cd842f9eedb05ae47d;Subject="";URI=spiffe://cluster.local/ns/istioinaction/sa/webapp] X-Forwarded-For:[10.10.0.20] X-Forwarded-Proto:[https] X-Request-Id:[c9b43ce7-25d4-94ae-b684-1565ad36f533]], body: []

 

 

 

 

정리

PeerAuthentication (피어 인증 정책)

  • PeerAuthentication은 서비스 간 통신에서 상호 TLS(mTLS) 사용 여부와 수준을 정의하는 정책. 이 정책을 통해 서비스 간 트래픽이 암호화되어, 네트워크 상에서 데이터가 노출되거나 도청되는 것을 방지.
  • STRICT 모드를 적용하면, 해당 워크로드(또는 네임스페이스, 또는 전체 mesh)로 들어오는 모든 트래픽에 대해 반드시 mTLS가 적용되어야 하며, 평문 트래픽은 거부. 이를 통해 외부에서 암호화되지 않은 트래픽이 유입되는 것을 원천적으로 차단
  • 반면, PERMISSIVE 모드는 mTLS와 평문 트래픽을 동시에 허용. 이를 활용하면, 기존에 평문 트래픽을 사용하던 환경에서 다운타임 없이 점진적으로 mTLS로 마이그레이션할 수 있다.

AuthorizationPolicy (인가 정책)

  • AuthorizationPolicy는 서비스 간 요청이나 최종 사용자 요청에 대해 허용(Allow) 또는 거부(Deny) 정책을 정의.
  • 이 정책은 워크로드의 ID 인증서(예: SPIFFE ID) 또는 최종 사용자의 JWT에서 추출한 검증 가능한 메타데이터(클레임 등)를 기반으로 동작. 즉, 서비스 또는 사용자 신원에 따라 세밀한 접근 제어가 가능.
  • 정책은 ALLOW, DENY, AUDIT 등 다양한 액션을 지원하며, 여러 정책이 동시에 적용될 경우 DENY가 우선 적용.
  • AuthorizationPolicy는 기본적으로 Istio 프록시가 자체적으로 인가를 집행하지만, 외부 인가 시스템과 연동할 수도 있다.

RequestAuthentication (요청 인증 정책)

  • RequestAuthentication은 JWT가 포함된 최종 사용자 요청을 인증하는 데 사용.
  • 이 정책을 통해 JWT의 위치, 발급자(issuer), 공개키(JWKS) 등 검증에 필요한 정보를 지정할 수 있다. Istio는 요청에 포함된 JWT가 정책에 부합하는지 검증하고, 유효하지 않은 토큰이 있으면 요청을 거부.
  • 단, 토큰이 없는 요청은 기본적으로 허용. 토큰이 없는 요청을 명시적으로 거부하려면 AuthorizationPolicy에서 별도의 규칙을 추가해야 한다.

AuthorizationPolicy의 CUSTOM 액션 (외부 인가 서비스 연동)

  • Istio의 AuthorizationPolicy는 CUSTOM 액션을 통해 외부 인가 서비스(External Authorization Service) 와 연동할 수 있다.
  • CUSTOM 액션을 사용하면, 프록시가 요청을 받을 때 내부 정책이 아닌 외부 인가 서비스로 요청 정보를 전달하고, 인가 결과(허용/거부)를 받아 집행한다. 이를 통해 조직 고유의 인가 로직이나 타사 인가 시스템(예: OPA, 자체 인가 서버 등)과 쉽게 통합할 수 있다.
  • 외부 인가 서비스는 Istio meshconfig의 extensionProviders에 등록되어야 하며, AuthorizationPolicy에서 provider 이름으로 지정.
  • 이 방식은 Istio의 기본 인가 기능으로는 커버할 수 없는 복잡한 요구사항이나, 외부 시스템과의 통합이 필요한 경우에 유용.

요약 


 

PeerAuthentication 피어(서비스 간) 인증 및 트래픽 암호화 STRICT: mTLS만 허용, PERMISSIVE: mTLS+평문 허용, 점진적 마이그레이션 지원
AuthorizationPolicy 서비스/사용자 요청 인가(허용/거부) 워크로드 ID, JWT 등 메타데이터 기반 세밀한 접근 제어, ALLOW/DENY/AUDIT 지원
RequestAuthentication 최종 사용자 JWT 인증 JWT 유효성 검증, 토큰 없는 요청은 기본 허용(추가 정책 필요)
AuthorizationPolicy-CUSTOM 외부 인가 서비스 연동 외부 인가 시스템과 통합, 복잡한 인가 로직 지원
 

이처럼 Istio는 다양한 인증·인가 정책을 조합해 서비스 메시 내 트래픽의 신뢰성, 기밀성, 접근 제어를 유연하고 강력하게 구현할 수 있습니다.

9.1 애플리케이션 네트워크 보안의 필요성

  • 애플리케이션 보안은 인가되지 않은 사용자가 중요한 데이터를 훼손, 탈취, 무단 접근하지 못하도록 보호하는 모든 활동을 의미한다.
  • 이를 위해 사용자 인증·인가 절차와, 네트워크를 오가는 데이터의 암호화가 필수적이다.
  • 인증은 사용자의 정체를 확인하는 과정이고, 인가는 인증된 사용자의 리소스 접근 권한을 판단하는 절차다.

서비스 간 인증 Service-to-service authentication - SPIFFE 프레임워크

  • SPIFFE는 서비스 간 신뢰를 위해 제3자가 발급한 짧은 수명의 암호화된 ID 문서(SVID)를 자동으로 제공하고, 이를 통해 서비스들이 서로를 인증한다.
  • 이 ID는 TLS 연결이나 JWT 서명 검증 등에 사용되어, 서비스 간 안전한 상호 인증을 구현한다.

 

최종 사용자 인증 End-user authentication - JWT 등 자격증명

최종 사용자 인증은 사용자가 인증 서버에서 받은 자격 증명을 서비스에 제시하고, 서비스는 이를 인증 서버에 검증하여 접근을 허용하는 절차다.

 

인가 Authorization - 작업 수행 승인/거부

인가는 인증된 사용자의 ID를 바탕으로 수행 가능한 작업을 확인하여 승인 또는 거부하는 절차이며, 이스티오는 이를 세분화하여 제공한다.

 

MSA에서의 인증/인가 

마이크로서비스 환경에서는 서비스가 동적으로 생성·소멸되고 여러 네트워크와 클라우드에 분산되기 때문에, IP 주소 기반 식별 방식은 신뢰할 수 없다.

이스티오는 SPIFFE 오픈소스 표준을 도입해, 다양한 환경에서 각 워크로드에 고유하고 검증 가능한 ID를 자동으로 부여함으로써 안전한 서비스 식별과 인증을 지원한다.

 

Istio에서의 인증/인가 

이스티오의 보안은 PeerAuthentication, RequestAuthentication, AuthorizationPolicy 세 가지 커스텀 리소스 기반으로 구성된다.

PeerAuthentication은 서비스 간 트래픽에 대해 mTLS 인증을 적용하고, 상대 서비스의 인증서 정보를 추출한다.

RequestAuthentication은 최종 사용자의 자격 증명(JWT 등)을 검증하며, 인증된 사용자 정보를 추출한다.

AuthorizationPolicy는 앞서 추출된 서비스 또는 사용자 정보를 바탕으로 요청을 허용하거나 거부하는 인가 정책을 설정한다.

이 세 리소스는 프록시가 요청의 ID 정보를 필터 메타데이터로 저장하고, 인가 정책에 따라 세분화된 접근 제어를 실현하게 한다.

 

 

9.2 자동 상호 TLS (Auto mTLS)

  • 이스티오는 프록시가 주입된 서비스 간 트래픽을 자동으로 암호화하고 상호 인증하며, 인증서 발급·갱신을 자동화해 휴먼 에러와 서비스 중단을 예방한다.
  • mTLS와 최소 권한 정책을 적용하면 기본적으로 안전한 상태를 유지할 수 있지만, 메시 전체를 더 안전하게 만들기 위해서는 추가적인 보안 작업이 필요하다.

 

실습환경

#mTLS 실습 환경
# catalog와 webapp 배포
kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction
kubectl apply -f services/webapp/kubernetes/webapp.yaml -n istioinaction

# webapp과 catalog의 gateway, virtualservice 설정
kubectl apply -f services/webapp/istio/webapp-catalog-gw-vs.yaml -n istioinaction

# default 네임스페이스에 sleep 앱 배포
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

kubectl apply -f ch9/sleep.yaml -n default

# 확인
kubectl get deploy,pod,sa,svc,ep
kubectl get deploy,svc -n istioinaction
kubectl get gw,vs -n istioinaction

 

 

이스티오의 PeerAuthentication 리소스 이해하기

PeerAuthentication 리소스는 mTLS 인증 모드를 서비스 메시 전체, 네임스페이스, 또는 특정 워크로드 단위로 엄격(STRICT) 또는 허용(PERMISSIVE)하게 설정할 수 있다.

 
 
 
메시 범위 정책으로 모든 미인증 트래픽 거부하기
#
cat ch9/meshwide-strict-peer-authn.yaml 
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "default" # Mesh-wide policies must be named "default"
  namespace: "istio-system" # Istio installation namespace
spec:
  mtls:
    mode: STRICT # mutual TLS mode

# 적용
kubectl apply -f ch9/meshwide-strict-peer-authn.yaml -n istio-system

# 요청 실행
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

# 확인
kubectl get PeerAuthentication -n istio-system
kubectl logs -n istioinaction -l app=webapp -c webapp -f
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
[2025-05-01T08:32:08.511Z] "- - -" 0 NR filter_chain_not_found - "-" 0 0 0 - "-" "-" "-" "-" "-" - - 10.10.0.17:8080 10.10.0.16:51930 - -
[2025-05-01T08:32:10.629Z] "- - -" 0 NR filter_chain_not_found - "-" 0 0 0 - "-" "-" "-" "-" "-" - - 10.10.0.17:8080 10.10.0.16:53366 - -
# NR → Non-Route. Envoy에서 라우팅까지 가지 못한 단계에서 발생한 에러라는 의미입니다.
# filter_chain_not_found → 해당 Listener에서 제공된 SNI(Server Name Indication), IP, 포트, ALPN 등의 조건에 맞는 filter_chain이 설정에 없다는 뜻입니다.

 

 

상호 인증하기 않은 트래픽 허용하기

네임스페이스 범위 PeerAuthentication 정책은 메시 전체 정책을 덮어쓸 수 있어, 특정 네임스페이스의 워크로드에 맞는 인증 요구사항을 유연하게 적용할 수 있다.

 
cat << EOF | kubectl apply -f -
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "default"             # Uses the "default" naming convention so that only one namespace-wide resource exists
  namespace: "istioinaction"  # Specifies the namespace to apply the policy
spec:
  mtls:
    mode: PERMISSIVE          # PERMISSIVE allows HTTP traffic.
EOF


# 요청 실행
kubectl exec deploy/sleep -c sleep -- curl -s http://webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"

# 확인
kubectl get PeerAuthentication -A 
NAMESPACE       NAME      MODE         AGE
istio-system    default   STRICT       2m51s
istioinaction   default   PERMISSIVE   7s

kubectl logs -n istioinaction -l app=webapp -c webapp -f
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f

# 다음 실습을 위해 삭제 : PeerAuthentication 단축어 pa
kubectl delete pa default -n istioinaction
 
 
 
워크로드별 PeerAuthentication 정책 적용하기

워크로드 셀렉터를 지정해 PeerAuthentication 정책을 webapp에만 적용하고, 네임스페이스 전체가 아닌 경우 정책 이름을 'webapp'으로 변경하는 것이 권장된다.

# istiod 는 PeerAuthentication 리소스 생성을 수신하고, 이 리소스를 엔보이용 설정으로 변환하며, 
# LDS(Listener Discovery Service)를 사용해 서비스 프록시에 적용
docker exec -it myk8s-control-plane istioctl proxy-status
kubectl logs -n istio-system -l app=istiod -f
...
2025-05-01T09:48:32.854911Z     info    ads     LDS: PUSH for node:catalog-6cf4b97d-2r9bn.istioinaction resources:23 size:85.4kB
2025-05-01T09:48:32.855510Z     info    ads     LDS: PUSH for node:webapp-7685bcb84-jcg7d.istioinaction resources:23 size:94.0kB
...

#
cat ch9/workload-permissive-peer-authn.yaml
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "webapp"
  namespace: "istioinaction"
spec:
  selector:
    matchLabels:
      app: "webapp"  # 레이블이 일치하는 워크로드만 PERMISSIVE로 동작
  mtls:
    mode: PERMISSIVE

kubectl apply -f ch9/workload-permissive-peer-authn.yaml
kubectl get pa -A

# 요청 실행
kubectl logs -n istioinaction -l app=webapp -c webapp -f
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
kubectl exec deploy/sleep -c sleep -- curl -s http://webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"

#
kubectl logs -n istioinaction -l app=catalog -c catalog -f
kubectl logs -n istioinaction -l app=catalog -c istio-proxy -f
kubectl exec deploy/sleep -c sleep -- curl -s http://catalog.istioinaction/api/items -o /dev/null -w "%{http_code}\n"
2025-05-01T09:32:00.197Z] "- - -" 0 NR filter_chain_not_found - "-" 0 0 0 - "-" "-" "-" "-" "-" - - 10.10.0.18:3000 10.10.0.16:33192 - -
...

 

 

tcpdump로 서비스 간 트래픽 스니핑하기

# 패킷 모니터링 실행 해두기
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)'
# -l : 표준 출력(stdout)을 라인 버퍼 모드로 설정. 터미널에서 실시간으로 결과를 보기 좋게 함 (pipe로 넘길 때도 유용).
# --immediate-mode : 커널 버퍼에서 패킷을 모아서 내보내지 않고, 캡처 즉시 사용자 공간으로 넘김 → 딜레이 최소화.
# -vv : verbose 출력. 패킷에 대한 최대한의 상세 정보를 보여줌.
# -s 0 : snap length를 0으로 설정 → 패킷 전체 내용을 캡처. (기본값은 262144 bytes, 예전 버전에서는 68 bytes로 잘렸음)
# '(((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0) and not (port 53)' : DNS패킷 제외하고 TCP payload 길이가 0이 아닌 패킷만 캡처
# 즉, SYN/ACK/FIN 같은 handshake 패킷(데이터 없는 패킷) 무시, 실제 데이터 있는 패킷만 캡처
# 결론 : 지연 없이, 전체 패킷 내용을, 매우 자세히 출력하고, DNS패킷 제외하고 TCP 데이터(payload)가 1 byte 이상 있는 패킷만 캡처

# 요청 실행
kubectl exec deploy/sleep -c sleep -- curl -s webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
...
## (1) sleep -> webapp 호출 HTTP
14:07:24.926390 IP (tos 0x0, ttl 63, id 63531, offset 0, flags [DF], proto TCP (6), length 146)
    10-10-0-16.sleep.default.svc.cluster.local.32828 > webapp-7685bcb84-hp2kl.http-alt: Flags [P.], cksum 0x14bc (incorrect -> 0xa83b), seq 2741788650:2741788744, ack 3116297176, win 512, options [nop,nop,TS val 490217013 ecr 2804101520], length 94: HTTP, length: 94
	GET /api/catalog HTTP/1.1
	Host: webapp.istioinaction
	User-Agent: curl/8.5.0
	Accept: */*

## (2) webapp -> catalog 호출 HTTPS
14:07:24.931647 IP (tos 0x0, ttl 64, id 18925, offset 0, flags [DF], proto TCP (6), length 1304)
    webapp-7685bcb84-hp2kl.37882 > 10-10-0-19.catalog.istioinaction.svc.cluster.local.3000: Flags [P.], cksum 0x1945 (incorrect -> 0x9667), seq 2146266072:2146267324, ack 260381029, win 871, options [nop,nop,TS val 1103915113 ecr 4058175976], length 1252

## (3) catalog -> webapp 응답 HTTPS
14:07:24.944769 IP (tos 0x0, ttl 63, id 7029, offset 0, flags [DF], proto TCP (6), length 1789)
    10-10-0-19.catalog.istioinaction.svc.cluster.local.3000 > webapp-7685bcb84-hp2kl.37882: Flags [P.], cksum 0x1b2a (incorrect -> 0x2b6f), seq 1:1738, ack 1252, win 729, options [nop,nop,TS val 4058610491 ecr 1103915113], length 1737

## (4) webapp -> sleep 응답 HTTP
14:07:24.946168 IP (tos 0x0, ttl 64, id 13699, offset 0, flags [DF], proto TCP (6), length 663)
    webapp-7685bcb84-hp2kl.http-alt > 10-10-0-16.sleep.default.svc.cluster.local.32828: Flags [P.], cksum 0x16c1 (incorrect -> 0x37d1), seq 1:612, ack 94, win 512, options [nop,nop,TS val 2804101540 ecr 490217013], length 611: HTTP, length: 611
	HTTP/1.1 200 OK
	content-length: 357
	content-type: application/json; charset=utf-8
	date: Thu, 01 May 2025 14:07:24 GMT
	x-envoy-upstream-service-time: 18
	server: istio-envoy
	x-envoy-decorator-operation: webapp.istioinaction.svc.cluster.local:80/*

	[{"id":1,"color":"amber","department":"Eyewear","name":"Elinor Glasses","price":"282.00"},{"id":2,"color":"cyan","department":"Clothing","name":"Atlas Shirt","price":"127.00"},{"id":3,"color":"teal","department":"Clothing","name":"Small Metal Shoes","price":"232.00"},{"id":4,"color":"red","department":"Watches","name":"Red Dragon Watch","price":"232.00"}] [|http]
...

 

 

워크로드 ID가 워크로드 서비스 어카운트에 연결돼 있는지 확인하기

openssl 명령어로 catalog 워크로드의 X.509 인증서를 확인하면, SVID 문서의 유효성, SPIFFE ID가 SAN(Subject Alternative Name) 필드에 URI로 인코딩되어 있는지, 그리고 해당 ID가 워크로드 서비스 어카운트와 일치하는지 직접 검증할 수 있다.

# (참고) 패킷 모니터링 : 아래 openssl 실행 시 동작 확인
kubectl exec -it -n istioinaction deploy/catalog -c istio-proxy \
  -- sudo tcpdump -l --immediate-mode -vv -s 0 'tcp port 3000'
  

# catalog 의 X.509 인증서 내용 확인
kubectl -n istioinaction exec deploy/webapp -c istio-proxy -- ls -l /var/run/secrets/istio/root-cert.pem
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- openssl x509 -in /var/run/secrets/istio/root-cert.pem -text -noout
...

kubectl -n istioinaction exec deploy/webapp -c istio-proxy -- openssl -h
kubectl -n istioinaction exec deploy/webapp -c istio-proxy -- openssl s_client -h

# openssl s_client → TLS 서버에 연결해 handshake와 인증서 체인을 보여줌
# -showcerts → 서버가 보낸 전체 인증서 체인 출력
# -connect catalog.istioinaction.svc.cluster.local:80 → Istio 서비스 catalog로 TCP 80 연결
# -CAfile /var/run/secrets/istio/root-cert.pem → Istio의 root CA로 서버 인증서 검증
# 결론 : Envoy proxy에서 catalog 서비스로 연결하여 TLS handshake 및 인증서 체인 출력 후 사람이 읽을 수 있는 형식으로 해석
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
...
        Validity 
            Not Before: May  1 09:55:10 2025 GMT # 유효기간 1일 2분
            Not After : May  2 09:57:10 2025 GMT
        ...
        X509v3 extensions:
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication # 사용처 : 웹서버, 웹클라이언트
            ...
            X509v3 Subject Alternative Name: critical
                URI:spiffe://cluster.local/ns/istioinaction/sa/catalog # SPIFFE ID 확인

# catalog 파드의 서비스 어카운트 확인
kubectl describe pod -n istioinaction -l app=catalog | grep 'Service Account'
Service Account:  catalog

 

 

이스티오 보안: SPIFFE

  • PKI(공개 키 인프라)는 서버와 클라이언트가 안전하게 통신할 수 있도록 디지털 인증서(X.509)를 발급·검증하는 표준화된 프레임워크다.
  • 인증서에는 공개 키와 소유자 신원 정보가 포함되며, 서버는 이를 통해 자신의 정체를 증명한다.
  • TLS 프로토콜에서는 핸드셰이크 과정에서 서버가 X.509 인증서를 제시하고, 클라이언트가 신뢰 체인을 검증한다.
  • 검증에 성공하면 클라이언트는 서버의 공개 키로 암호화한 세션 키(대칭 키)를 서버에 전달하고, 서버는 개인 키로 복호화한다.
  • 이렇게 안전하게 교환된 대칭 키로 이후 트래픽을 암호화해 성능과 보안을 모두 확보한다.
  • 최종 사용자 인증은 주로 세션 쿠키나 수명이 짧은 JWT를 발급해, 후속 요청에서 사용자를 인증하는 방식으로 구현된다.
  • 이스티오 등 현대 서비스 메시 환경에서도 JWT 기반의 최종 사용자 인증과 PKI 기반 서비스 인증이 함께 활용된다.

SPIFFE: 모든 이를 위한 안전한 운영 환경 ID 프레임워크

  • SPIFFE ID는 spiffe://trust-domain/path 형식의 RFC 3986 호환 URI로, trust-domain은 발급자를, path는 워크로드를 고유하게 식별한다.
  • path의 구체적 구조는 SPIFFE 구현체가 결정하며, 예를 들어 이스티오는 쿠버네티스 서비스 어카운트를 path로 활용한다.
  • Workload API는 워크로드가 SPIFFE ID가 포함된 SVID(인증서)를 요청·발급받을 수 있도록 표준 gRPC 엔드포인트를 제공한다.
  • 이 API는 워크로드가 자체 비밀을 보유하지 않도록 설계되었으며, 안전하게 SVID를 수령하고, 상호 인증이나 메시지 서명 등에 활용할 수 있게 한다.
  • 워크로드 엔드포인트는 SPIFFE 데이터플레인 구성요소로, 워크로드의 무결성을 attestation(증명)하고 SPIFFE ID가 포함된 CSR을 생성해 워크로드 API에 제출한다.
  • 워크로드 API(예: Istio의 istiod)는 CSR을 검증·서명해 SPIFFE ID가 포함된 SVID(X.509 인증서)를 발급하며, 이 인증서는 SAN 필드에 SPIFFE ID가 URI로 인코딩된다.
  • 이스티오에서는 워크로드 엔드포인트를 프록시(pilot-agent)가, 워크로드 API를 Istio CA(istiod)가 구현하며, 프록시가 ID 부트스트랩과 인증서 수령을 자동화한다.
  • 결과적으로 모든 워크로드는 자동으로 고유한 SVID를 받아 상호 인증과 자동 mTLS 기반 암호화 통신이 가능해진다.

워크로드 ID의 단계별 부트스트랩

  • 쿠버네티스에서 초기화된 모든 파드에는 /var/run/secrets/kubernetes.io/serviceaccount/ 경로에 서비스 어카운트 토큰, 클러스터 CA 인증서, 네임스페이스 정보가 포함된 시크릿이 기본적으로 마운트된다.
  • 이 중 서비스 어카운트 토큰은 파드의 ID와 권한 정보를 담고 있으며, 서명되어 있어 페이로드를 임의로 수정할 수 없고, 쿠버네티스 API와의 안전한 통신 및 워크로드 ID 부트스트랩에 핵심적으로 사용된다.
#
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets/kubernetes.io/serviceaccount/
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
TOKEN=$(kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# 헤더 디코딩 
echo $TOKEN | cut -d '.' -f1 | base64 --decode | sed 's/$/}/' | jq
{
  "alg": "RS256",
  "kid": "nKgUYnbjH9BmgEXYbu56GFoBxwDF_jF9Q6obIWvinAM"
}

# 페이로드 디코딩
echo $TOKEN | cut -d '.' -f2 | base64 --decode | sed 's/$/}/' | jq
{
  "aud": [
    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1777689454,
  "iat": 1746153454,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "kubernetes.io": {
    "namespace": "istioinaction",
    "pod": {
      "name": "webapp-7685bcb84-hp2kl",
      "uid": "98444761-1f47-45ad-b739-da1b7b22013a"
    },
    "serviceaccount": {
      "name": "webapp",
      "uid": "5a27b23e-9ed6-46f7-bde0-a4e4684949c2"
    },
    "warnafter": 1746157061
  },
  "nbf": 1746153454,
  "sub": "system:serviceaccount:istioinaction:webapp"
}

# (옵션) brew install jwt-cli  # Linux 툴 추천 부탁드립니다.
jwt decode $TOKEN
Token header
------------
{
  "alg": "RS256",
  "kid": "nKgUYnbjH9BmgEXYbu56GFoBxwDF_jF9Q6obIWvinAM"
}

Token claims
------------
{
  "aud": [ # 이 토큰의 대상(Audience) : 토큰이 어떤 API나 서비스에서 사용될 수 있는지 정의 -> k8s api가 aud 가 일치하는지 검사하여 올바른 토큰인지 판단.
    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1777689454, # 토큰 만료 시간 Expiration Time (Unix timestamp, 초 단위) , date -r 1777689454 => (1년) Sat May  2 11:37:34 KST 2026
  "iat": 1746153454, # 토큰 발급 시간 Issued At (Unix timestamp), date -r 1746153454 => Fri May  2 11:37:34 KST 2025
  "iss": "https://kubernetes.default.svc.cluster.local", # Issuer, 토큰을 발급한 주체, k8s api가 발급
  "kubernetes.io": {
    "namespace": "istioinaction",
    "pod": {
      "name": "webapp-7685bcb84-hp2kl",
      "uid": "98444761-1f47-45ad-b739-da1b7b22013a" # 파드 고유 식별자
    },
    "serviceaccount": {
      "name": "webapp",
      "uid": "5a27b23e-9ed6-46f7-bde0-a4e4684949c2" # 서비스 어카운트 고유 식별자
    },
    "warnafter": 1746157061 # 이 시간 이후에는 새로운 토큰을 요청하라는 Kubernetes의 신호 (토큰 자동 갱신용) date -r 1746157061 (1시간) => Fri May  2 12:37:41 KST 2025
  },
  "nbf": 1746153454, # Not Before, 이 시간 이전에는 토큰이 유효하지 않음. 보통 iat와 동일하게 설정됩니다.
  "sub": "system:serviceaccount:istioinaction:webapp" # 토큰의 주체(Subject)
}

# sa 에 토큰 유효 시간 3600초 = 1시간 + 7초
kubectl get pod -n istioinaction -l app=webapp -o yaml
...
    - name: kube-api-access-nt4qb
      projected:
        defaultMode: 420
        sources:
        - serviceAccountToken:
            expirationSeconds: 3607
            path: token
...

 

 

파일럿 에이전트는 쿠버네티스 서비스 어카운트 토큰을 디코딩해 SPIFFE ID를 생성하고, 이 ID가 포함된 CSR과 토큰을 이스티오 CA에 전송한다.
이스티오 CA는 TokenReview API로 토큰의 유효성을 검증한 뒤, 검증에 성공하면 CSR에 서명해 인증서를 파일럿 에이전트에 반환한다.

 

파일럿 에이전트는 SDS Secrets Discovery Service 를 통해 인증서와 키를 엔보이 프록시로 전달하고, 이로써 ID 부트스트랩 과정이 마무리된다.

# 유닉스 도메인 소켓 listen 정보 확인
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ss -xpl
Netid           State            Recv-Q           Send-Q                                                    Local Address:Port                      Peer Address:Port           Process                                        
u_str           LISTEN           0                4096                                                etc/istio/proxy/XDS 13207                                * 0               users:(("pilot-agent",pid=1,fd=11))           
u_str           LISTEN           0                4096                       ./var/run/secrets/workload-spiffe-uds/socket 13206                                * 0               users:(("pilot-agent",pid=1,fd=10))  

kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ss -xp
Netid State Recv-Q Send-Q                                Local Address:Port    Peer Address:Port   Process                             
u_str ESTAB 0      0      ./var/run/secrets/workload-spiffe-uds/socket 21902              * 23737   users:(("pilot-agent",pid=1,fd=16))
u_str ESTAB 0      0                               etc/istio/proxy/XDS 1079087            * 1080955 users:(("pilot-agent",pid=1,fd=8)) 
...

# 유닉스 도메인 소켓 정보 확인
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- lsof -U
COMMAND   PID        USER   FD   TYPE             DEVICE SIZE/OFF    NODE NAME
pilot-age   1 istio-proxy    8u  unix 0x00000000bda7185a      0t0 1079087 etc/istio/proxy/XDS type=STREAM # 소켓 경로 및 스트림 타입
pilot-age   1 istio-proxy   10u  unix 0x0000000009112f4b      0t0   13206 ./var/run/secrets/workload-spiffe-uds/socket type=STREAM # SPIFFE UDS (SPIFFE SVID 인증용)
# TYPE 파일 유형 (unix → Unix Domain Socket)
## 8u → 8번 디스크립터, u = 읽기/쓰기
## 10u → 10번 디스크립터, u = 읽기/쓰기

# 유닉스 도메인 소켓 파일 정보 확인
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets/workload-spiffe-uds/socket
srw-rw-rw- 1 istio-proxy istio-proxy 0 May  1 23:23 /var/run/secrets/workload-spiffe-uds/socket

# istio 인증서 확인 : 
docker exec -it myk8s-control-plane istioctl proxy-config secret deploy/webapp.istioinaction
RESOURCE NAME     TYPE           STATUS     VALID CERT     SERIAL NUMBER                               NOT AFTER                NOT BEFORE
default           Cert Chain     ACTIVE     true           45287494908809645664587660443172732423      2025-05-03T16:13:14Z     2025-05-02T16:11:14Z
ROOTCA            CA             ACTIVE     true           338398148201570714444101720095268162852     2035-04-29T07:46:14Z     2025-05-01T07:46:14Z

docker exec -it myk8s-control-plane istioctl proxy-config secret deploy/webapp.istioinaction -o json
...

echo "." | base64 -d  | openssl x509 -in /dev/stdin -text -noout


# istio ca 관련
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets/istio
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- openssl x509 -in /var/run/secrets/istio/root-cert.pem -text -noout

kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets/tokens
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/tokens/istio-token
TOKEN=$(kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/tokens/istio-token)
# 헤더 디코딩 
echo $TOKEN | cut -d '.' -f1 | base64 --decode | sed 's/$/}/' | jq
# 페이로드 디코딩
echo $TOKEN | cut -d '.' -f2 | base64 --decode | sed 's/$/"}/' | jq
# (옵션) brew install jwt-cli 
jwt decode $TOKEN


# (참고) k8s ca 관련
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets/kubernetes.io/serviceaccount
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- openssl x509 -in /var/run/secrets/kubernetes.io/serviceaccount/ca.crt -text -noout
kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
TOKEN=$(kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# 헤더 디코딩 
echo $TOKEN | cut -d '.' -f1 | base64 --decode | sed 's/$/}/' | jq
# 페이로드 디코딩
echo $TOKEN | cut -d '.' -f2 | base64 --decode | sed 's/$/}/' | jq
# (옵션) brew install jwt-cli 
jwt decode $TOKEN


# (참고)
kubectl port-forward deploy/webapp -n istioinaction 15000:15000
open http://localhost:15000
curl http://localhost:15000/certs

 

 

 

요청 ID 이해하기

  • 요청 ID는 요청의 필터 메타데이터에 저장되며, 이 메타데이터에는 JWT나 피어 인증서에서 추출한 신뢰할 수 있는 정보(사실, 클레임)가 포함된다.
  • PeerAuthentication 리소스는 워크로드 간 상호 인증(mTLS)을 강제하고, RequestAuthentication 리소스는 JWT를 검증해 최종 사용자 정보를 추출한다.
  • 필터 메타데이터에는 워크로드 ID(Principal), 네임스페이스, 최종 사용자 주체, 사용자 클레임 등 다양한 인증·인가 관련 정보가 저장된다.
  • 이렇게 수집된 메타데이터는 서비스 프록시가 표준 출력 등으로 기록해 관찰할 수 있으며, 인가 정책의 근거로 활용된다.

 

RequestAuthentication 리소스로 수집한 메타데이터

docker exec -it myk8s-control-plane istioctl proxy-config log deploy/istio-ingressgateway -n istio-system --level rbac:debug


#초기화
kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction
kubectl apply -f services/webapp/kubernetes/webapp.yaml -n istioinaction
kubectl apply -f services/webapp/istio/webapp-catalog-gw-vs.yaml -n istioinaction

kubectl apply -f ch9/enduser/jwt-token-request-authn.yaml
kubectl apply -f ch9/enduser/allow-all-with-jwt-to-webapp.yaml # :30000 포트 추가 필요, 아래 실습 설정 참고.
kubectl get requestauthentication,authorizationpolicy -A


# 로깅
kubectl logs -n istio-system -l app=istio-ingressgateway -f


# admin 토큰을 사용하는 요청 : 필터 메타데이터 확인
ADMIN_TOKEN=$(< ch9/enduser/admin.jwt)

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
     -sSl -o /dev/null -w "%{http_code}\n" webapp.istioinaction.io:30000/api/catalog
...
dynamicMetadata: filter_metadata {
  key: "envoy.filters.http.jwt_authn"
  value {
    fields {
      key: "auth@istioinaction.io"
      value {
        struct_value {
          fields {
            key: "exp"
            value {
              number_value: 4745145071
            }
          }
          fields {
            key: "group"
            value {
              string_value: "admin"
            }
          }
          fields {
            key: "iat"
            value {
              number_value: 1591545071
            }
          }
          fields {
            key: "iss"
            value {
              string_value: "auth@istioinaction.io"
            }
          }
          fields {
            key: "sub"
            value {
              string_value: "218d3fb9-4628-4d20-943c-124281c80e7b"
            }
          }
        }
      }
    }
  }
}
filter_metadata {
  key: "istio_authn"
  value {
    fields {
      key: "request.auth.claims"
      value {
        struct_value {
          fields {
            key: "group"
            value {
              list_value {
                values {
                  string_value: "admin"
                }
              }
            }
          }
          fields {
            key: "iss"
            value {
              list_value {
                values {
                  string_value: "auth@istioinaction.io"
                }
              }
            }
          }
          fields {
            key: "sub"
            value {
              list_value {
                values {
                  string_value: "218d3fb9-4628-4d20-943c-124281c80e7b"
                }
              }
            }
          }
        }
      }
    }
    fields {
      key: "request.auth.principal"
      value {
        string_value: "auth@istioinaction.io/218d3fb9-4628-4d20-943c-124281c80e7b"
      }
    }
    fields {
      key: "request.auth.raw_claims"
      value {
        string_value: "{\"iat\":1591545071,\"sub\":\"218d3fb9-4628-4d20-943c-124281c80e7b\",\"group\":\"admin\",\"exp\":4745145071,\"iss\":\"auth@istioinaction.io\"}"
      }
    }
  }
}
...

 

 

 

 

정리

아래는 첨부한 이미지를 기반으로, Istio에서 서비스 메쉬 워크로드에 인증서가 할당되는 과정을 단계별로 상세히 설명한 내용입니다.

Istio 인증서 발급 및 적용 과정 상세 설명

  1. 서비스 어카운트 토큰 주입 (Inject token)
    • 쿠버네티스는 파드가 생성될 때, 서비스 어카운트 토큰을 /var/run/secrets/kubernetes.io/serviceaccount/ 경로에 자동으로 마운트한다.
    • 이 토큰은 해당 파드(워크로드)의 신원을 증명하는 데 사용된다.
    • 이미지에서 파드 내부의 Istio proxy(Envoy)와 Pilot agent가 이 토큰에 접근할 수 있다.
  2. CSR과 토큰 전송 (CSR + token)
    • Istio 프록시 내부의 Pilot agent는 서비스 어카운트 토큰을 디코딩하여 파드의 신원 정보를 추출한다.
    • 이 정보를 바탕으로 SPIFFE ID가 포함된 CSR(Certificate Signing Request, 인증서 서명 요청)을 생성한다.
    • Pilot agent는 CSR과 서비스 어카운트 토큰을 함께 Istio CA(istiod)로 전송한다.
  3. 토큰 유효성 검증 (Validate token)
    • Istio CA(istiod)는 쿠버네티스 API의 TokenReview 기능을 이용해, 전달받은 토큰이 실제로 쿠버네티스에서 발급한 유효한 토큰인지 확인한다.
    • 이 단계에서 토큰이 위조되었거나 만료된 경우, 인증서 발급이 거부된다.
  4. 인증서 발급 (Certificate)
    • 토큰 검증에 성공하면, Istio CA는 CSR에 서명하여 SPIFFE ID가 포함된 X.509 인증서를 생성한다.
    • 이 인증서는 다시 Pilot agent로 반환된다.
  5. Envoy에 인증서 및 키 적용 (Configure with certificate and private key)
    • Pilot agent는 전달받은 인증서와 개인 키를 Envoy 프록시에 동적으로 전달(SDS: Secret Discovery Service)한다.
    • Envoy는 이 인증서를 사용해 서비스 간 mTLS(상호 TLS) 통신 시 자신의 신원을 증명하고, 암호화된 트래픽을 주고받을 수 있게 된다.

 

 

+ Recent posts