9장 Securing microservice communication

김진원·2025년 5월 8일

Istio

목록 보기
9/16

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

9.1 The need for application-networking security

애플리케이션 보안이란, 인가 받지 않은 사용자가 오염시키거나 훔치거나 접근해서는 안 되는 귀중한 애플리케이션 데이터를 보호하는 데 기여하는 모든 행동을 말한다.

  • 사용자 데이터를 지키려면 다음 사항이 필요하다.
    • 리소스 접근을 허가하기 전에 사용자 인증 및 인가
    • 데이터를 요청한 클라이언트로 가면서 여러 네트워크 장치를 거쳐가는 동안 데이터 도청을 방지하는 전송 중 데이터 암호화

인증(authentication)이란 클라이언트나 서버가 자신의 정체를 입증하는 절차를 말하며, 아는 것(패스워드)이나 갖고 있는 것(장치, 인증서) 또는 자기 자신(지문 같은 고유 특성)을 이용한다.
인가(authorization)란 이미 인증된 사용자가 리소스의 생성이나 조회, 갱신, 삭제 같은 작업을 수행하는 것을 허용하거나 거부하는 절차를 말한다.


9.1.1 Service-to-service authentication

  • 서비스는 안전하기 위해서 상호 작용하는 서비스를 모두 인증해야 합니다.
  • 확인 가능한 ID를 통해 다른 서비스를 신뢰한다.
  • Istio가 SPIFFE 프레임워크를 이용하여 서비스들의 ID 발급을 자동화하는 방법을 다룹니다.

9.1.2 End-user authentication

  • 최종 사용자 인증은 사용자의 개인 데이터를 저장하는 애플리케이션의 핵심입니다.
  • 인증 프로토콜은 여러 가지가 있지만, 대부분 인증 서버로 리다이렉션하는 것이 핵심입니다.
  • 사용자가 인증 서버에서 로그인을 성공하면 사용자 정보를 담고 있는 자격 증명(HTTP Cookie, JWT로 저장)을 받습니다.
  • 인증을 위해 전달 받은 자격 증명을 서비스에 제시합니다.

서비스는 어떤 종류든 접근을 허용하기 전에 자격 증명을 발급한 인증 서버에 자격 증명을 검증합니다.


9.1.3 Authorization

  • 인가는 호출자가 인증된 후에 진행됩니다.
  • 서버가 호출자를 "누구"인지를 식별하고, 이 ID가 "어떤" 작업을 할 수 있도록 허용돼 있는지, 확인 후에 승인 또는 거부합니다.
  • Istio는 서비스 인증과 ID 모델을 기반으로 서비스 사이 또는 사용자와 서비스 사이에서 세분화된 인가 기능을 제공합니다.

9.1.4 Comparison of security in monoliths and microservices

  • 마이크로서비스와 모놀리스 모두 최종 사용자 및 서비스인증인가를 구현해야 합니다.
  • 그러나 마이크로서비스에는 보호해야 하는, 네트워크를 오가는 커넥션과 요청이 훨씬 더 많다.
  • 반면 모놀리스는 커넥션이 더 적고, 보통은 가상머신 혹은 물리 머신 같은 더 정적인 인프라에서 실행된다.
  • 정적인 인프라에서 실행하면 (고정) IP 주소ID 확인 근거로 심기 좋으며, 덕분에 인증용 인증서에서 흔하게 사용한다. (네트워크 방화벽 규칙에도 사용한다)

  • 반면에 마이크로서비스는 쉽게 수백, 수천 개의 서비스로 불어나므로 정적 환경에서는 서비스를 운영할 수 없다.

  • 클라우드와 컨테이너는 동적 환경을 활용하기에 수명이 짧기에 IP 주소를 사용하는 전통적인 방법들은 ID 근거로 부족해 집니다.

  • 설상가상으로 서비스가 반드시 같은 네트워크에서 실행되는 것도 아니며, 여러 클라우드 프로바이더에 걸쳐 있거나 심지어아래 그림처럼 온프레미스에서도 실행 될 수 있다.

  • 이런 문제를 해결하여 고도로 동적이고 이질적인 환경에서 ID제공하고자 IstioSPIFFE 사양을 사용한다.
  • SPIFFE는 고도로 동적이고 이질적인 환경에서 워크로드에 ID를 제공하기 위한 일렬의 오픈소스 표준이다

