스터디/Istio

Istio 스터디 5주차 - <마이크로서비스 통신 보호 2부>

황동리 2025. 5. 11. 00:11
반응형

CloudNet@ 가시다님이 진행하는 Istio 스터디 1기 - 5주차 정리 내용 입니다.

 

앞서 진행했던 주차를 보고 오시면 이해가 빠릅니다.

 

이번 글에서는, 아래 주제에 대해 알아보겠습니다.

  • 서비스 간 트래픽 인가
  • 최종 사용자 인증 및 인가
  • 커스텀 외부 인가 서비스와 통합하기

 

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

✅ 인가 란?

사용자가 특정 리소스에 대해 어떤 작업을 수행할 수 있는 권한이 있는지를 판단하는 과정 입니다.

즉, 사용자가 로그인했더라도, 모든 기능이나 데이터에 접근할 수 있는 건 아니며, 접근 가능한 범위를 제한하고 제어하는 것이 인가입니다.

 

이런 인가 작업을 관리하는 것이 Istio 에서는 AuthorizationPolicy 리소스 입니다.

✅ AuthorizationPolicy 리소스 란?

Istio에서 인증(Authentication) 이후에 수행되는 인가(Authorization) 를 처리하기 위한 리소스입니다.

이 리소스는 서비스 메시에 메시 범위, 네임스페이스 범위, 워크로드별 접근 정책을 정의하는 선언적 API 입니다.

 

이제 Istio에서 인가를 어떻게 구현하는지 알아보겠습니다.

 

Istio에서는 요청이 서비스로 도달하기 전에 Envoy 프록시 레벨에서 인증 -> 인가 단계를 거쳐 요청을 처리합니다.

 

 

위 그림 처럼

 

1. 사용자가 요청을 하면 사용자 정보나 권한 등이 JWT (JSON Web Token) 에 담겨져 애플리케이션 쪽으로 요청을 합니다.

2. 애플리케이션의 사이드카 프록시(Envoy)가 요청을 먼저 받아, JWT 토큰의 서명을 검증하고 상호 TLS 인증을 통해 SVID를 추출합니다.

3. JWT 클레임, SPIFFE ID, 기타 인증 정보 등 프록시 내부의 Filter metadata로 저장

4. 앞에서 생성된 Filter medata를 기반으로 AuthorizationPolicy 에 의해 AuthZfilters가 요청을 허용할지 거부할지를 판단합니다.

5. 요청을 허용하면 최종적으로 Application으로 요청이 전달됩니다.

 

이제 AuthorizationPolicy 리소스의 YAML 파일을 살펴보면,

 

$ 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

 

위와 같이 워크로드에 허용할 트래픽 규칙을 정해주면 됩니다.

 

YAML 파일의 내용을 해석해보면,

