CloudNet@에서 진행하는 Istio Study 5주차 9장 내용입니다.
📕 This chapter cover
- Handling service-to-service authentication and authorization in the service mesh
- Handling end-user authentication and authorization
애플리케이션 보안이란, 인가 받지 않은 사용자가 오염시키거나 훔치거나 접근해서는 안 되는 귀중한 애플리케이션 데이터를 보호하는 데 기여하는 모든 행동을 말한다.
인증(authentication)이란 클라이언트나 서버가 자신의 정체를 입증하는 절차를 말하며, 아는 것(패스워드)이나 갖고 있는 것(장치, 인증서) 또는 자기 자신(지문 같은 고유 특성)을 이용한다.
인가(authorization)란 이미 인증된 사용자가 리소스의 생성이나 조회, 갱신, 삭제 같은 작업을 수행하는 것을 허용하거나 거부하는 절차를 말한다.
서비스는 어떤 종류든 접근을 허용하기 전에 자격 증명을 발급한 인증 서버에 자격 증명을 검증합니다.

반면에 마이크로서비스는 쉽게 수백, 수천 개의 서비스로 불어나므로 정적 환경에서는 서비스를 운영할 수 없다.
클라우드와 컨테이너는 동적 환경을 활용하기에 수명이 짧기에 IP 주소를 사용하는 전통적인 방법들은 ID 근거로 부족해 집니다.
설상가상으로 서비스가 반드시 같은 네트워크에서 실행되는 것도 아니며, 여러 클라우드 프로바이더에 걸쳐 있거나 심지어아래 그림처럼 온프레미스에서도 실행 될 수 있다.



PeerAuthentication : 서비스-to-서비스 인증 설정, 인가를 위한 피어 정보 추출RequestAuthentication : End-user 인증 설정, 인가를 위한 유저 정보 추출AuthorizationPolicy : PeerAuthentication, RequestAuthentication 에서 추출한 피어/유저 정보에 기초하여 권한 판단을 위한 인가 정책을 설정
- 서비스 메시가 서로 인증한 트래픽만 허용하도록 설정해야 한다.
- 왜 이것이 설치할 때 기본값이 아닌지 궁금할 수 있다. 이는 메시 채택을 용이하게 하려는 설계 결정이다.
- 여러 팀이 자체 서비스를 관리하는 거대 엔터프라이즈에서는 모든 서비스를 메시로 옮기기까지 몇 달 혹은 몇 년에 걸치 조직적인 노력이 필요할 수 있다.
- 두 번째로, 서비스를 인증하면 최소 권한 원칙을 준수할 수 있고, 각 서비스에 정책을 만들 수 있으며, 기능에 필요한 최소한의 접근만 허용할 수 있다.
- 이는 아주 중요한데, 서비스의 ID를 나타내는 인증서가 잘못된 사람에게 넘어갔을 때 피해 범위를 ID가 접근할 수 있도록 허용된 일부 서비스만으로 좁힐 수 있기 때문이다.


# 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
kubectl apply -f ch9/sleep.yaml -n default
확인

Kiali 확인 : unknown -> webapp 구간은 평문 통신

기본적으로 Istio는 평문 요청을 허용하는데, 이는 모든 워크로드를 메시로 옮길 때까지 서비스 중단을 일으키지 않고 서비스 메시를 점진적으로 채택할 수 있게 하기 위해서다.
그러나 PeerAuthentication 리소스로 평문 트래픽을 금지할 수 있다.
PeerAuthentication 리소스를 사용하면 워크로드가 mTLS를 엄격하게 요구하거나 평문 트래픽을 허용하고 받아들이게 설정할 수 있습니다.
메시 범위 리소스의 이름을 ‘default’로 짓는 것은 필수가 아닌 일종의 컨벤션(convention)으로, 메시 범위 PeerAuthentication 리소스를 딱 하나만 만들기 위해서입니다.
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

# NR → Non-Route. Envoy에서 라우팅까지 가지 못한 단계에서 발생한 에러라는 의미입니다.
# filter_chain_not_found → 해당 Listener에서 제공된 SNI(Server Name Indication), IP, 포트, ALPN 등의 조건에 맞는 filter_chain이 설정에 없다는 뜻입니다.
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

# 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



istiod는 PeerAuthentication 리소스 생성을 수신하고, 이 리소스를 엔보이용 설정으로 변환하며, LDS(Listener Discovery Service)를 사용해 서비스 프록시에 적용한다. 구성된 정책들은 들어오는 요청마다 평가된다.

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 이상 있는 패킷만 캡처

# (참고) 패킷 모니터링 : 아래 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 및 인증서 체인 출력 후 사람이 읽을 수 있는 형식으로 해석

루트 인증서 서명 확인
openssl verify 로 인증 기관 CA 루트 인증서에 대해 서명을 확인함으로써 X.509 SVID의 내용물이 유효한지 살펴보자./var/run/secrets/istio/root-cert.pem 경로에 마운트돼 있다.
peer-to-peer 인증을 용의하게 하는 모든 구성 요소를 검증했으므로, 발급된 ID는 검증할 수 있는 것이고 트래픽은 안전하다는 것을 확신할 수 있다.
- 인가란 인증된 주체가 리소스 접근, 편집, 삭제 같은 작업을 수행하도록 허용됐는지 정의하는 절차다.
- 정책은 인증된 주체(’누가’)와 인가(’무엇’)를 결합해 형성되며, 누가 무슨 일을 할 수 있는지 정의한다

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 사양에서 정책을 설정하고 정의하는 필드는 세 가지다.
인가 정책 규칙 이해하기
# 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