9.1.5 How Istio implements SPIFFE

  • SPIFFE ID는 RFC 3986 호환 URI로, spiffe://trust-domain/path 형식으로 구성된다.
  • trust-domain 은 개인 또는 조직 같은 ID 발급자를 나타낸다.
  • path는 trust-domain 내에서 워크로드를 유일하게 식별한다.
  • path가 워크로드를 식별하는 자세한 방법은 정해져 있지 않아서 SPIFFE 명세 구현자가 결정할 수 있다.
  • 이스티오에서는 이 path를 특정 워크로드가 사용하는 서비스 어카운트로 채운다.
  • SPIFFE IDSVID (Spiffe Verifiable Identity Document, SPIFFE 검증할 수 있는 ID 문서) 라고도 하는 X.509 인증서인코딩되며, 이는 Istio의 컨트롤 플레인이 워크로드마다 만들어낸다.
  • 이 인증서는 전송 데이터를 암호화함으로써 서비스 간 통신의 전송을 보호하는데 사용된다.

9.1.6 Istio security in a nutshell

  • 이스티오 보안을 이해하기 위해 이스티오가 정의한 커스텀 리소스로 프록시를 설정하는 서비스 메시 운영자의 관점으로 바꿔보자.
    • PeerAuthentication 리소스는 서비스 간의 트래픽을 인증하도록 프록시를 설정한다.
      • 인증에 성공하면, 프록시는 상대 peer의 인증서에 인코딩된 정보를 추출해 요청 인가에 사용할 수 있도록 한다.
    • RequestAuthentication 리소스는 프록시가 최종 사용자의 자격 증명을 발급 서버에 확인해 인증하도록 설정한다.
      • 인증에 성공하면, 역시 자격 증명에 인코딩된 정보를 추출해 요청 인가에 사용할 수 있도록 한다.
    • AuthorizationPolicy 리소스는 앞선 두 리소스에 따라 추출한 정보를 토대로 프록시가 요청을 인가하거나 거부하도록 구성한다.

  • 아래 그림은 PeerAuthenticationRequestAuthentication 리소스가 어떻게 요청을 인증하도록 프록시를 구성하는지, 자격 증명(SVID나 JWT)에 인코딩된 정보가 어느 시점에 추출돼 필터 메타데이터로 저장되는지를 보여준다.
  • 필터 메타데이터커넥션 ID를 나타낸다.
  • AuthorizationPolicy 리소스는 그 커넥션 ID에 기반해 요청을 허가할지 거부할지를 결정한다.

  • PeerAuthentication : 서비스-to-서비스 인증 설정, 인가를 위한 피어 정보 추출
  • RequestAuthentication : End-user 인증 설정, 인가를 위한 유저 정보 추출
  • AuthorizationPolicy : PeerAuthenticationRequestAuthentication 에서 추출한 피어/유저 정보에 기초하여 권한 판단을 위한 인가 정책을 설정

Istio Security Architecture

  • Istio CA인증서를 관리하며, 인증서의 SAN은 SPIFFE 형식입니다.
  • Istiod는 메쉬모든 사이드카인증권한 부여 보안 정책을 배포합니다.
  • 사이드카는 Istiod가 배포한 보안 정책에 따라 인증 및 권한 부여를 시행합니다.