app=webapp 라벨을 가진 파드(워크로드)에 /api/catalog/* 경로로 오는 요청을 허용 해줍니다.

 

이런 식으로 Policy를 정해주면 간단하게 정책을 적용해줄 수 있습니다.

 

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

 

app=webapp 라벨을 가진 파드(워크로드)에 /api/catalog/* 경로로 오는 요청을 허용 해줍니다.

 

이런 식으로 Policy를 정해주면 간단하게 정책을 적용해줄 수 있습니다.

 

AuthorizationPolicy 리소스의 구성 요소들에 대해 좀 더 자세히 알아보겠습니다.

📌 인가 정책의 속성

  • AuthorizationPolicy 리소스 사양에서 정책을 설정하고 정의하는 필드는 세 가지 입니다.
    • selector 필드는 정책을 적용할 워크로드 정의. [ex) label 명]
    • action 필드는 이 정책이 허용(ALLOW)인지, 거부(DENY)인지, 커스텀(CUSTOM)인지 지정
      • action은 rules에 정의한 어떤 조건과도 일치하지 않으면, 그 정책은 무시되고 action은 실행되지 않습니다.
    • rules 필드는 정책을 활성화할 요청을 식별하는 규칙 목록을 정의

📌 인가 정책 규칙 이해하기

  • 하나의 rule 안에 from, to, when 이 있다면 모두 만족 해야 해당 rule 이 적용 됩니다.
  • 전체 AuthorizationPolicy 에는 여러 rule 이 있을 수 있으며, 그 중 하나라도 만족하면 action 이 실행 됩니다.
rules:
- from:
  - source:
      principals:
        - "cluster.local/ns/istioinaction/sa/frontend"
      namespaces:
        - "istioinaction"
      ipBlocks:
        - "192.168.0.0/24"
  • 위 예시 처럼 단일 규칙 필드로 3가지가 있습니다.
    - principals: 서비스 계정(SPIFFE ID)을 기준으로 요청의 주체를 식별
    - namespaces: 요청을 보낸 주체의 네임스페이스 기준으로 제한
    • ipBlocks: 요청을 보낸 IP 주소나 CIDR 범위를 기준으로 제한
  • 또한 principal, namespace를 조건으로 사용하려면 mTLS가 필수 입니다.

이제 실습을 통해 좀 더 자세히 알아보겠습니다.

📌 실습 환경 요약

  • 실습 환경 요약
    • sleep 워크로드는 default 네임스페이스에 배포했고, 평문 HTTP 요청을 만드는 데 사용합니다.
    • webapp 워크로드는 istioinaction 네임스페이스에 배포했고, default 네임스페이스에 있는 워크로드에서 미인증 요청을 받아들이고 있습니다.
    • catalog 워크로드는 istioinaction 네임스페이스에 배포했고, 같은 네임스페이스의 인증된 워크로드로부터만 요청을 받아들이고 있습니다.
🔧 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
---
$ 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

🔧 앞서 설정 해둔 PeerAuthentication 리소스 확인
$ kubectl get peerauthentications.security.istio.io -A

결과
NAMESPACE       NAME      MODE         AGE
istio-system    default   STRICT       7h50m
istioinaction   webapp    PERMISSIVE   6h52m

 

이제 워크로드에 정책 적용 시 동작 확인해보겠습니다.

 

일단 워크로드에 하나 이상의 ALLOW 인가 정책이 적용되면, 모든 트래픽에서 해당 워크로드의 접근은 기본적으로 거부 됩니다.

 

트래픽을 받아들이려면, 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

 

위 AuthorizationPolicy 리소스를 보면, app=webapp 라벨이 붙은 파드로 요청 중 HTTP 경로에 /api/catalog* 가 포함된 것을 허용 하도록 설정을 했습니다.

 

해당 리소스를 적용 시켜주면, webapp 라벨을 가진 워크로드(즉, webapp 파드)에 대해서

/api/catalog* 경로로 요청이 들어온 경우에만 트래픽을 허용(ALLOW) 합니다.

 

그 외의 요청 (예: /api/users, /, /healthz 등)은 명시적으로 허용되지 않았기 때문에 차단(DENY) 됩니다.

 

실제로 다른 경로로 요청을 주면 DENY 되는지 확인해보겠습니다.

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

🔧 로그 켜두기
$ 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

🔧 AuthorizationPolicy 리소스 적용 전 호출 테스트
$ kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog1

결과
[{"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":"Wa

$ kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/hello/world

결과
...
        <body>
                <div id="wrapper">
                        <div id="container">
                                <div class="navtop">
                                        <h1>Not Found</h1>
                                </div>
                                <div id="content">
                                        <br>The page you have requested has flown the coop.<br>Perhaps you are here because:<br><br><ul><br>The page has moved<br>The page no longer exists<br>You were looking for your puppy and got lost<br>You like 404 pages</ul>
                                        <a href="/" title="Home" class="button">Go Home</a><br />

                                        <br>Powered by beego 2.0.0
                                </div>
                        </div>
                </div>
        </body>
</html>

🔧 AuthorizationPolicy 리소스 적용
$ kubectl apply -f ch9/allow-catalog-requests-in-web-app.yaml

🔧 적용 후 호출 테스트
# 해당 결과는 정상적으로 나옴
$ kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog

# 해당 결과는 403 에러를 리턴 합니다.
$ kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/hello/world

명령어 실행 결과
RBAC: access denied

로그 결과
[2025-05-09T06:43:35.194Z] "GET /hello/world HTTP/1.1" 403 - rbac_access_denied_matched_policy[none] - "-" 0 19 0 - "-" "curl/8.5.0" "3395543e-f5d2-9923-bfff-c212cd95dedd" "webapp.istioinaction" "-" inbound|8080|| - 10.10.0.14:8080 10.10.0.10:41078 - -

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

 

그리고 kiail에 접속해서 webapp 워크로드에 Istio Config를 살펴보면 AuthorizationPolicy가 적용 된 것을 확인할 수 있습니다.

 

이렇게 원하는 규칙만 정해놓고 그 외 다른 규칙은 DENY 되도록 하는 것이 좋습니다.

 

따라서 전체 정책을 기본적으로 "모든 요청 거부" 상태로 해두겠습니다.

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

🔧 기본 거부 정책 리소스 확인
$ 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

결과
[{"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"}]

🔧 정책 적용
$ kubectl apply -f ch9/policy-deny-all-mesh.yaml

🔧 적용 후 테스트 1
$ kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog

결과
error calling Catalog service

🔧 적용 후 테스트 2
$ curl -s http://webapp.istioinaction.io:30000/api/catalog

결과
RBAC: access denied

로그에서도 403 에러가 나오는 것을 확인 할 수 있습니다.
[2025-05-09T07:03:08.030Z] "GET /items HTTP/1.1" 403 - via_upstream - "-" 0 19 1 1 "-" "beegoServer" "16e5568c-a5fd-9ebb-8fcd-34d22d613a75" "catalog.istioinaction:80" "10.10.0.13:3000" outbound|80||catalog.istioinaction.svc.cluster.local 10.10.0.14:49452 10.200.1.64:80 10.10.0.14:32802 - default

 

그리고 이제 특정 네임스페이스에서 온 요청만 허용 해보도록 하겠습니다.

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

🔧 default 네임스페이스로 부터 온 HTTP GET 요청만 허용
$ 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                       20m
istioinaction   webapp-allow-view-default-ns   9s

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

결과
# 왜냐하면 sleep 파드에는 사이드카 프록시(Envoy)가 존재 하지 않기 때문에 정책 적용이 되지 않아서 access denied 가 나오게 됩니다.
RBAC: access denied

🔧 sleep 파드에 사이드카 프록시 주입
$ kubectl label ns default istio-injection=enabled
$ kubectl delete pods -l app=sleep

🔧 sleep 파드에 사이드카 프록시 생겼는지 확인
$ docker exec -it myk8s-control-plane istioctl proxy-status

결과
NAME                                                   CLUSTER        CDS        LDS        EDS        RDS          ECDS         ISTIOD                    VERSION
sleep-6f8cfb8c8f-sn959.default                         Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED       NOT SENT     istiod-8d74787f-wx8zm     1.17.8

🔧 다시 호출 테스트 진행 (default 네임스페이스 -> webapp 파드까지는 성공)
$ kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction

로그 결과
[2025-05-09T07:50:40.286Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 4527 82 80 "-" "curl/8.5.0" "df93ee84-e6c6-9c0a-870d-249cd0bd0265" "webapp.istioinaction" "10.10.0.14:8080" inbound|8080|| 127.0.0.6:51033 10.10.0.14:8080 10.10.0.15:45152 outbound_.80_._.webapp.istioinaction.svc.cluster.local default

🔧 webapp -> catalog 까지 호출
$ kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog 

로그 결과
# webapp에서 catalog 까지의 호출은 실패한 것을 확인 할 수 있습니다.
[2025-05-09T07:56:02.196Z] "GET /items HTTP/1.1" 403 - via_upstream - "-" 0 19 10 9 "-" "beegoServer" "b91c1b8a-dfe1-9660-be29-b9c0a2b9de61" "catalog.istioinaction:80" "10.10.0.13:3000" outbound|80||catalog.istioinaction.svc.cluster.local 10.10.0.14:59366 10.200.1.64:80 10.10.0.14:33860 - default
[2025-05-09T07:56:02.191Z] "GET /api/catalog HTTP/1.1" 500 - via_upstream - "-" 0 29 17 16 "-" "curl/8.5.0" "b91c1b8a-dfe1-9660-be29-b9c0a2b9de61" "webapp.istioinaction" "10.10.0.14:8080" inbound|8080|| 127.0.0.6:51033 10.10.0.14:8080 10.10.0.15:45152 outbound_.80_._.webapp.istioinaction.svc.cluster.local default

🔧 default 네임스페이스에서 catalog로 호출 테스트
# 로그 활성화
$ kubectl logs -n istioinaction -l app=catalog -c istio-proxy -f

$ kubectl exec deploy/sleep -- curl -sSL catalog.istioinaction/items 

로그 결과
# 성공한 것을 확인할 수 있습니다.
[2025-05-09T08:00:01.105Z] "GET /items HTTP/1.1" 200 - via_upstream - "-" 0 502 43 42 "-" "curl/8.5.0" "2f745bbc-712d-9005-94a0-8a1bb66b3f03" "catalog.istioinaction" "10.10.0.13:3000" inbound|3000|| 127.0.0.6:58667 10.10.0.13:3000 10.10.0.15:60956 outbound_.80_._.catalog.istioinaction.svc.cluster.local default

🔧 다음 실습을 위해 원복
$ kubectl label ns default istio-injection-
$ kubectl rollout restart deploy/sleep

 

이제 미 인증 워크로드에서 온 요청을 허용 해보도록 하겠습니다.

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

미 인증 워크로드에서 온 요청을 허용하려면 from 필드를 삭제 해야 합니다.

 

실습을 통해 좀 더 자세히 알아보겠습니다.

🔧 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

🔧 호출 테스트 (default 네임스페이스 -> webapp)
$ kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction

로그 결과
[2025-05-09T08:09:21.424Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 4527 25 24 "-" "curl/8.5.0" "2181af65-4436-97db-a415-4cfe3ec2cd65" "webapp.istioinaction" "10.10.0.14:8080" inbound|8080|| 127.0.0.6:51033 10.10.0.14:8080 10.10.0.16:47930 - default

🔧 catalog 쪽으로도 호출 테스트 (default 네임스페이스 -> catalog)
$ kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog 

로그 결과
# webapp -> catalog 요청은 허용하지 않아서 에러가 나옵니다.
[2025-05-09T08:12:39.303Z] "GET /items HTTP/1.1" 403 - via_upstream - "-" 0 19 1 0 "-" "beegoServer" "6f91d0e5-68a0-9b6d-af80-dce969e1dc71" "catalog.istioinaction:80" "10.10.0.13:3000" outbound|80||catalog.istioinaction.svc.cluster.local 10.10.0.14:59366 10.200.1.64:80 10.10.0.14:59856 - default
[2025-05-09T08:12:39.297Z] "GET /api/catalog HTTP/1.1" 500 - via_upstream - "-" 0 29 7 7 "-" "curl/8.5.0" "6f91d0e5-68a0-9b6d-af80-dce969e1dc71" "webapp.istioinaction" "10.10.0.14:8080" inbound|8080|| 127.0.0.6:51033 10.10.0.14:8080 10.10.0.16:53974 - default

 

이번엔 특정 서비스 어카운트에서 온 요청을 허용해보겠습니다.

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

트래픽이 특정 워크로드에서 왔는지 인증할 수 있는 간단한 방법은 트래픽에 주입된 서비스 어카운트를 사용하는 것 입니다.

 

서비스 어카운트 정보는 SVID에 인코딩 되어 있으며, 상호 인증 중에 해당 정보를 검증하고 필터 메타데이터에 저장합니다.

 

이제 실습을 통해 알아보겠습니다.

🔧 서비스 어카운트가 webapp인 워크로드에서 온 트래픽만 허용하도록 설정
$ 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 logs -n istioinaction -l app=webapp -c istio-proxy -f
$ kubectl logs -n istioinaction -l app=catalog -c istio-proxy -f

🔧 호출테스트 sleep --(미인증 레거시 허용)--> webapp --(principals webapp 허용)--> catalog
$ kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction
$ kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog 

따라서 호출 테스트 결과를 보면 모두 성공으로 나오게 됩니다.

webapp 프록시 로그
[2025-05-10T01:36:10.820Z] "GET /items HTTP/1.1" 200 - via_upstream - "-" 0 502 3 3 "-" "beegoServer" "b67e3b7b-9ab2-99b9-a147-54ee1e6bff30" "catalog.istioinaction:80" "10.10.0.6:3000" outbound|80||catalog.istioinaction.svc.cluster.local 10.10.0.12:59912 10.200.1.64:80 10.10.0.12:53090 - default
[2025-05-10T01:36:10.818Z] "GET /api/catalog HTTP/1.1" 200 - via_upstream - "-" 0 357 12 12 "-" "curl/8.5.0" "b67e3b7b-9ab2-99b9-a147-54ee1e6bff30" "webapp.istioinaction" "10.10.0.12:8080" inbound|8080|| 127.0.0.6:35993 10.10.0.12:8080 10.10.0.7:47404 - default

catalog 프록시 로그
[2025-05-10T01:36:03.815Z] "GET /items HTTP/1.1" 200 - via_upstream - "-" 0 502 24 23 "-" "beegoServer" "4fe877a7-455b-982a-9a27-d64de9413666" "catalog.istioinaction:80" "10.10.0.6:3000" inbound|3000|| 127.0.0.6:59521 10.10.0.6:3000 10.10.0.12:59900 outbound_.80_._.catalog.istioinaction.svc.cluster.local default
[2025-05-10T01:36:10.821Z] "GET /items HTTP/1.1" 200 - via_upstream - "-" 0 502 2 2 "-" "beegoServer" "b67e3b7b-9ab2-99b9-a147-54ee1e6bff30" "catalog.istioinaction:80" "10.10.0.6:3000" inbound|3000|| 127.0.0.6:43847 10.10.0.6:3000 10.10.0.12:59912 outbound_.80_._.catalog.istioinaction.svc.cluster.local default

📌 정책의 조건부 적용

이제 특정 조건이 충족되는 경우에만 요청이 허용되도록 설정해보도록 하겠습니다.

 

설정할 조건은 2가지 입니다.

  • 토큰은 요청 주체 auth@istioninaction.io/* 가 발급한 것이야 한다.
  • JWT에 값이 'admin'인 group 클레임이 포함돼 있어야 한다.

이 두 정책이 모두 충족되는 경우에만 요청이 허용이 됩니다.

🔧  조건을 만족할  요청을 허용하는 AuthorizationPolicy 리소스
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"] # 반드시 일치해야 하는 값의 목록을 지정한다

📌 값 비교 표현식 이해하기

Istio는 규칙을 더 다야앟게 만들 수 있도록 간단한 비교 표현식을 지원합니다.

  • 일치: 예를 들어 GET은 값이 정확히 일치해야합니다.
  • 접두사 비교: 예를 들어 /api/catalog* 는 /api/catalog/1과 같이 이 접두사로 시작하는 모든 값에 부합합니다.
  • 접미사 비교: 예를 들어 *.istioinaction.io 는 login.istioninaction.io 와 같이 모든 서브도메인에 부합합니다.
  • 존재성 비교: 모든 값에 부합하며, *로 표기합니다. 이는 필드가 존재해야 하지만, 값은 중요하지 않아 어떤 값이 들어가 된다는 의미 입니다.

이제 AuthorizationPolicy 리소스를 살펴보면서 좀 더 자세히 알아보겠습니다.

apiVersion: "security.istio.io/v1beta1"
kind: "AuthorizationPolicy"
metadata:
  name: "allow-mesh-all-ops-admin"
  namespace: istio-system
spec:
  rules: 
  - from: # 첫 번째 규칙
    - source:
        principals: ["cluster.local/ns/istioinaction/sa/webapp"]
    - source:
        namespace: ["default"]
    to:
    - operation:
        methods: ["GET"]
        paths: ["/users*"]
    - operation:
        methods: ["POST"]
        paths: ["/data"]
    when:
    - key: request.auth.claims[group]
      values: ["beta-tester", "admin", "developer"]
  - to: # 두 번째 규칙
    - operation:
        paths: ["*.html", "*.js", "*.png"]

 

위 리소스에서는 규칙이 2개가 있습니다.

 

rules 아래에 있는 규칙들은 서로 독립적인 규칙 입니다.
-> 즉 , 하나라도 조건을 만족하면 ALLOW 또는 DENY 같은 action 이 적용됩니다.

 

다만, 하나의 규칙 안에서는 from, to, when 이 모두 AND 조건 입니다.
-> 즉, 하나의 rule 내부에서는 from and to and when 로서 모두 만족해야 rule 이 적용됩니다.

📌 인가 정책이 평가되는 순서 이해하기

한 워크로드에 많은 정책이 적용되고 순서를 이해하기 어려울 때 정책의 복잡성이 커집니다.

 

많은 솔루션이 priority(우선순위) 필드를 사용하여 순서를 정의 합니다.

 

Istio는 정책 평가에 따른 접근법을 사용합니다.

 

정책 평가 순서는 아래와 같습니다.

요청이 들어오면,

  1. CUSTOM 정책 평가
    • 이 정책에서 DENY 반환 시, 요청 거부
    • 그렇지 않으면 평가 진행
  2. DENY 정책 평가
    • 일치하는 DENY 규칙이 있으면 요청 거부
    • 그렇지 않으면 평가 진행
  3. ALLOW 정책 존재 여부 평가
    • ALLOW 정책이 존재하는 지 여부 확인, 존재 시 다음 단계로 넘어가 ALLOW 정책 평가
    • 그렇지 않으면, 일반 정책이 존재하는지 평가
  4. ALLOW 정책 평가
    • 여러 개의 ALLOW 정책 중 하나라도 매치되면 요청 허용
    • 어떤 것도 매치 되지 않으면, 일반 정책으로 평가 진행

이제 Istio에서 최종 사용자 인증 및 인가에 대해 알아보겠습니다.

📌 최종 사용자 인증 및 인가

먼저 JWT 토큰에 대해 알아보겠습니다.

✅ JWT란 ?

JWT는 클라이언트를 서버에 인증하는 데 사용하는 간단한 클레임 표현 입니다.

JWT는 3가지 부분으로 이루어져 있습니다,

  • 헤더: 유형 및 해싱 알고리즘으로 구성
  • 페이로드: 사용자 클레임 포함
  • 서명: JWT의 진위 여부를 파악하는 데 사용

위 3가지가 점(.)으로 구분되고 Base64 URL로 인코딩되기 때문에, JWT는 HTTP 요청에서 사용하기 좋습니다.

 

실제 jwt 토큰을 보고 디코딩 해보겠습니다.

🔧 jwt 토큰 내용 확인
$ cat ./ch9/enduser/user.jwt

결과
eyJhbGciOiJSUzI1NiIsImtpZCI6IkNVLUFESkpFYkg5YlhsMHRwc1FXWXVvNEV3bGt4RlVIYmVKNGNra2FrQ00iLCJ0eXAiOiJKV1QifQ.eyJleHAiOjQ3NDUxNDUwMzgsImdyb3VwIjoidXNlciIsImlhdCI6MTU5MTU0NTAzOCwiaXNzIjoiYXV0aEBpc3Rpb2luYWN0aW9uLmlvIiwic3ViIjoiOWI3OTJiNTYtN2RmYS00ZTRiLWE4M2YtZTIwNjc5MTE1ZDc5In0.jNDoRx7SNm8b1xMmPaOEMVgwdnTmXJwD5jjCH9wcGsLisbZGcR6chkirWy1BVzYEQDTf8pDJpY2C3H-aXN3IlAcQ1UqVe5lShIjCMIFTthat3OuNgu-a91csGz6qtQITxsOpMcBinlTYRsUOICcD7UZcLugxK4bpOECohHoEhuASHzlH-FYESDB-JYrxmwXj4xoZ_jIsdpuqz_VYhWp8e0phDNJbB6AHOI3m7OHCsGNcw9Z0cks1cJrgB8JNjRApr9XTNBoEC564PX2ZdzciI9BHoOFAKx4mWWEqW08LDMSZIN5Ui9ppwReSV2ncQOazdStS65T43bZJwgJiIocSCg

🔧 jwt 토큰 디코딩 방법
$ jwt -show ch9/enduser/user.jwt

결과
Header:
{
    "alg": "RS256",
    "kid": "CU-ADJJEbH9bXl0tpsQWYuo4EwlkxFUHbeJ4ckkakCM",
    "typ": "JWT"
}
Claims:
{
    "exp": 4745145038,	# 만료시간
    "group": "user",	# group 클레임
    "iat": 1591545038,	# 발행시각
    "iss": "auth@istioinaction.io",	# 토큰 발행자
    "sub": "9b792b56-7dfa-4e4b-a83f-e20679115d79"	# 토큰의 주체
}

 

여기서 나오는 클레인 덕분에 서비스는 클라이언트의 ID 및 인가를 판단할 수 있습니다.

 

이제 JWT는 어떻게 발행이되고 검증이 되는지 알아보겠습니다.

 

JWT는 인증 서버에서 발급이 되는데,
인증 서버는 토큰을 서명하는 비밀 키와 검증하기 위한 공개 키를 가지고 있습니다.

 

공개 키는 JWKS(JSON Web Key Set, JSON 웹 키셋)라고 하며, well-known HTTP 엔드포인트에 노출됩니다.

 

서비스는 해당 엔드포인트에서 공개 키를 가져와 인증 서버가 발급한 토큰을 검증할 수 있습니다.

  • 인증 서버는 여러 솔루션으로 준비할 수 있다.
    1. 애플리케이션 백엔드 프레임워크에서 구현할 수 있다.
    2. OpenIAM 혹은 Keycloak 등의 서비스로, 자체적으로 구현할 수 있다.
    3. Auth0, Okta 등의 서비스형 ID Identity-as-a-Service 솔루션으로 구현할 수 있다.

이제 서버가 토큰을 검증하는 데 JWKS를 어떻게 사용하는지 알아보겠습니다.

  1. 클라이언트가 인증 서버에 로그인
    → JWT(토큰)를 발급받음
  2. 클라이언트가 서버에 요청 보냄
    → JWT를 Authorization 헤더에 담아 전송
  3. 서버가 JWT의 서명을 검증해야 함
    → 어떤 키로 서명됐는지 확인 (kid 확인)
  4. 서버가 인증 서버에서 JWKS를 다운로드
     https://[인증서버]/.well-known/jwks.json 주소로 요청
  5. JWKS에서 kid에 해당하는 공개 키 찾기
    → JWT의 서명 검증에 사용
  6. 서명이 유효하면 요청 처리, 아니면 거부 (401)

이제 Ingress Gateway에서 최종 사용자 인증 및 인가에 대해 알아보겠습니다.

✅ 최종 사용자란?

ID 제공자에게 인증받고 ID와 클레임을 나타내는 토큰을 발급받은 사용자를 뜻합니다.

 

최종 사용자 인가는 모든 워크로드 수준에서 수행할 수 있지만, 보통 Ingress Gateway에서 수행합니다.

 

이렇게 하면, 불필요한 트래픽 낭비를 막고, 보안을 강화할 수 있습니다.

  • 외부 요청을 가장 먼저 받는 곳이 Ingress Gateway
    → Gateway에서 인가를 걸면, 내부 서비스까지 트래픽이 들어오지 않도록 차단 가능
  • 리소스를 절약하고 성능 최적화
    → 불필요한 요청이 내부 서비스까지 전달되지 않으므로 리소스 낭비 방지
  • 보안 제어를 중앙에서 일괄 적용 가능
    → 사용자 권한 체크를 Gateway에 집중시켜 인가 정책을 관리하기 쉬움

이제 실습을 하기 위해 환경을 준비해보겠습니다.

🔧 앞서 생성한 istioincation 네임스페이스에 존재하는 리소스들 삭제
$ kubectl delete virtualservice,deployment,service,\
destinationrule,gateway,peerauthentication,authorizationpolicy --all -n istioinaction

🔧 앞서 생성한 peerauthentication,authorizationpolicy 리소스 삭제
$ kubectl delete peerauthentication,authorizationpolicy -n istio-system --all

🔧 실습 환경 배포
$ 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
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: webapp-gateway
  namespace: istioinaction
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "webapp.istioinaction.io"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: webapp-virtualservice
  namespace: istioinaction
spec:
  hosts:
  - "webapp.istioinaction.io"
  gateways:
  - webapp-gateway
  http:
  - route:
    - destination:
        host: webapp
        port:
          number: 80

$ kubectl apply -f ch9/enduser/ingress-gw-for-webapp.yaml -n istioinaction

 

이제 RequestAuthentication 리소스를 탐색할 준비가 되었습니다.

✅ RequestAuthentication 리소스의 주 목적은 JWT를 검증하고, 유효한 토큰의 클레임을 추출하고,

이 클레임을 필터 메타데이터에 저장하는 것 입니다.

 

위에서 언급한 필터 메타데이터는 인가 정책이 조치를 취하는 근거로 사용됩니다.

✅ 필터 메타데이터란 ?

서비스 프록시에서 필터 간 요청을 처리하는 동안 사용할 수 있는 키-값 쌍의 모음을 말합니다.

예를 들어, 클레임 group=admin 이 있는 요청이 검증되면 이 값은 필터 메타 데이터로 저장되며, 필터 메타 데이터는 인가 정책이 요청을 허용하거나 거부하는 데 사용 됩니다.

 

결국 최종 사용자 요청에 따라 결과는 셋 중 하나가 됩니다.

  • 유효한 토큰을 갖고 있는 요청은 클러스터로 받아들여지며, 이들의 클레임은 필터 메타데이터 형태로 정책에 전달된다.
  • 유효하지 않은 토큰을 갖고 있는 요청은 거부된다.
  • 토큰이 없는 요청은 클러스터로 받아들여지지만 요청 ID가 없다. 즉, 어떤 클레임도 필터 메타데이터에 저장되지 않는다.

이제 RequestAuthentication 리소스를 만들어 실습 해보도록 하겠습니다.

🔧 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

🔧 유효한 JWT를 변수로 등록
$ cat ch9/enduser/user.jwt
$ USER_TOKEN=$(< ch9/enduser/user.jwt)
$ echo "$USER_TOKEN" | jwt -show -

결과
Header:
{
    "alg": "RS256",
    "kid": "CU-ADJJEbH9bXl0tpsQWYuo4EwlkxFUHbeJ4ckkakCM",
    "typ": "JWT"
}
Claims:
{
    "exp": 4745145038,
    "group": "user",
    "iat": 1591545038,
    "iss": "auth@istioinaction.io",
    "sub": "9b792b56-7dfa-4e4b-a83f-e20679115d79"
}

🔧 jwt 토큰을 가지고 catalog 워크로드까지 호출
$ curl -H "Authorization: Bearer $USER_TOKEN" \
     -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog
     
결과
200

🔧 유효하지 않은 JWT를 변수로 등록
$ cat ch9/enduser/not-configured-issuer.jwt
$ WRONG_ISSUER=$(< ch9/enduser/not-configured-issuer.jwt)
$ echo "$WRONG_ISSUER" | jwt -show -

결과
Header:
{
    "alg": "RS256",
    "kid": "CU-ADJJEbH9bXl0tpsQWYuo4EwlkxFUHbeJ4ckkakCM",
    "typ": "JWT"
}
Claims:
{
    "exp": 4745151548,
    "group": "user",
    "iat": 1591551548,
    "iss": "old-auth@istioinaction.io",
    "sub": "79d7506c-b617-46d1-bc1f-f511b5d30ab0"
}

🔧 유효하지 않은 JWT 토큰을 가지고 catalog 워크로드까지 호출
$ curl -H "Authorization: Bearer $WRONG_ISSUER" \
     -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog
     
결과
401

로그 결과
[2025-05-10T13:47:11.744Z] "GET /api/catalog HTTP/1.1" 401 - jwt_authn_access_denied{Jwt_issuer_is_not_configured} - "-" 0 28 0 - "172.18.0.1" "curl/8.5.0" "e2126bf2-687a-9487-8bcd-b0207a3af51d" "webapp.istioinaction.io:30000" "-" outbound|80||webapp.istioinaction.svc.cluster.local - 10.10.0.10:8080 172.18.0.1:50376 - -

🔧 JWT 토큰 없이 요청 실행
$ curl -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog

결과
200

🔧 JWT 토큰 없은 요청 거부하기 위한 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

🔧 AuthorizationPolicy 리소스 적용 후 JWT 토큰 없이 호출
$ curl -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog

결과
403

🔧 유효한 JWT 토큰을 가지고 호출
$ curl -H "Authorization: Bearer $USER_TOKEN" \
     -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog
     
결과
200

 

이번엔 JWT 토큰 마다 권한을 다르게 주어서 실습을 해보도록 하겠습니다.

🔧 user.jwt 토큰 -> 'group: user'
$ jwt -show ch9/enduser/user.jwt

결과
Header:
{
    "alg": "RS256",
    "kid": "CU-ADJJEbH9bXl0tpsQWYuo4EwlkxFUHbeJ4ckkakCM",
    "typ": "JWT"
}
Claims:
{
    "exp": 4745145038,
    "group": "user",
    "iat": 1591545038,
    "iss": "auth@istioinaction.io",
    "sub": "9b792b56-7dfa-4e4b-a83f-e20679115d79"
}

🔧 admin.jwt 토큰 -> 'group: admin'
$ jwt -show ch9/enduser/admin.jwt

결과
Header:
{
    "alg": "RS256",
    "kid": "CU-ADJJEbH9bXl0tpsQWYuo4EwlkxFUHbeJ4ckkakCM",
    "typ": "JWT"
}
Claims:
{
    "exp": 4745145071,
    "group": "admin",
    "iat": 1591545071,
    "iss": "auth@istioinaction.io",
    "sub": "218d3fb9-4628-4d20-943c-124281c80e7b"
}

🔧 일반 유저(user)가 webapp에서 데이터를 읽을 수 있도록 AuthorizationPolicy 리소스 설정
$ vim 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"]
        
🔧 관리자(admin) 에게 모든 작업을 허용하는 AuthorizationPolicy 리소스 설정
$ vim 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"] # 이 클레임을 포함한 요청만 허용.
      
🔧 AuthorizationPolicy 리소스 적용
$ kubectl apply -f ch9/enduser/allow-all-with-jwt-to-webapp.yaml
$ kubectl apply -f ch9/enduser/allow-mesh-all-ops-admin.yaml

🔧 일반 유저(user) 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
     
결과 (get)
200

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

🔧 관리자(admin) 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
     
결과 (get)
200

$ curl -H "Authorization: Bearer $ADMIN_TOKEN" \
     -XPOST webapp.istioinaction.io:30000/api/catalog \
     --data '{"id": 2, "name": "Shoes", "price": "84.00"}'
     
결과 (post)
[{"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"}]

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

지금까지 SPIFFE를 기반으로 구축된 Istio의 인증 메커니즘이 서비스 인가를 구축할 수 있는 기반을 제공하는 방법을 살펴보았습니다.

 

이번엔 인가에 좀 더 정교한 커스텀 메커니즘을 추가하는 방법을 알아보겠습니다.

 

인가에 커스텀 설정을 추가하는 방법은 외부 인가 서비스를 호출 하면 됩니다.

 

구성을 하면 아래 그림처럼 동작을 하게 됩니다.

✅ 요청 흐름 순서

 

1. 클라이언트 요청(Request)
사용자의 요청이 Envoy 프록시에 도착합니다.

 

2. CUSTOM 정책 적용
Envoy는 우선 CUSTOM 정책이 설정되어 있는 경우, 외부의 Authorization 서버에 판단을 위임합니다.
→ 이 Authorization 서버가 "허용(Allow)" 또는 "거부(Reject)" 응답을 반환합니다.

 

3. ALLOW/DENY 정책 확인
외부 서버가 허용하더라도, Envoy 내에 설정된 DENY 정책이 있으면 여전히 요청을 차단할 수 있습니다.

 

4. 최종적으로 App 전달
모든 정책이 통과되면 요청이 애플리케이션(App)으로 전달됩니다.

 

이제 실습을 통해 좀 더 자세히 알아보도록 하겠습니다.

🔧 앞선 설정 초기화
$ 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

🔧 Istio 샘플에서 샘플 외부 인가 서비스 배포
$ docker exec -it myk8s-control-plane bash
---
🔧 외부 인가 서버 적용
$ 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 istio

🔧 빠져나오기
$ exit
---

🔧 외부 인가 서비스 설치 확인
$ kubectl get deploy,svc ext-authz -n istioinaction

결과
NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ext-authz   1/1     1            1           36s

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

🔧 Istio가 새로운 외부 인가 서비스를 인식하도록 설정
# 이를 위해서 Istio meshconfig 설정에서 extensionProviders 를 설정해야 한다.
$ KUBE_EDITOR="vim" 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

 

이렇게 설정을 해주면, envoyExtAuthz 서비스의 HTTP 구현체인 sample-ext-authz-http를 인식 하게 됩니다.

 

이제 커스텀 AuthorizationPolicy 리소스를 사용하여 동작이 되는지 확인해보겠습니다.

🔧 AuthorizationPolicy 생성
$ 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

🔧 호출 전 로그 활성화
$ 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

로그 결과
(webapp Envoy 프록시)
[2025-05-10T15:01:25.642Z] "GET /api/catalog HTTP/1.1" 403 UAEX ext_authz_denied - "-" 0 76 18 16 "-" "curl/8.5.0" "804a3344-6ef5-90a2-8c00-e7f02a610db2" "webapp.istioinaction" "-" inbound|8080|| - 10.10.0.14:8080 10.10.0.4:41290 - -

(ext-authz Envoy 프록시)
2025/05/10 15:01:25 [HTTP][denied]: GET webapp.istioinaction/api/catalog, headers: map[Content-Length:[0] X-B3-Parentspanid:[3db4f6b89f472cef] X-B3-Sampled:[1] X-B3-Spanid:[32186977617a9220] X-B3-Traceid:[080ecf1d24a0fb00d79e3954ac14c507] X-Envoy-Expected-Rq-Timeout-Ms:[600000] X-Envoy-Internal:[true] X-Forwarded-Client-Cert:[By=spiffe://cluster.local/ns/istioinaction/sa/default;Hash=c0676bf16d242383ca31ce2e1975b107f0d75fb70e1c4fbeaec85e9b61475a24;Subject="";URI=spiffe://cluster.local/ns/istioinaction/sa/webapp] X-Forwarded-For:[10.10.0.14] X-Forwarded-Proto:[https] X-Request-Id:[548028d9-c235-986a-a632-2cc42a5ca381]], body: []

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

로그 결과
(webapp Envoy 프록시)
[2025-05-10T15:03:48.747Z] "GET /items HTTP/1.1" 200 - via_upstream - "-" 0 502 37 36 "-" "beegoServer" "0da22895-ed4d-9788-8866-02f25cb0deeb" "catalog.istioinaction:80" "10.10.0.13:3000" outbound|80||catalog.istioinaction.svc.cluster.local 10.10.0.14:47656 10.200.1.117:80 10.10.0.14:36328 - default
[2025-05-10T15:03:48.728Z] "GET /api/catalog HTTP/1.1" 200 - via_upstream - "-" 0 357 58 49 "-" "curl/8.5.0" "0da22895-ed4d-9788-8866-02f25cb0deeb" "webapp.istioinaction" "10.10.0.14:8080" inbound|8080|| 127.0.0.6:57785 10.10.0.14:8080 10.10.0.4:34342 - default

(extAuth Envoy 프록시)
2025/05/10 15:03:48 [HTTP][allowed]: GET webapp.istioinaction/api/catalog, headers: map[Content-Length:[0] X-B3-Parentspanid:[62329a86b4e18a5a] X-B3-Sampled:[1] X-B3-Spanid:[04d43a12530650bf] X-B3-Traceid:[1bccc54cc3adaf3522127297d2bad053] 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=c0676bf16d242383ca31ce2e1975b107f0d75fb70e1c4fbeaec85e9b61475a24;Subject="";URI=spiffe://cluster.local/ns/istioinaction/sa/webapp] X-Forwarded-For:[10.10.0.14] X-Forwarded-Proto:[https] X-Request-Id:[0092d3f0-27e0-95d1-ace1-2da21077f03e]], body: []

 

이렇게 커스텀 정책을 사용하는 방법까지 알아보았습니다.

 

Istio 스터디 5주차의 내용을 요약 하면 아래와 같습니다.

✅ 요약

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

 

이상 입니다.

 

감사합니다.

 

 

반응형