다음 AuthorizationPolicy 리소스는 webapp 으로의 요청 중 HTTP 경로에 /api/catalog* 가 포함된 것을 허용한다.
# 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 리턴

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
"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 - -


# 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
...

# cat ch9/policy-allow-all-mesh.yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: allow-all
namespace: istio-system
spec:
rules:
- {}
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
...
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 은 성공
# 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"]
# 호출 테스트 : 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은 sleep 서비스에서 요청을 허용했지만, 메시 범위 전체 거부 정책이 catalog 서비스로의 후속 요청을 거부했다.
# 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"]
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

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/* 가 발급한 것이어야 한다.Principals vs. request principals 차이점
source 를 정의하는 문서를 보면 https://istio.io/latest/docs/reference/config/security/authorization-policy/#Source
from 절에서 요청의 주체를 인식하는 방법에는 Principals , request principals 가 있다는 것을 알 수 있다.
Principals 은 PeerAuthentication 으로 설정한 상호 TLS 커넥션의 참가자인 것과 달리,
request principals 는 최종 사용자 Request Authentication 용이며 JWT에서 온다는 점에서 차이가 있다.
/api/catlog* 는 /api/catalog/1 과 같이 접두사로 시작하는 모든 값에 부합한다.*.istioinaction.io 는 login.istioinaction.io 와 같이 모든 서브도메인에 부합한다.*로 표기한다. 이는 필드가 존재해야 하지만, 값은 중요하지 않아 어떤 값이든 괜찮음을 의미한다.apiVersion: "security.istio.io/v1beta1"
kind: "AuthorizationPolicy"
metadata:
name: "allow-mesh-all-ops-admin"
namespace: istio-system
spec:
rules:
- from: # 첫 번째 규칙
- source: # 소스들 Sources
principals: ["cluster.local/ns/istioinaction/sa/webapp"]
- source:
namespace: ["default"]
to: # operations
- operation:
methods: ["GET"]
paths: ["/users*"]
- operation:
methods: ["POST"]
paths: ["/data"]
when: # Conditions
- key: request.auth.claims[group]
values: ["beta-tester", "admin", "developer"]
- to: # 두 번째 규칙
- operation:
paths: ["*.html", "*.js", "*.png"]

Istio는 다른 솔루션에서 정책 적용/순서를 위해 priority 필드를 사용하지만, 다른 접근법을 사용합니다.

이 세 부분, 즉 헤더, 페이로드, 서명이 점(.)으로 구분되고 Base64 URL로 인코딩되기 때문에 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
}
이 데이터는 주체 subject 에 대한 클레임을 표현한다. 클레임 덕분에 서비스는 클라이언트의 ID 및 인가를 판단할 수 있다.

private key 와 “토큰 검증”을 위한 public key를 가지고 있음private key 로 서명한 JWT (JSON Web Token) 을 발급public key는 JWKS (JSON Web Key Set) 형태의 HTTP 엔드포인트로 제공public key를 JWKS 에서 찾습니다.public key로 JWT 서명을 복호화 하여 얻은 해시값과 JWT 토큰 데이터의 해시값을 비교하여claim에 변조가 없었음을 보장하므로 신뢰할 수 있습니다.Istio 워크로드가 JWT로 최종 사용자 요청을 인증하고 인가하도록 설정할 수 있다.
최종 사용자란 ID 제공자에게 인증받고 ID와 클레임을 나타내는 토큰을 발급받은 사용자를 말한다.
최종 사용자 인가는 모든 워크로드 수준에서 수행할 수 있지만, 보통은 Istio Ingress Gateway에서 수행한다.
실습 준비
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 리소스는 이스티오의 인그레스 게이트웨이에 적용된다.
# 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

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

워크로드에 적용된 인가 정책 AuthorizationPolicy 이 없으므로 기본적으로 허용 ALLOW 된다.
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

# 호출
curl -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog
# 로그
kubectl logs -n istio-system -l app=istio-ingressgateway -f

- 실제로는 애플리케이션의 프론트엔드에 서비스를 제공하는 등 요청에 토큰이 없는 시나리오가 많이 있다.
- 이런 이유로, 토큰이 없는 요청을 거부하려면 다음에 설명할 약간의 추가 작업이 필요하다.
# 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 - -

유저별로 다른 접근 정책 설정
# 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"]
# 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
# 수집된 메타데이터를 관찰하고자 서비스 프록시에 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"}'

관리자가 catalog 에 새 아이템을 만들 수 있도록 허용하고 있음을 확인.
요청을 허용할지 여부를 결정할 때 외부 인가 서비스를 호출하도록 Istio의 서비스 프록시를 설정할 수 있다.

# 기존 인증/인가 정책 모두 삭제
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

# 아래 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 은 피어 간 인증을 정의하는 데 사용하며, 엄격한 인증 요구 사항을 적용하면 트래픽이 암호화돼 도청할 수 없다.
- PERMISSIVE 정책은 이스티오 워크로드가 암호화된 트래픽과 평문 트래픽을 모두 수용할 수 있게 해서 다운타임 없이 천천히 마이그레이션할 수 있도록 해준다.
- AuthorizationPolicy 는 워크로드 ID 인증서나 최종 사용자 JWT에서 추출한 검증 가능한 메타데이터를 근거로 서비스 사이의 요청이나 최종 사용자의 요청을 인가(허용, 차단)하는 데 사용한다.
- RequestAuthentication 은 JWT가 포함된 최종 사용자 요청을 인증하는 데 사용한다.
- AuthorizationPolicy 에서 CUSTOM action을 사용하면 외부 인가 서비스를 통합할 수 있다.