9.2 Auto mTLS

  • 사이드카 프록시가 주입된 서비스 사이의 트래픽은 기본적으로 암호화되며 서로 인증합니다.
  • 인증서를 발급하고 로테이션하는 절차를 자동화하는 것은 사람이 관리하여 문제가 많이 발생하기에 자동화를 통해 문제를 해결할 수 있습니다.
  • 아래 그림은 컨트롤 플레인에서 발급한 인증서로 서비스들이 서로 인증하고 트래픽을 암호화하는 방식입니다.
  • 서비스 메시가 서로 인증한 트래픽만 허용하도록 설정해야 한다.
  • 왜 이것이 설치할 때 기본값이 아닌지 궁금할 수 있다. 이는 메시 채택을 용이하게 하려는 설계 결정이다.
  • 여러 팀이 자체 서비스를 관리하는 거대 엔터프라이즈에서는 모든 서비스를 메시로 옮기기까지 몇 달 혹은 몇 년에 걸치 조직적인 노력이 필요할 수 있다.
  • 두 번째로, 서비스를 인증하면 최소 권한 원칙을 준수할 수 있고, 각 서비스에 정책을 만들 수 있으며, 기능에 필요한 최소한의 접근만 허용할 수 있다.
  • 이는 아주 중요한데, 서비스의 ID를 나타내는 인증서가 잘못된 사람에게 넘어갔을 때 피해 범위를 ID가 접근할 수 있도록 허용된 일부 서비스만으로 좁힐 수 있기 때문이다.

9.2.1 Setting up the environment(실습)

  • mTLS 실습을 위해 3가지 서비스 준비합니다.
  • sleep 서비스를 추가 : 레거시 워크로드로, 사이드카 프록시가 없어서 상호 인증을 할 수 없습니다.

  • 실습 환경 설정
# 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 리소스로 평문 트래픽을 금지할 수 있다.


9.2.2 Understanding Istio’s PeerAuthentication resource

PeerAuthentication 리소스를 사용하면 워크로드가 mTLS를 엄격하게 요구하거나 평문 트래픽을 허용하고 받아들이게 설정할 수 있습니다.

  • 이들 각각 STRICT 혹은 PERMISSIVE 인증 모드를 사용한다.
  • 상호 mutual 인증 모드는 다양한 범위에서 구성할 수 있다.
    • Mesh-wide PeerAuthentication 정책은 서비스 메시의 모든 워크로드에 적용된다.
    • Namespace-wide PeerAuthentication 정책은 네임스페이스 내 모든 워크로드에 적용된다.
    • Workload-specific PeerAuthentication 정책은 정책에서 명시한 셀렉터에 부합하는 모든 워크로드에 적용된다.

# 메시 범위 정책으로 모든 미인증 트래픽 거부하기

  • 메시의 보안을 향상시키기 위해 STRICT 상호 인증 모드를 강제하는 메시 범위 MESH-WIDE 정책을 만들어서 평문 트래픽을 금지할 수 있다.
  • 메시 범위 PeerAuthentication 정책은 두 가지 조건을 충족해야 한다.
  • 반드시 이스티오를 설치한 네임스페이스에 적용해야 하고, 이름은 ‘default’여야 한다.

메시 범위 리소스의 이름을 ‘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이 설정에 없다는 뜻입니다.
  • 평문 요청이 거부된 것을 확인했고, STRICT 모드는 진행하는 프로젝트에서 협업으로 인해 쓰기 어려우니, 제한을 점진적으로 늘려 시간을 가지기 위해 PERMISSIVE 모드를 사용하여 암호화와 평문 요청을 모두 허용합니다.

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

  • 네임스페이스 범위 정책을 사용하면 메시 범위 정책을 덮어 쓸 수 있고, 네임스페이스의 워크로드에 더 잘 맞는 PeerAuthentication 요구 사항을 적용할 수 있다.
  • 다음 PeerAuthentication 리소스는 istioinaction 네임스페이스의 워크로드가 sleep 서비스와 같이 메시의 일부가 아닌 레거시 워크로드로부터 평문 트래픽을 받아들이도록 허용한다.
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

  • 200 통신 확인

# 워크로드별 PeerAuthentication 정책 적용하기

  • webapp 만 목표로 하기 위해 워크로드 셀렉터를 지정해 상술했던 PeerAuthentication 정책을 업데이트 합니다.
  • 이에 렉터에 부합하는 워크로드에만 적용됩니다.
  • 또한 이름을 ‘default’에서 'webapp'으로 바꿉니다.
  • 동작이 바꾸지는 않지만, 네임스페이스 전체에 적용되는 PeerAuthentication 정책만 ‘default’로 짓는 컨벤션을 따르려는 겁니다.
# 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
  • webapp 통신 성공
  • catalog 통신 실패

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


# 두 가지 추가적인 상호 인증 모드

  • 대부분의 경우 STRICTPERMISSIVE 모드를 사용할 것이다. 그러나 두 가지 모드가 더 있다.
    • UNSET : 부모의 PeerAuthentication 정책을 상속한다. Inherit the PeerAuthentication policy of the parent.
    • DISABLE : 트래픽을 터널링하지 않는다. 그냥 보낸다. Do not tunnel the traffic; send it directly to the service.
  • PeerAuthentication 리소스를 이렇게 사용할 수 있다.
  • 상호 인증 트래픽, 평문 트래픽 등 워크로드로 터널링할 트래픽 유형을 지정하거나, 요청을 프록시로 보내지 않고 애플리케이션으로 바로 포워딩할 수 있다.

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

  • Istio Proxy는 tcpdump가 설치되어 있어, 인터페이스에서 네트워크 트래픽을 분석할 수 있다.
  • tcpdump는 보안으로 인해 권한(privileged permission)이 필요합니다.
  • istioctl로 속성 values.global.proxy.privileged=true 로 설정해 이스티오 설치를 업데이트합니다.

  • 파드 트래픽을 스니핑 sniffing
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 이상 있는 패킷만 캡처
  • 호출 확인

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

  • 상호 인증을 다룬 절을 끝내기 전에 발급된 인증서가 유효한 SVID 문서인지, SPIFFE ID가 인코딩돼 있는지, 그 ID가 워크로드 서비스 어카운트와 일치하는지 확인합니다.
  • openssl 명령어를 사용해 catalog 워크로드의 X.509 인증서 내용물을 확인합니다.
# (참고) 패킷 모니터링 : 아래 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의 내용물이 유효한지 살펴보자.
  • 루트 인증서는 istio-proxy 컨테이너에서 /var/run/secrets/istio/root-cert.pem 경로에 마운트돼 있다.

peer-to-peer 인증을 용의하게 하는 모든 구성 요소를 검증했으므로, 발급된 ID는 검증할 수 있는 것이고 트래픽은 안전하다는 것을 확신할 수 있다.


9.3

  • 인가란 인증된 주체가 리소스 접근, 편집, 삭제 같은 작업을 수행하도록 허용됐는지 정의하는 절차다.
  • 정책은 인증된 주체(’누가’)와 인가(’무엇’)를 결합해 형성되며, 누가 무슨 일을 할 수 있는지 정의한다


9.3.1 Understanding authorization in Istio

  • 각 서비스와 함께 배포되는 서비스 프록시가 인가 또는 집행 enforcement 엔진이다.
  • 서비스 프록시요청을 거절하거나 허용할지 여부를 판단하기 위한 정책을 모두 포함하고 있기 때문이다.
  • 그러므로 이스티오의 접근 제어는 대단히 효율적이다. 모든 결정이 프록시에서 직접 내려지기 때문이다.
  • 프록시는 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 필드는 이 정책이 허용(ALLOW)인지, 거부(DENY)인지, 커스텀(CUSTOM)인지 지정한다.
    • action은 규칙 중 하나가 요청과 일치하는 경우에만 적용된다.
  • rules 필드는 정책을 활성화할 요청을 식별하는 규칙 목록을 정의한다.

인가 정책 규칙 이해하기

  • 인가 정책 규칙은 커넥션은 출처를 지정하며, 일치해야 규칙을 활성화하는 작업 조건을 지정할 수도 있다.
  • 인가 정책은 규칙 중 하나의 출처와 작업 조건을 모두 만족시키는 경우에만 집행된다.
  • 이 경우에만 정책이 활성화되고, 커넥션은 action 속성에 따라 허용되거나 거부된다.
  • 단일 규칙의 필드는 다음과 같다.
    • from 필드는 요청의 출처를 다음 유형 중 하나로 지정한다.
      • principals : 출처 ID(mTLS 예제에서 볼 수 있는 SPIFFE ID). 요청이 주체 집합에서 온 것이 아니면 부정 속성인 notprincipals 가 적용된다. 이 기능이 작동하려면 서비스가 상호 인증해야 한다.
      • namespaces : 출처 네임스페이스와 비교할 네임스페이스 목록. 출처 네임스페이스는 참가자의 SVID에서 가져온다. 이런 이유로, 작동하려면 mTLS가 활성화돼야 한다.
      • ipBlocks : 출처 IP 주소와 비교할 단일 IP 주소나 CIDR 범위 목록.
    • to 필드는 요청의 작업을 지정하며, 호스트나 요청의 메서드 등이 있다.
    • when 필드는 규칙이 부합한 후 충족해야 하는 조건 목록을 지정한다.

9.3.2 Setting up the workspace (실습)

# 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


9.3.3 Behavior changes when a policy is applied to a workload

  • 워크로드에 하나 이상의 ALLOW 인가 정책이 적용되면, 모든 트래픽에서 해당 워크로드로의 접근은 기본적으로 거부된다.
  • 트래픽을 받아들이려면, ALLOW 정책이 최소 하나는 부합해야 한다.

다음 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 리턴
  • 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 - -
  • 확인

  • 첫 번째 호출은 경로가 일치하여 요청을 허용했다.
  • 두 번째는 ALLOW 정책을 워크로드에 적용했을 때만 적용되는 deny-by-default 동작으로 거부되었다.

  • 아래 그림은 전체 거부 정책이 어떻게 ‘명시적으로 지정되지 않으면 요청을 거부한다’로 바꾸는지 보여준다. 그럼 트래픽을 허용하기만 하면 된다.

9.3.4 Denying all requests by default with a catch-all policy

  • 보안성을 증가시키고 과정을 단순화하기 위해, ALLOW 정책을 명시적으로 지정하지 않은 모든 요청을 거부하는 메시 범위 정책을 정의합니다.
  • 기본 거부 catch-all-deny-all 정책을 정의한다.
# 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
...

# 빈 규칙 rules 은 모든 요청을 허용 의미

# cat ch9/policy-allow-all-mesh.yaml                         
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-all
  namespace: istio-system
spec: 
  rules: 
  - {}

9.3.5 Allowing requests originating from a single namespace

  • 특정 네임스페이스에서 시작한, 모든 서비스에 대한 트래픽을 허용하고 싶을 것이다.
  • source.namespace 속성으로 할 수 있다.
  • 예제는 한 네임스페이스에서 온 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                       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
...
  • 사이트카가 없으므로, ID도 없다. 그러므로 webapp 프록시는 요청이 default 네임이스페이스의 워크로드에서 온 것인지 확인할 수 없다.
  • 이를 해결하려면 다음 중 하나를 할 수 있다.
    1. sleep 서비스에 서비스 프록시 주입하기 → 실습 진행
    2. 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 은 성공

9.3.6 Allowing requests from non-authenticated legacy workloads

  • 미인증 워크로드에서 온 요청을 허용하려면 from 필드를 삭제해야 한다.
  • 아래 정책을 webapp에만 적용하기 위해 app: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"]
  • 호출 테스트
# 호출 테스트 : 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 서비스로의 후속 요청을 거부했다.


9.3.7 Allowing requests from a single service account

  • 트래픽이 webapp 서비스에서 왔는지 인증할 수 있는 간단한 방법은 트래픽에 주입된 서비스 어카운트를 사용하는 것이다.
  • 서비스 어카운트 정보는 SVID에 인코딩돼 있으며, 상호 인증 중에 그 정보를 검증하고 필터 메타데이터에 저장한다.
  • 다음 정책은 catalog 서비스가 필터 메타데이터를 사용해 서비스 어카운트가 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"]

실습

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


9.3.8 Conditional matching of policies

  • 어떤 정책은 특정 조건이 충족되는 경우에만 적용되기도 한다.
  • 사용자가 관리자일 때는 모든 작업을 허용하는 식이다.
  • 이는 다음 에제처럼 인가 정책의 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이 포함돼 있어야 한다.

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에서 온다는 점에서 차이가 있다.


9.3.9 Understanding value-match expressions

  • Istio는 규칙을 더 다양하게 만들 수 있도록 간단한 비교 표현식을 지원한다.
    • Exact matching of values 일치. 예를 들어 GET은 값이 정확히 일치해야 한다.
    • Prefix matching of values 접두사 (매칭)비교. 예를 들어 /api/catlog*/api/catalog/1 과 같이 접두사로 시작하는 모든 값에 부합한다.
    • Suffix matching of values 접미사 (매칭)비교. 예를 들어 *.istioinaction.iologin.istioinaction.io 와 같이 모든 서브도메인에 부합한다.
    • Presence matching 존재성 (매칭)비교. 모든 값에 부합하며 *로 표기한다. 이는 필드가 존재해야 하지만, 값은 중요하지 않아 어떤 값이든 괜찮음을 의미한다.
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"]
  1. 인가 정책이 요청에 적용되려면, 첫 번째 규칙이나 두 번째 규칙에 해당해야 한다.
  2. 규칙들에 해당하기 위해, 세 가지 속성(from, to, when)에 모두 부합해야 한다.
  • from 에서 정의한 source 가 to 에 정의한 operation 중 하나와 AND 연산되고, 둘 다 when 에서 지정한 조건들 모두와 AND 연산된다.
  1. operation 이 부합하려면 모든 속성이 부합해야 한다. 즉, 모든 속성이 AND로 연결된다.
  2. when 속성의 경우도 AND로 연결되기 때문에 모든 조건이 부합해야 한다.

9.3.10 Understanding the order in which authorization policies are evaluated

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

  1. CUSTOM policies are evaluated first.
  2. DENY policies are evaluated next. If no DENY policy is matched . . .
  3. ALLOW policies are evaluated. If one matches, the request is allowed. Otherwise. . .
  4. According to the presence or absence of a catch-all policy, we have two outcomes:
  • When a catch-all policy is present, it determines whether the request is approved.
  • When a catch-all policy is absent, the request is:
    - Allowed if there are no ALLOW policies, or it’s ALLOW
    - Rejected when there are ALLOW policies but none matches.

  • 아래 흐름도를 통해 조건에 따라 동작을 이해합니다.
  • DENY 정책을 정의하면 간단해진다.



9.4 End-user authentication and authorization

9.4.1 What is a JSON web token?

  • JWT는 클라이언트르 서버에 인증하는 데 사용하는 간단한 클레임 표현이다.
  • JWT는 다음 세 가지 부분으로 이뤄져 있다.
    • 헤더 : 유형 및 해싱 알고리듬으로 구성
    • 페이로드 : 사용자 클레임 포함
    • 서명 : JWT의 진위 여부를 파악하는 데 사용

이 세 부분, 즉 헤더, 페이로드, 서명이 점(.)으로 구분되고 Base64 URL로 인코딩되기 때문에 JWT는 HTTP 요청으로 사용하기에 매우 적합하다.

  • ch9/enduser/user.jwt 에 있는 토큰의 내용물을 확인해보고, 페이로드를 디코딩한다.
#
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 및 인가를 판단할 수 있다.


# JWT는 어떻게 발행되고 검증되는가?

  • JWT(JSON 웹 토큰)는 인증 서버에서 발급되는데, 인증 서버는 토큰을 서명하는 비밀 키와 검증하기 위한 공개 키를 갖고 있다.
  • 공개 키JWKS JSON Web Key Set, JSON 웹 키셋 라고 하며, well-known HTTP 엔드포인트노출된다.
  • 서비스는 이 엔드포인트에서 공개 키를 가져와 인증 서버가 발급한 토큰을 검증할 수 있다.

  • 아래 그림은 서버가 토큰을 검증하는 데 JWKS를 어떻게 사용하는지 시각화한다.
  1. 인증서버는 “토큰 서명”을 위한 private key 와 “토큰 검증”을 위한 public key를 가지고 있음
  2. 인증서버에서 private key 로 서명한 JWT (JSON Web Token) 을 발급
  3. 인증서버의 public key는 JWKS (JSON Web Key Set) 형태의 HTTP 엔드포인트로 제공
  4. 서비스는 인증서버에서 발급된 JWT 를 검증하기 위해 필요한 public key를 JWKS 에서 찾습니다.
  5. public key로 JWT 서명을 복호화 하여 얻은 해시값과 JWT 토큰 데이터의 해시값을 비교하여
  6. 해시값이 동일할 경우 토큰 claim에 변조가 없었음을 보장하므로 신뢰할 수 있습니다.

9.4.2 End-user authentication and authorization at the ingress gateway

  • Istio 워크로드가 JWT로 최종 사용자 요청을 인증하고 인가하도록 설정할 수 있다.

  • 최종 사용자ID 제공자에게 인증받고 ID클레임을 나타내는 토큰발급받은 사용자를 말한다.

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

    • 이렇게 하면 유효하지 않은 요청을 조기에 거부하므로 성능이 좋아진다
    • 또한 요청에서 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

9.4.3 Validating JWTs with RequestAuthentication

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


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

  • 유효한 JWT로 요청
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 된다.


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

  • 유효하지 않은 JWT로 요청
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 요청 실행
# 호출 
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가 없는 요청 거부하려면 명시적으로 거부하는 AuthorizationPolicy 리소스를 만들어야 한다.
  • 이 정책은 requestPrincipals 속성이 없는 source 에서 온 모든 요청에 적용되며, (action 속성에 지정된 대로) 요청을 거부한다.
  • JWT의 발행자 issuer 와 주체 subject 클레임을 ‘iss/sub’ 형태로 결합한 것
# 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 클레임에 기반한 다양한 접근 수준

유저별로 다른 접근 정책 설정

  • 일반 사용자가 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


# 수집된 메타데이터를 관찰하고자 서비스 프록시에 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 에 새 아이템을 만들 수 있도록 허용하고 있음을 확인.



9.5

요청을 허용할지 여부를 결정할 때 외부 인가 서비스를 호출하도록 Istio의 서비스 프록시를 설정할 수 있다.

  • 위 그림에서 서비스 프록시에 들어온 요청은 프록시가 외부 인가(ExtAuthz) 서비스를 호출하는 동안 잠시 멈춘다.
  • 외부 인가 서비스는 애플리케이션 사이드카로 메시 안에 존재하거나 메시 바깥에 존재할 수 있다.
  • 외부 인가는 엔보이CheckRequest API를 구현해야 한다.

9.5.1 Hands-on with external authorization (실습)

  • 실습 환경
# 기존 인증/인가 정책 모두 삭제
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 인지만 검사한다.
  • 이 헤더가 요청에 들어 있으면 요청허용되고, 들어 있지 않으면 요청은 거부된다.

9.5.2 Configuring Istio for ExtAuthz

  • 새로운 외부 인가 서비스를 인식하도록 설정하기 위해 meshconfig 설정에서 extensionProviders 를 설정해야 한다.
  • 이 설정은 istio-system 네임스페이스의 istio configmap에 있다.
  • 이 configmap 을 수정해 새 외부 인가 서비스에 대한 적절한 설정을 추가해보자.
# 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가 envoyExtAuthz 서비스의 HTTP 구현체새 확장 sample-ext-authz-http 를 인식하도록 설정했다.
  • 이 서비스는 에 ext-authz.istioinaction.svc.cluster.local 위치하는 것으로 정의했는데, 앞 절에서 봤던 쿠버네티스 서비스에 맞춘 것이다.
  • 외부 인가 서비스에 전달할 헤더를 구성할 수 있는데, 이 설정에서는 x-ext-authz 헤더를 전달한다.

9.5.3 Using a custom AuthorizationPolicy resource

  • action 이 CUSTOM 인 AuthorizationPolicy 를 만들고 정확히 어떤 외부 인가 서비스를 사용할지 지정합니다.
# 아래 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: []


Summary

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

0개의 댓글