Service_Mesh/Istio

[Istio-5주차] 마이크로서비스 통신 보안 (이론, 실습)

lati-tech 2025. 5. 11. 04:53

사전 지식 - 추가 정보 : 암호학 (정리 중)

더보기

배경 : 암호(대칭키/비대칭키), 전자서명, PKI(X.509) 등 이해 필요

  • 정보 보안 3요소 - Link
    • 기밀성(암호화, 액세스 제어..): 데이터의 개인정보 보호를 의미하며, 그 목표는 무단 액세스로부터 중요한 기밀 정보를 보호하는 것. 몇 가지 관련 도구에는 암호화, 액세스 제어, 데이터 손실 방지 등이 있음
    • 무결성(무단 변조 방지): 정보의 정확성과 신뢰성을 의미하며, 무단 변조 또는 수정을 방지하는 것을 목표로 함. 파일 권한, ID 관리, 감사 추적 등의 도구는 데이터의 무결성을 보장할 수 있도록 도움.
    • 가용성: 정보 시스템의 가용성과 접근성을 보장하여 비용이 많이 드는 다운타임이나 중단을 방지.
      데이터 가용성을 보장하는 데 도움이 되는 몇 가지 보호 조치 - 시스템 업데이트, 재해 복구 계획, 비즈니스 연속성 계획
  • 액세스 제어(AAA) - Link
    인증 (Authentication), 권한부여 (Authorization), 계정관리 (Accounting)​

    • 인증(Authentication):  망,시스템 접근을 허용하기 전에 사용자의 신원을 검증 (Who?)
    • 권한부여(Authorization):  검증된 사용자에게 어떤 수준의 권한과 서비스를 허용 (What?)
    • 계정관리(Accounting):  사용자의 자원에 대한 사용 정보를 모아서, 과금,감사,용량증설,리포팅 등


암호학 - 네트워크보안프로토콜 책 발췌(2004년 출판, 현재 절판)

  • 암호/복호과정 이란 평문을 제 3자가 해독할 수 없는 문장인 암호문으로 변환하여 전송하고, 수신측에서 평문으로 암호문으로부터 평문 복원하는 과정
  • 이 과정에서 사용되는 알고리즘 암호 알고리즘이라고 하며, 암호화하고 복호화는데 핵심이 되는 것이 
  • 암호화 과정에서 필요한 키를 쌍방이 확보할 수 있는 방법: 공유 비밀키 방식(shared secret key) / 공개키 방식(public key)
    • 공유 비밀키 암호화 방식 (shared secret key) : DES, 3DES, RC4/5
    • 공개키 암호화 방식 (public key) : RSA
    • 키 분배 방식 : ‘DH방식, RSA공개키, 키 분배 센터(KDC)’을 이용한 공유 비밀키 분배 방법


암복호화 예시

  • ex) ‘ABC’ 문장을 다음과 같은 규칙을 사용하여 전송한다고 가정

    🔑 “각 문자의 ASCII 코드 값에 x씩 더하여 전송한다.” 그리고 “x = 3” 이다.
    • 이 경우, “ABC” 문자열을 구성하는 0x41,0x42,0x43 값은 각각 0x44, 0x45, 0x46으로 변환되어 “DEF”로 전송
    • 이 규칙을 사전에 알고 있는 수신한 측은 수신된 바이트 열에서 3씩 빼서 원래의 “ABC”문자열을 복원
    • 전송과정과 수신과정에서 사용되는 변환 규칙인 “각 문자의 ASCII 코드 값에 x씩 더하여 전송한다.” 를 암호 알고리즘이라고 지칭하며, “x=3”이 키 값.
    • 암호 알고리즘은 제 3자에게 공개되어도 되지만, 키 값은 쌍방만이 알아야 함

공유 비밀키 암호 방식 (shared secret key)

대칭키 암호화 - https://www.lgcns.com/blog/cns-tech/security/16037/
  • 대칭키 암호화 방식
    • 쌍방이 공유하는 하나의 비밀키를 사용하여 암호화하고 복호하는 방식으로서, 대칭(symmetric) 혹은 관용(conventional) 암호 방식이라고도 지칭
    • 장점
      • 성능 우수
      • 암/복호화에 CPU 리소스 소모가 적음
    • 단점
      • 비밀키 분실 시 복호화가 힘듬
      • 비밀키를 상대방에게 전달해줘야 함
    • 암호화 예
      • des3 암호화 알고리즘을 이용하여 파일내용 암호화
        [root@ahchim ~]# echo 'this is the plane text' > plaintext.txt
        
        [root@ahchim ~]# openssl enc -e -des3 -salt -in plaintext.txt -out ciphertext.bin
        enter des-ede3-cvc encryption password:
        Verifying - enter des-ede3-cbc encryption password:
      • 암호화된 파일의 내용을 확인
        [root@ahchim ~]# cat ciphertext.bin
        Salted__▮^iw▮q0UUx0G▮   ▮▮AC] óJ^▮]
      • 암호화된 파일을 복호화하여 내용을 확인
        [root@ahchim ~]# openssl enc -d -des3 -in ciphertext.bin
        enterh des-ede3-cbc decryption password:
        this is the plain text
  • 블록 암호화 방식(Block cipher)
    • 평문을 일정길이 단위로 분할한 블록들에 대하여 암호화 하는 방식
    • DES, 3DES, IDEA, RC4/5 등이 있음
    • (a)는 DES방식으로, 평문을 64비트의 블록으로 분할한 후, 각 블록에 대하여 56비트의 키로 암호화하여, 64비트의 암호문 블록을 생성
      • 만약 평문이 64비트로 분할되지 않는 경우, 필요한 비트 수 만큼 패딩하여 암호화
    • (b)는 두 종류의 56비트 키와 세 번에 걸친 DES 과정을 수행하여 보다 안전한 암호문을 생성
  • 공유 비밀키 암호화 고려사항
    1. 보내는 사람과 받는 사람에 대한 신원 확인을 할 수 있는가? ⇒ 서명
    2. 누군가 중간에 데이터를 가로채서, 임의의 값을 추가하여 변조(수정)하여 전달 할 경우 어떻게 무결성을 확인 할 수 있는가? ⇒ 해시알고리즘
    3. 온라인 상에서, 키 값을 어떻게 안전하게 전달할 것인가?

 

단방향 알고리즘 (해시) 알아보기 - 생활코딩 , 노마드코더

  • 파일 무결성 확인 방법 - KrBlog
    • 파일 전체 비교 → 파일의 지문(fingerprint) 비교 : 해시 함수 적용의 결과값(다이제스트)을 비교
  • 일정한 크기의 출력 : 해시 값의 길이는 메시지 길이와 관계 없음. 메시지가 1비트, 1기가바이트라도 고정된 길이의 해시 값을 출력
  • 메시지가 다르면 해시 값도 다름 : 메시지가 1비트라도 변화하면 해시 값은 매우 높은 확률로 다른 값이 되어야 함
  • 일방향성 : 해시 값으로부터 메시지를 역산할 수 없음 쉽게 암호화는 가능하지만 복호화는 불가능
  • 충돌 내성 : 총돌을 발경하는 것이 어려운 성질 - 강한/약한 충돌 내성

 

  • 레인보우 테이블(영어: rainbow table) - wiki , namuwiki , 노마드코더
    • 해시 함수를 사용하여 변환 가능한 모든 해시 값을 저장시켜 놓은 표
    • 보통 해시 함수를 이용하여 저장된 비밀번호로부터 원래의 비밀번호를 추출해 내는데 사용
       ⇒ 입력값이 바뀌지 않고 고정이면 결과값도 그대로임을 이용
    • 상세
      • 레인보우 테이블은 일반적으로 비밀번호 해시 크래킹을 위해 암호화 해시 함수의 출력을 캐싱하기 위해 미리 계산된 표이다.
      • 비밀번호는 일반적으로 일반 텍스트 형식이 아닌 해시 값으로 저장된다.
      • 해시된 비밀번호 데이터베이스가 공격자의 손에 들어간 경우 사전 계산된 레인보우 테이블을 사용하여 일반 텍스트 비밀번호를 복구할 수 있다.
      • 이 공격에 대한 일반적인 방어는 각 비밀번호를 해시하기 전에 각 비밀번호에 "솔트"를 추가하는 키 파생 기능을 사용하여 해시를 계산하는 것이다.
      • 서로 다른 비밀번호는 해시와 함께 일반 텍스트로 저장되는 서로 다른 솔트를 수신한다.
      • 레인보우 테이블은 시공간 절충의 실제적인 예이다.
      • 모든 시도에서 해시를 계산하는 무차별 대입 공격보다 컴퓨터 처리 시간과 저장 공간을 덜 사용하지만, 데이터를 저장하는 간단한 테이블보다 처리 시간과 저장 공간을 더 많이 사용한다. 가능한 모든 비밀번호의 해시이다.

 

 

  • 서비스 메시에서 서비스 간 인증 및 인가 처리하기 Handling service-to-service authentication and authorization in the service mesh
  • 최종 사용자 인증 및 인가 처리하기 Handling end-user authentication and authorization
  • 들어가며
    • 트래픽을 메시로 허용하는 방법에 이어 트래픽을 보호하는 방법
    • 서비스 메시 기능을 사용해 서비스 기반 아키텍처의 보안 태세를 투명하게 개선하는 방법
    • Istio 기본적인 안전 보장 방법
      • 서비스 간 및 최종 사용자 인증
      • 서비스 메시 내 서비스에 대한 접근 제어

 

  • 들어가며: 인증, 인가, 전송 중 데이터 암호화
    • 애플리케이션 보안: 인가 받지 않은 사용자가 오염시키거나 훔치거나 접근해서는 안 되는 귀중한 애플리케이션 데이터를 보호하는 데 기여하는 모든 행동
      • 사용자 데이터 보호 방법
        • 사용자 인증 및 인가: 리소스 접근을 허가하기 전 감사 절차
        • 전송 중 데이터 암호화: 데이터를 요청한 클라이언트로 가면서 여러 네트워크 장치를 거쳐가는 동안 데이터 도청을 방지
인증, 인가 차이점

인증
(authentication): 클라이언트나 서버가 자신의 정체를 입증하는 절차. 아는 것(패스워드)이나 갖고 있는 것(장치, 인증서) 또는 자기 자신(지문 같은 고유 특성)을 이용

인가
(authorization): 이미 인증된 사용자가 리소스의 생성이나 조회, 갱신, 삭제 같은 작업을 수행하는 것을 허용하거나 거부하는 절차

 

  1. 9.1.1 서비스 간 인증 Service-to-service authentication - SPIFFE 프레임워크
    • 서비스는 자신이 상호작용하는 서비스는 모두 인증해야 안전 (확인할 수 있는 ID 문서를 제시한 후에만 다른 서비스를 신뢰)
    • 신뢰할 수 있는 제3자에게 확인 (문서 발급 주체)
    • Istio는 서비스들의 ID 발급을 자동화하기 위해 SPIFFE Secure Prduction Identity Framework For Everyone 프레임워크를 사용
      • 분산 시스템용 범용 ID 컨트롤 플레인 - https://spiffe.io/
      • SPIFFE 와 SPIRE 는 다양한 플랫폼의 워크로드에 강력하게 검증된 암호화 ID 제공
      • SPIFFE 는 이기종 환경과 조직 경계를 넘나드는 서비스에 ID를 부트스트래핑하고 발급할 수 있는 framework를 위한 오픈 소스 스펙(specifications) 세트
        • 이 스펙의 핵심은 단기 암호화 ID 문서(short lived cryptographic identity documents) – 즉 SVID 를  간단한(simple) API를 통해 정의하는 것
        • 워크로드는 다른 워크로드에 대한 인증 시 이러한 신원 문서를 사용할 수 있음
          ex) TLS 연결을 설정하거나 JWT 토큰에 서명하고 검증하는 등의 작업 수행 가능
      • https://spiffe.io/docs/latest/spiffe-about/overview/Implements 확인
    • 발급된 ID는 서비스들이 서로 인증하는 데 사용

  2. 9.1.2 최종 사용자 인증 End-user authentication - JWT 등 자격증명
    • 최종 사용자 인증: 사용자의 개인 데이터를 저장하는 애플리케이션의 핵심
    • 최종 사용자 인증 프로토콜 대부분은 사용자를 인증 서버로 리다이렉션하는 것이 핵심
    • 과정
      • 사용자 → 인증 서버에서 로그인 성공 사용자 정보를 담고 있는 자격 증명(HTTP 쿠키나 JWT 등으로 저장) 발급 받음 사용자가 자격 증명 서비스에 제시(인증을 위함) 서비스에서 검증 접근 허용
    • 서비스는 어떤 종류든 접근을 허용하기 전에 자격 증명을 발급한 인증 서버에 자격 증명을 검증


  3. 9.1.3 인가 Authorization - 작업 수행 승인/거부
    • 인가: 사용자(호출자)가 인증된 후 진행
    • 과정
      • 호출자가 ‘누구’인지 서버에서 식별 → 서버는 해당 ID가 ‘어떤’ 작업을 수행할 수 있도록 허용돼 있는지 권한 확인  승인하거나 거부
    • 예시
      • 웹 애플리케이션에서 인가는 사용자가 리소스를 생성, 조회, 업데이트, 삭제할 수 있는지 여부를 확인
    • Istio의 인가 기능
      • 서비스 인증과 ID 모델을 기반으로 함
      • 세분화된 인가 기능
        • 서비스와 서비스 사이
        • 최종 사용자와 서비스 사이


  4. 9.1.4 모놀리스와 마이크로서비스의 보안 비교 Comparison of security in monoliths and microservices
    • 마이크로서비스 / 모놀리스 - 서비스인증, 인가 차이점
      • 모놀리스
        • 상대적으로 커넥션 소수
        • 보편적으로 가상머신 혹은 물리 머신 같은 더 정적인 인프라에서 실행
        • 정적인 인프라에서 실행하면 (고정) IP 주소ID 확인 근거로 심기 좋아, 인증용 인증서에서 흔하게 사용 (네트워크 방화벽 규칙에 사용)
          정적 인프라 - IP를 신뢰의 근거로 사용
      • 마이크로서비스
        • 커넥션 및 요청 다수 (네트워크를 오가는 보호 대상 다수)
        • 정적 환경에서는 서비스 운영 불가
          • 쉽게 수백, 수천 개의 서비스로 불어남
          • 수많은 서버로 스케줄링되고 수명이 짧음
          • 서비스가 반드시 같은 네트워크에서 실행되지 않음 (클라우드 프로바이더에 걸쳐 있거나 온프레미스에서도 실행)
        • 위와 같은 이유로 IP 주소를 사용 등의 전통적인 방법들은 ID로 활용하기 적합하지 않음
          마이크로서비스 - 클라우드와 온프레미스에서 실행되는 상호 연결 다수

        • Istio ID제공하고자 SPIFFE specification 스펙을 사용
          • SPIFFE: 고도로 동적이고 이질적인 환경에서 워크로드에 ID를 제공하기 위한 일련의 오픈소스 표준


  5. 9.1.5 Istio가 SPIFFE를 구현하는 방법 How Istio implements SPIFFE - SVID
    • SPIFFE ID
      • RFC 3986 호환 URI
      • spiffe://trust-domain/path 형식으로 구성
        • trust-domain: 개인 또는 조직 같은 ID 발급자
        • path
          • trust-domain 내에서 워크로드 식별 (유일)
          • 워크로드 식별 방식은 SPIFFE 명세 구현자가 결정 가능 (상세 방법이 정해져 있지 않음)
      • SVID (Spiffe Verifiable Identity Document, SPIFFE 검증할 수 있는 ID 문서) 라고 불리는 X.509 인증서 인코딩
      • SVID를 통해 전송 데이터를 암호화함으로써 서비스 간 통신의 전송을 보호할 수 있음
    • Istio에서의 path 처리: 특정 워크로드가 사용하는 서비스 어카운트
    • Istio에서 SPIFFE ID 인증서 인코딩 처리
      • SVID컨트롤 플레인이 워크로드마다 생성


  6. 9.1.6 Istio 보안 요약 Istio security in a nutshell - PeerAuthentication , RequestAuthentication , AuthorizationPolicy
    • Istio 보안 이해하기: Istio가 정의한 커스텀 리소스로 프록시 설정 과정 (서비스 메시 운영자 관점)
      • PeerAuthentication 리소스: 서비스 간의 트래픽을 인증하도록 프록시 설정
        • 인증 성공 시 - 프록시는 상대 peer의 인증서에 인코딩된 정보를 추출해 요청 인가에 사용 가능하도록 함
      • RequestAuthentication 리소스: 프록시가 최종 사용자의 자격 증명을 발급 서버에 확인해 인증하도록 설정
        • 인증 성공 시 - 이 역시 자격 증명에 인코딩된 정보를 추출해 요청 인가에 사용 가능하도록 함.
      • AuthorizationPolicy 리소스: 앞선 두 리소스에 따라 추출한 정보를 토대로 프록시가 요청을 인가하거나 거부하도록 구성
        메타데이터에서 검증된 데이터 수집




      • 요청을 인증 및 인가하도록 서비스 프록시를 구성하는 리소스
        • PeerAuthenticationRequestAuthentication 리소스: 요청을 인증하도록 프록시를 구성
        • 자격 증명(SVID나 JWT)에 인코딩된 정보: 특정 시점에 추출하여 필터 메타데이터로 저장
      • 필터 메타데이터: 커넥션 ID
      • AuthorizationPolicy 리소스: 그 커넥션 ID에 기반해 요청을 허가할지 거부할지를 결정
        요청을 인증 및 인가하도록 서비스 프록시를 구성하는 리소스
        • PeerAuthentication: 서비스-to-서비스 인증 설정, 인가를 위한 피어 정보 추출
        • RequestAuthentication: End-user 인증 설정, 인가를 위한 유저 정보 추출
        • AuthorizationPolicy: PeerAuthentication, RequestAuthentication 에서 추출한 피어/유저 정보에 기초하여 권한 판단을 위한 인가 정책을 설정
      • Istio 보안 아키텍처
        • Istio CA는 와 인증서를 관리하며, 인증서의 SAN은 SPIFFE 형식
        • Istiod는 메시의 모든 사이드카에 인증인가 보안 정책을 배포
        • 사이드카는 Istiod에서 배포한 보안 정책에 의거하여 인증 및 인가를 강제 시행

 

 

Istio 1.18 공식 문서 - Security 정리

 


  • 들어가며 : 인증서 발급/갱신 자동화, 추가 작업(인증, 인가)
    • 사이드카 프록시가 주입된 서비스 사이의 트래픽은 기본적으로 암호화되고 상호 인증
    • 인증서를 발급하고 로테이션하는 절차를 자동화하는 것은 매우 중요 - 역사적으로 사람이 관리 시 오류 발생
    • 인적 실수로 인한 서비스 중단은 Istio처럼 절차를 자동화했다면 피할 수 있었을 문제
    • 컨트롤 플레인에서 발급한 인증서를 사용해 서비스들이 서로 인증하고 트래픽을 암호화
      • SVID 인증서 (Istio 인증 기관에서 발급)를 활용해 상호 인증
      • 이 방식을 통해 기본적으로 안전한 상태를 유지
        워크로드는 Istio 인증 기관에서 발급한 SVID 인증서를 사용해 상호 인증함
    • 사실 ‘기본적으로 안전한’이라고 하면 기본적으로는 대부분 안전하다는 의미
      • 메시의 안전을 위해 수행해야 할 작업 잔존
        • 서비스 메시가 서로 인증한 트래픽만 허용하도록 설정 필요
          • 메시 채택을 용이하게 하기 위해 기본값이 아님
            • 여러 팀이 자체 서비스를 관리하는 거대 엔터프라이즈에서는 모든 서비스를 메시로 옮기기까지 몇 달 혹은 몇 년에 걸치 조직적인 노력이 필요할 수 있음
        • 서비스 인증 설정 필요
          • 최소 권한 원칙을 준수할 수 있고, 각 서비스에 정책을 만들 수 있으며, 기능에 필요한 최소한의 접근만 허용 가능
          • 서비스의 ID를 나타내는 인증서가 잘못된 사람에게 넘어갔을 때 피해 범위를 ID가 접근할 수 있도록 허용된 일부 서비스만으로 좁힐 수 있음
          • 때문에 아주 중요
  • TLS vs mTLS
    • TLS - 암호화방식 인증서 Handshake
      • TLS는 네트워크로 통신을 하는 과정에서 도청, 간섭, 위조를 방지하기 위해서 설계됨. 암호화를 통해 인증, 통신 기밀성을 제공.
      • TLS의 3단계 기본 절차: (1) 지원 가능한 알고리즘 서로 교환 (2) 키 교환, 인증 (3) 대칭키 암호로 암호화하고 메시지 인증
    • TLS vs MTLS - 링크 소개
      • MTLS 절차 : 서버측클라이언트측에 대한 인증서를 확인 및 액세스 권한 확인
        https://blog.cloudflare.com/protecting-the-origin-with-tls-authenticated-origin-pulls/
  1. 9.2.1 환경 설정하기 (실습~)
    • mTLS 기능 실습을 위해 3가지 서비스를 준비.
    • sleep 서비스를 추가 : 레거시 워크로드로, 사이드카 프록시가 없어서 상호 인증을 할 수 없음
      3가지 서비스를 준비
    • 실습 환경 설정
      # catalog와 webapp 배포
      kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction
      kubectl apply -f services/webapp/kubernetes/webapp.yaml -n istioinaction
      
      # webapp과 catalog의 gateway, virtualservice 설정
      kubectl apply -f services/webapp/istio/webapp-catalog-gw-vs.yaml -n istioinaction
      
      # default 네임스페이스에 sleep 앱 배포
      cat ch9/sleep.yaml
      ...
          spec:
            serviceAccountName: sleep
            containers:
            - name: sleep
              image: governmentpaas/curl-ssl
              command: ["/bin/sleep", "3650d"]
              imagePullPolicy: IfNotPresent
              volumeMounts:
              - mountPath: /etc/sleep/tls
                name: secret-volume
            volumes:
            - name: secret-volume
              secret:
                secretName: sleep-secret
                optional: true
      
      kubectl apply -f ch9/sleep.yaml -n default
      
      # 확인
      kubectl get deploy,pod,sa,svc,ep
      kubectl get deploy,svc -n istioinaction
      kubectl get gw,vs -n istioinaction


    • 기본 통신 확인 : 레거시 sleep 워크로드 → webapp 워크로드로 평문 요청 실행
      # 요청 실행
      kubectl exec deploy/sleep -c sleep -- curl -s webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
      
      # 반복 요청
      watch 'kubectl exec deploy/sleep -c sleep -- curl -s webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"'

      • 키알리 확인: 네임스페이스(default, istioinaction 선택), Show Legend 클릭 후 아이콘 확인, unknown → webapp 구간은 평문 통신
        kiali 확인

        • 응답이 성공했다는 것은 서비스들이 올바르게 준비됐으며 webapp 서비스가 sleep 서비스의 평문 요청을 받아들였다는 사실을 보여줌
        • 기본적으로 Istio는 평문 요청을 허용하는데, 이는 모든 워크로드를 메시로 옮길 때까지 서비스 중단을 일으키지 않고 서비스 메시를 점진적으로 채택할 수 있게 하기 위함
        • 그러나 PeerAuthentication 리소스로 평문 트래픽 금지 가능
  2. 9.2.2 이스티오의 PeerAuthentication 리소스 이해하기
    • 들어가며
      • PeerAuthentication 리소스를 사용 시
        • 워크로드가 mTLS를 엄격하게 요구하거나 평문 트래픽을 허용하고 받아들이게 설정할 수 있음
      • 각각 STRICT 혹은 PERMISSIVE 인증 모드를 사용
      • 상호 mutual 인증 모드는 다양한 범위에서 구성 가능
        • Mesh-wide PeerAuthentication 정책: 서비스 메시의 모든 워크로드에 적용
        • Namespace-wide PeerAuthentication 정책: 네임스페이스 내 모든 워크로드에 적용
        • Workload-specific PeerAuthentication 정책: 정책에서 명시한 셀렉터에 부합하는 모든 워크로드에 적용
    • 메시 범위 정책으로 모든 미인증 트래픽 거부하기 DENYING ALL NON-AUTHENTICATED TRAFFIC USING A MESH-WIDE POLICY
      • 메시의 보안을 향상시키기 위해 STRICT 상호 인증 모드를 강제하는 메시 범위 MESH-WIDE 정책을 만들어서 평문 트래픽을 금지할 수 있음
      • 메시 범위 PeerAuthentication 정책은 두 가지 조건을 충족해야 함
      • 반드시 Istio를 설치한 네임스페이스에 적용해야 하고, 이름은 ‘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
        
        # 확인
        kubectl get PeerAuthentication -n istio-system
        kubectl logs -n istioinaction -l app=webapp -c webapp -f
        kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
        [2025-05-01T08:32:08.511Z] "- - -" 0 NR filter_chain_not_found - "-" 0 0 0 - "-" "-" "-" "-" "-" - - 10.10.0.17:8080 10.10.0.16:51930 - -
        [2025-05-01T08:32:10.629Z] "- - -" 0 NR filter_chain_not_found - "-" 0 0 0 - "-" "-" "-" "-" "-" - - 10.10.0.17:8080 10.10.0.16:53366 - -
        # NR → Non-Route. Envoy에서 라우팅까지 가지 못한 단계에서 발생한 에러라는 의미입니다.
        # filter_chain_not_found → 해당 Listener에서 제공된 SNI(Server Name Indication), IP, 포트, ALPN 등의 조건에 맞는 filter_chain이 설정에 없다는 뜻입니다.
        설정 및 라우터 동작 확인
        NR 필터 확인
        • 이는 평문 요청이 거부됐다는 것을 확인함
        • 상호 인증 요구 사항을 STRICT로 지정하는 것은 좋은 기본값
          • 다만 진행 중인 프로젝트에서는 그런 급격한 변화가 실현될 가능성이 없음 (워크로드를 옮기려면 여러 팀 간의 협업이 필요하기 때문)
          • 더 나은 방법은 적용하는 제한을 점진적으로 늘리고, 팀들이 자신의 서비스를 서비스 메시로 옮길 수 있도록 시간을 주는 것
        • PERMISSIVE 상호 인증이 점진적인 제한의 역할을 함
          • 워크로드가 암호화된 요청평문 요청을 모두 받아들일 수 있게 허용
            요청을 인증 및 인가하도록 서비스 프록시를 구성하는 리소스



    • 상호 인증하기 않은 트래픽 허용하기 PERMITTING NON-MUTUALLY AUTHENTICATED TRAFFIC
      • 네임스페이스 범위 정책을 사용 시
        • 메시 범위 정책을 덮어 쓸 수 있음
        • 네임스페이스의 워크로드에 더 잘 맞는 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
          실행 확인
          로그 확인 - webapp
          로그 확인 - istio-proxy 허용 확인

          • 좀 더 보안을 강화하기
            • 미인증 트래픽은 sleep 워크로드에서 webapp으로 향하는 것만 허용하고, catalog 워크로드에는 STRICT 상호 인증을 계속 유지
            • 이렇게 하면 보안이 뚫렸을 때 공격 표면을 더 좁힐 수 있음
    • 워크로드별 PeerAuthentication 정책 적용하기 APPLYING WORKLOAD-SPECIFIC PEERAUTHENTICATION POLICIES
        • webapp 만 목표로 하기 위해 워크로드 selector를 지정해 PeerAuthentication 정책을 업데이트
          • selector에 부합하는 워크로드에만 적용하기 위함
        • 이름을 ‘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
          ...
          
          # webapp 만 목표로 하는 permissive PeerAuthentication 설정 확인
          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)
          kubectl logs -n istioinaction -l app=webapp -c webapp -f
          kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
          kubectl exec deploy/sleep -c sleep -- curl -s http://webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
          
          # 요청 실행 (catalog)
          kubectl logs -n istioinaction -l app=catalog -c catalog -f
          kubectl logs -n istioinaction -l app=catalog -c istio-proxy -f
          kubectl exec deploy/sleep -c sleep -- curl -s http://catalog.istioinaction/api/items -o /dev/null -w "%{http_code}\n"
          2025-05-01T09:32:00.197Z] "- - -" 0 NR filter_chain_not_found - "-" 0 0 0 - "-" "-" "-" "-" "-" - - 10.10.0.18:3000 10.10.0.16:33192 - -
          ...
          LDS 상태 확인
          LDS 로그 확인
          webapp 만 목표로 하는 permissive PeerAuthentication 적용
          로그 확인 - istiod

          로그 확인 - webapp (200 코드 반환)

          로그 확인 - istio-proxy (webapp) - 요청 성공 200


          로그 확인 - istiod

          로그 확인 - catalog (404 반환)

          로그 확인 - istio-proxy (catalog) - 404 반환
          webapp 요청 시 성공 코드 반환(PERMISSIVE), catalog 직접 요청 시 실패, 상호 인증 필요 (STRICT - default)
          • webapp은 성공 응답을 반환 (메시 범위 정책으로 엄격한 기본값을 적용)
          • 그러나 일부 서비스(뒤처진 것들)에는 그 서비스들이 메시로 옮겨질 때까지 상호 인증이 아닌 트래픽도 허용되도록 워크로드별 정책을 사용
            webapp은 HTTP 트래픽 허용. catalog 서비스에는 상호 인증 필요

            istiodPeerAuthentication 리소스 생성을 수신하고, 이 리소스를 엔보이용 설정으로 변환하며, LDS(Listener Discovery Service)를 사용해 서비스 프록시에 적용
            구성된 정책들은 들어오는 요청마다 평가됨
    • 두 가지 추가적인 상호 인증 모드 TWO ADDITIONAL MUTUAL AUTHENTICATION MODES
      • 대부분의 경우 STRICTPERMISSIVE 모드를 사용하지만, 두 가지 모드가 더 있음
        • UNSET : 부모의 PeerAuthentication 정책을 상속
        • DISABLE : 트래픽을 터널링하지 않고 그냥 보냄
      • 상호 인증 트래픽, 평문 트래픽 등 워크로드로 터널링할 트래픽 유형을 지정하거나,
        요청을 프록시로 보내지 않고 애플리케이션으로 바로 포워딩 가능
    • tcpdump로 서비스 간 트래픽 스니핑하기 EAVESDROPPING ON SERVICE-TO-SERVICE TRAFFIC USING TCPDUMP
      • Istio 프록시에는 기본적으로 tcpdump설치되어 있음
        • tcpdump: 네트워크 인터페이스를 통과하는 네트워크 트래픽을 포착하고 분석
      • tcpdump는 보안성으로 인해 privileged permission 권한이 필요 (기본적으로 off)
        • 이 권한을 켜려면 istioctl로 속성 values.global.proxy.privileged=true 로 설정해 이스티오 설치를 업데이트해야 함
          격상시킨 서비스 프록시의 권한은 악의적 공격의 매개체가 될 수 있음
          운영 환경 클러스터에서 이스티오를 설치할 때는 프록시의 권한을 격상시키지 말아야 함
          서비스 하나를 빠르게 디버깅하고 싶을때는 kubectl edit로 deployment의 필드를 수작업으로 바꿀 수 있음
          # 확인
          kubectl get istiooperator -n istio-system installed-state -o yaml
          ...
                proxy:
                  ...
                  privileged: true
          ...
          
          kubectl get pod -n istioinaction -l app=webapp -o json
                                  "image": "docker.io/istio/proxyv2:1.17.8",
                                  "imagePullPolicy": "IfNotPresent",
                                  "name": "istio-proxy",
                                  ...
                                  "securityContext": {
                                      "allowPrivilegeEscalation": true,
                                      "capabilities": {
                                          "drop": [
                                              "ALL"
                                          ]
                                      },
                                      "privileged": true,
                                      "readOnlyRootFilesystem": true,
                                      "runAsGroup": 1337,
                                      "runAsNonRoot": true,
                                      "runAsUser": 1337
                                  },
          ...
          
          # webapp pod 유저/id 확인 후 tcpdump --help 실행 확인
          kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- whoami
          kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- id
          kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- sudo whoami
          kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- sudo tcpdump -h
          proxy - privileged: true
          istio-proxy 유저에서 sudo로 root권한 실행하여 tcpdump help보기
        • 파드 트래픽을 스니핑 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 이상 있는 패킷만 캡처
          
          # 요청 실행
          kubectl exec deploy/sleep -c sleep -- curl -s webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
          ...
          ## (1) sleep -> webapp 호출 HTTP
          14:07:24.926390 IP (tos 0x0, ttl 63, id 63531, offset 0, flags [DF], proto TCP (6), length 146)
              10-10-0-16.sleep.default.svc.cluster.local.32828 > webapp-7685bcb84-hp2kl.http-alt: Flags [P.], cksum 0x14bc (incorrect -> 0xa83b), seq 2741788650:2741788744, ack 3116297176, win 512, options [nop,nop,TS val 490217013 ecr 2804101520], length 94: HTTP, length: 94
          	GET /api/catalog HTTP/1.1
          	Host: webapp.istioinaction
          	User-Agent: curl/8.5.0
          	Accept: */*
          
          ## (2) webapp -> catalog 호출 HTTPS
          14:07:24.931647 IP (tos 0x0, ttl 64, id 18925, offset 0, flags [DF], proto TCP (6), length 1304)
              webapp-7685bcb84-hp2kl.37882 > 10-10-0-19.catalog.istioinaction.svc.cluster.local.3000: Flags [P.], cksum 0x1945 (incorrect -> 0x9667), seq 2146266072:2146267324, ack 260381029, win 871, options [nop,nop,TS val 1103915113 ecr 4058175976], length 1252
          
          ## (3) catalog -> webapp 응답 HTTPS
          14:07:24.944769 IP (tos 0x0, ttl 63, id 7029, offset 0, flags [DF], proto TCP (6), length 1789)
              10-10-0-19.catalog.istioinaction.svc.cluster.local.3000 > webapp-7685bcb84-hp2kl.37882: Flags [P.], cksum 0x1b2a (incorrect -> 0x2b6f), seq 1:1738, ack 1252, win 729, options [nop,nop,TS val 4058610491 ecr 1103915113], length 1737
          
          ## (4) webapp -> sleep 응답 HTTP
          14:07:24.946168 IP (tos 0x0, ttl 64, id 13699, offset 0, flags [DF], proto TCP (6), length 663)
              webapp-7685bcb84-hp2kl.http-alt > 10-10-0-16.sleep.default.svc.cluster.local.32828: Flags [P.], cksum 0x16c1 (incorrect -> 0x37d1), seq 1:612, ack 94, win 512, options [nop,nop,TS val 2804101540 ecr 490217013], length 611: HTTP, length: 611
          	HTTP/1.1 200 OK
          	content-length: 357
          	content-type: application/json; charset=utf-8
          	date: Thu, 01 May 2025 14:07:24 GMT
          	x-envoy-upstream-service-time: 18
          	server: istio-envoy
          	x-envoy-decorator-operation: webapp.istioinaction.svc.cluster.local:80/*
          
          	[{"id":1,"color":"amber","department":"Eyewear","name":"Elinor Glasses","price":"282.00"},{"id":2,"color":"cyan","department":"Clothing","name":"Atlas Shirt","price":"127.00"},{"id":3,"color":"teal","department":"Clothing","name":"Small Metal Shoes","price":"232.00"},{"id":4,"color":"red","department":"Watches","name":"Red Dragon Watch","price":"232.00"}] [|http]
          ...
          tcpdump 뜨기

          1. sleep -> webapp 호출 HTTP
          2. webapp -> catalog 호출 HTTPS
          3. catalog -> webapp 응답 HTTPS
          4. webapp -> sleep 응답 HTTP

          # 서비스, 엔드포인트 확인
          kubectl get svc,ep -n istioinaction
          NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
          service/catalog   ClusterIP   10.200.1.46    <none>        80/TCP    7h4m
          service/webapp    ClusterIP   10.200.1.201   <none>        80/TCP    7h4m
          
          NAME                ENDPOINTS         AGE
          endpoints/catalog   10.10.0.19:3000   7h4m
          endpoints/webapp    10.10.0.20:8080   7h4m
          
          # webapp 덤프를 endpoint (catalog,webapp 3000, 8080 포트)에서 실행
          kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy \
            -- sudo tcpdump -l --immediate-mode -vv -s 0 'tcp port 3000 or tcp port 8080'
          
          # 요청 실행
          kubectl exec deploy/sleep -c sleep -- curl -s webapp.istioinaction/api/catalog -o /dev/null -w "%{http_code}\n"
          ...

          Pod traffic sniffing
    • 워크로드 ID가 워크로드 서비스 어카운트에 연결돼 있는지 확인하기 VERIFYING THAT WORKLOAD IDENTITIES ARE TIED TO THE WORKLOAD SERVICE ACCOUNT
      • 발급된 인증서가 유효한 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 및 인증서 체인 출력 후 사람이 읽을 수 있는 형식으로 해석
        kubectl -n istioinaction exec deploy/webapp -c istio-proxy \
          -- openssl s_client -showcerts \
          -connect catalog.istioinaction.svc.cluster.local:80 \
          -CAfile /var/run/secrets/istio/root-cert.pem | \
          openssl x509 -in /dev/stdin -text -noout
        ...
                Validity 
                    Not Before: May  1 09:55:10 2025 GMT # 유효기간 1일 2분
                    Not After : May  2 09:57:10 2025 GMT
                ...
                X509v3 extensions:
                    X509v3 Extended Key Usage:
                        TLS Web Server Authentication, TLS Web Client Authentication # 사용처 : 웹서버, 웹클라이언트
                    ...
                    X509v3 Subject Alternative Name: critical
                        URI:spiffe://cluster.local/ns/istioinaction/sa/catalog # SPIFFE ID 확인
        
        # catalog 파드의 서비스 어카운트 확인
        kubectl describe pod -n istioinaction -l app=catalog | grep 'Service Account'
        Service Account:  catalog

        catalog 의 X.509 인증서 내용 확인 (CA: TRUE)
        openssl s_client → TLS 서버에 연결해 handshake와 인증서 체인을 보여줌 Envoy proxy에서 catalog 서비스로 연결하여 TLS handshake 및 인증서 체인 출력 후 사람이 읽을 수 있는 형식으로 해석

        사용자가 istio ca에서 발급받은 인증서 (CA:FALSE)
        serviceAccount가 catalog이며 이것이 인증서 URI에 반영됨 (마지막 path)

        URI:spiffe://cluster.local/ns/istioinaction/sa/catalog

      • 루트 인증서 서명 확인
        • openssl verify 로 인증 기관 CA 루트 인증서에 대해 서명을 확인함으로써 X.509 SVID의 내용물이 유효한지 살펴보기
        • 루트 인증서는 istio-proxy 컨테이너에서 /var/run/secrets/istio/root-cert.pem 경로에 마운트돼 있음
          # webapp.istio-proxy 쉘 접속
          kubectl -n istioinaction exec -it deploy/webapp -c istio-proxy -- /bin/bash
          -----------------------------------------------
          # 인증서 검증
          openssl verify -CAfile /var/run/secrets/istio/root-cert.pem \
            <(openssl s_client -connect \
            catalog.istioinaction.svc.cluster.local:80 -showcerts 2>/dev/null)
          /dev/fd/63: OK
          # 검증에 성공 시 OK 메시지 출력: 이스티오 CA가 인증서에 서명했으며, 내부 데이터가 믿을 수 있다는 것임을 알려줌.
          
          exit
          -----------------------------------------------

          • 참가자 간 peer-to-peer 인증을 용의하게 하는 모든 구성 요소를 앞의 과정을 통해 검증함
            •  발급된 ID는 검증할 수 있는 것이고 트래픽은 안전하다는 것을 확신할 수 있음
          • 검증할 수 있는 ID가 접근 제어의 선행 조건
            • 이는 워크로드의 ID를 알고 있으므로 수행할 수 있는 작업을 정의할 수 있음을 의미
  1. PKI를 사용한 인증 Authentication using PKI (public key infrastructure)
    • 들어가며
      • World Wide Web에서 통신 당사자는 PKI Public Key Infrastructure (공개 키 인프라) 규격을 따라 발급한 디지털 서명 인증서를 사용해 인증
      • PKI는 절차를 정의하는 프레임워크 - https://www.securew2.com/blog/public-key-infrastructure-explained
        • 서버(웹 앱 등): 자신의 정체를 증명할 수 있는 디지털 인증서를 제공
        • 클라이언트: 디지털 인증서의 유효성을 검증할 수 있는 수단을 제공 
      • PKI에서 제공하는 인증서
        • 공개 키
        • 개인 키 
      • PKI는 클라이언트에게 인증서를 인증 수단으로 제시 (공개 키는 이 인증서 안에 포함)
        • 클라이언트는 공개된 네트워크에서 서버로 데이터를 전송하기 전에 공개 키를 사용해 데이터를 암호화
      • 개인 키를 가진 서버만이 클라이언트 데이터를 복호화 가능
      • 위 과정으로 데이터는 전송 중에 안전하게 보호됨
        공개 키 인증서의 표준 형식을 X.509 인증서라고 한다. 이 책에서는 X.509 인증서라는 용어와 디지털 인증서라는 용어를 같은 뜻으로 사용한다.
      • 국제 인터넷 표준화 기구 IETF는 전송 계층 보안 TLS Transport Layer Security 프로토콜을 정의
        • TLS 프로토콜은 PKI를 사용하기는 하지만 PKI만 사용해야 하는 것은 아님
        • X.509 인증서를 공급해 트래픽 인증 및 암호화를 용이하게 함

    • 1.1 TLS 및 최종 사용자 인증을 통한 트래픽 암호화 Traffic encryption via TLS and end-user authentication
      • TLS 프로토콜은 TLS 핸드셰이크 절차에서 X.509 인증서를 기본 메커니즘으로 사용
        • 서버의 유효성을 인증
        • 트래픽 대칭 키 암호화용 키를 안전하게 교환
          TLS 핸드셰이크 단계
          1. 클라이언트가 자신이 지원하는 TLS 버전과 암호화 수단을 포함한 ClientHello 로 핸드셰이크를 시작
          2. 서버는 ServerHello 와 자신의 X.509 인증서로 응답
            • 인증서에는 서버의 ID 정보공개 키 포함
          3. 클라이언트는 서버의 인증서 데이터가 변조되지 않았음을 확인 후 신뢰 체인 검증
          4. 검증에 성공하면, 클라이언트서버비밀 키를 전송
            • 비밀 키: 임의로 생성한 문자열서버공개 키암호화한 것
          5. 서버는 자신의 개인 키로 비밀 키를 복호화하고, 복호화된 비밀 키로 ‘finished’ 메시지를 암호화클라이언트로 전송
          6. 클라이언트비밀 키로 암호화한 ‘finished’ 메시지를 서버에 보내면 TLS 핸드셰이크 완료
        • TLS 핸드셰이크의 결실은 클라이언트가 서버를 인증했고 대칭 키를 안전하게 교환했다는 것
          • 대칭 키: 해당 커넥션에서 클라이언트와 서버를 오가는 트래픽을 암호화하는데 사용
        • TLS 핸드셰이크 방식은 비대칭 암호화보다 성능이 더 좋음
        • 최종 사용자에게 이런 절차는 브라우저가 투명하게 수행하는 것
          • 주소 표시줄에 녹색 자물쇠로 표시돼 수신자가 인증됐고 트래픽이 암호화돼 수신자만 복호화할 수 있다는 것을 확인 가능
        • 서버에서 최종 사용자를 인증하는 것은 구현하기 나름이며 여러 가지 방법이 있음
          • 인증의 핵심은 비밀번호를 알고 있는 사용자가 세션 쿠키JWT(JSON Web Token)를 받는 것
          • JWT수명이 짧고 사용자의 후속 요청을 서버에 인증하기 위한 정보를 포함하는 것이 이상적
        • Istio는 JWT를 사용하는 최종 사용자 인증을 지원
  2. SPIFFE: 모든 이를 위한 안전한 운영 환경 ID 프레임워크 Secure Production Identity Framework for Everyone (실습)
    • 들어가며
      • SPIFFE는 고도로 동적이며 이질적인 환경에서 워크로드에 ID를 제공하기 위한 오픈소스 표준 집합
      • ID를 발급하고 부트스트랩하기 위해 SPIFFE는 다음 사양(specifications)을 정의
        • SPIFFE ID : 신뢰 도메인 내에서 서비스를 고유하게 구별
        • Workload Endpoint : 워크로드의 ID를 부트스트랩
        • Workload API : SPIFFE ID가 포함된 인증서를 서명하고 발급
        • SVID SPIFFE Verifiable Identity Document : 워크로드 API가 발급한 인증서로 표현
      • SPIFFE 사양은 SPIFFE ID 형식으로 워크로드에 ID를 발급하고 이를 SVID에 인코딩하는 절차를 정의
        • 컨트롤 플레인 구성 요소(워크로드 API)와 데이터 플레인 구성 요소(워크로드 엔드포인트)가 워크로드의 ID를 검증하고 할당하고 형식의 유효성을 검사하기 위해 협동하는 방법 또한 정의
      • Istio가 이런 사양을 구현
    • 2.1 SPIFFE ID: Workload identity 워크로드 ID
      • SPIFFE ID는 RFC 3986 호환 URI로, spiffe://trust-domain/path 형식을 따름
        • trust-domain 은 개인이나 조직 같은 ID 발급자를 나타냄
        • path는 trust-domain 내에서 워크로드를 고유하게 식별
      • 경로 path로 워크로드를 식별하는 방법의 세부 사항에는 제약이 없으며 SPIFFE 사양 구현자가 결정 가능
      • Istio가 쿠버네티스 서비스 어카운트를 사용해 워크로드를 식별하는 경로를 정의하는 방법을 실습에서 확인
    • 2.2 Workload API 워크로드 API
      • 워크로드 API는 SPIFFE 사양에서 컨트롤 플레인 구성 요소를 나타내며, 워크로드가 자신의 ID를 정의하는 SVID 형식 디지털 인증서를 가져갈 수 있도록 엔드포인트를 노출
      • 워크로드 API의 두 가지 주요 기능
        • 워크로드가 제출한 인증서 서명 요청 CSR에 인증 기관 CA 개인 키로 서명함으로써 워크로드에 인증서 발급
        • 워크로드 엔드포인트에서 해당 기능을 사용할 수 있도록 API 노출
      • 사양 specification sets 은 워크로드가 자신의 ID를 정의하는 비밀이나 기타 정보를 보유해서는 안 된다제한(규칙)을 둠 (그렇지 않으면, 해당 비밀에 접근할 수 있는 악의적인 사용자가 시스템을 쉽게 악용할 수 있음)
        • 제한으로 인해 워크로드에는 인증 수단이 없어 워크로드 API로 보안 통신을 시작할 수 없음
      • 보안 통신이 불가한 사항을 해결하고자 SPIFFE는 워크로드 엔드포인트 사양을 정의
        • 데이터 플레인 구성 요소를 나타내고, 워크로드의 ID를 부트스트랩하는 데 필요한 모든 작업을 수행
        • 작업 예시
          • 워크로드 API와 보안 통신을 시작
          • 도청 또는 중간자 공격에 취약하지 않게 SVID를 가져오
          • ...기타 등등
    • 2.3 Workload endpoints 워크로드 엔드포인트
      • 워크로드 엔드포인트
        • SPIFFE 사양의 데이터 플레인 구성 요소를 나타냄
        • 모든 워크로드와 함께 배포됨

        • 기능
          • 워크로드 증명 attestation
            • 커널 검사 kernel introspection 또는 orchestrator interrogation (쿼리, 질문) 같은 방법을 사용해 워크로드의 ID를 확인
          • 워크로드 API 노출 exposure
            • 워크로드 API와 보안 통신을 시작하고 유지한다. 이 보안 통신은 SVID를 가져오고 로테이션하는 데 사용

        • 워크로드에 ID를 발급하는 단계의 개요
          워크로드에 ID를 발급하기
          1. 워크로드 엔드포인트는 워크로드의 무결성을 확인하고(즉, 워크로드 증명을 수행하고) SPIFFIE ID가 인코딩된 CSR을 생성
          2. 워크로드 엔드포인트는 서명을 위해 워크로드 API에 CSR을 제출
          3. 워크로드 API는 CSR을 서명하고 디지털 서명된 인증서로 응답
            • 이 인증서의 SAN의 URI 확장에는 SPIFFE ID 존재
            • 이 인증서는 워크로드 ID를 나타내는 SVID
    • 2.4 SPIFFE Verifiable Identity Documents 검증할 수 있는 ID 문서
      • SVID (SPIFFE 검증할 수 있는 ID 문서)
        • 워크로드의 정체를 나타내는 검증할 수 있는 문서
        • 검증할 수 있다는 것이 가장 중요한 속성 (그렇지 않으면 수신자가 워크로드의 정체를 신뢰할 수 없기 때문)
      • 사양은 SVID 표현 기준을 충족하는 문서로 두 가지 유형인 X.509 인증서JWT를 정의
      • X.509 인증서 JWT은 다음 요소로 구성됨
        • SPIFFE ID, 워크로드 ID를 나타냄
        • 유효한 서명, SPIFFE ID가 변조되지 않았음을 확인
        • (선택 사항) 워크로드 간에 보안 통신 채널을 구축하기 위한 공개 키
      • Istio는 SVIDX.509 인증서로 구현
        • 구현 방법: SAN Subject Alternative Name 확장에 SPIFFE ID를 URI로 인코딩
        • X.509 인증서 사용에 따른 이점: 워크로드가 서로 간의 트래픽을 상호 인증하고 암호화할 수 있음
          자신의 SVID를 가져오고 보안 통신을 시작하는 워크로드
      • Istio가 SPIFFE 사양을 구현함으로써, 모든 워크로드가 각자의 ID를 공급받고 그 ID를 증거로 인증서를 받는다는 것이 자동으로 보장됨
        • 이런 인증서는 상호 인증과 모든 서비스 간 통신을 암호화하는 데 사용
        • 그러므로 이 기능을 자동 상호 TLS라고 지칭
    • 2.5 How Istio implements SPIFFE Istio가 SPIFFE를 구현하는 방법
      • Istio를 사용하면 다음 두 구성 요소가 협업해 워크로드에 ID를 제공
        • ID를 부트스트랩하는 워크로드 엔드포인트 (데이터플레인, Istio 프록시 pilot agent)
        • 인증서를 발급하는 워크로드 API (컨트롤플레인, istiod 의 Istio CA)
      • Istio에서 워크로드 엔드포인트 사양은 워크로드와 함께 배포되는 Istio 프록시가 구현
      • Istio 프록시는 ID를 부트스트랩하고 Istio CA에서 인증서를 가져오는데, Istio CA는 istiod의 구성 요소로 워크로드 API 사양을 구현
      • Istio가 SPIFFE 구성 요소를 구현하는 방법
        Istio 구성 요소를 SPIFFE 사양에 맵핑
        • 워크로드 엔드포인트는 ID 부트스트랩을 수행하는 Istio 파일럿 에이전트로 구현
        • 워크로드 API는 인증서를 발급하는 Istio CA로 구현
        • Istio에서 ID를 발급하는 워크로드는 서비스 프록시
    • 2.6 Step-by-step bootstrapping of workload identity 워크로드 ID의 단계별 부트스트랩
      • 기본적으로 쿠버네티스에서 초기화된 모든 Pod에는 /var/run/secrets/kubernetes.io/serviceaccount/ 경로에 시크릿이 마운트됨
      • 이 시크릿에는 쿠버네티스 API 서버와 안전하게 통신하는 데 필요한 모든 데이터 포함
        • ca.crt 는 쿠버네티스 API 서버가 발급한 인증서의 유효성을 검증
        • 네임스페이스는 Pod가 위치한 곳을 나타냄
        • 서비스 어카운트 토큰에는 Pod를 나타내는 서비스 어카운트에 대한 (토큰)클레임들이 포함됨
      • ID 부트스트랩 과정에서 가장 중요한 요소는 쿠버네티스 API가 발급한 토큰
      • 토큰의 페이로드는 수정할 수 없음 - 수정하면 서명 유효성 검사를 통과하지 못함
      • 페이로드에는 애플리케이션 식별 데이터가 포함됨
        # TOKEN 확인 및 환경변수 추가 
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets/kubernetes.io/serviceaccount/
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
        TOKEN=$(kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
        
        # Linux의 base64 -d는 표준 base64만 지원하므로, base64url → base64 변환과 패딩 추가 필요
        # 일반 base64와 달리, base64url은
        ## + 대신 -
        ## / 대신 _
        ## 패딩(=)이 생략되어 있음
        header=$(echo "$TOKEN" | cut -d '.' -f1 | tr '_-' '/+' | awk '{l=length($0)%4; if(l==2)print $0"=="; else if(l==3)print $0"="; else print $0;}')
        payload=$(echo "$TOKEN" | cut -d '.' -f2 | tr '_-' '/+' | awk '{l=length($0)%4; if(l==2)print $0"=="; else if(l==3)print $0"="; else print $0;}')
        signature=$(echo "$TOKEN" | cut -d '.' -f3 | tr '_-' '/+' | awk '{l=length($0)%4; if(l==2)print $0"=="; else if(l==3)print $0"="; else print $0;}') 
        
        
        # 헤더 디코딩
        echo $header | base64 --decode | jq
        {
          "alg": "RS256",
          "kid": "nKgUYnbjH9BmgEXYbu56GFoBxwDF_jF9Q6obIWvinAM"
        }
        
        # 페이로드 디코딩
        echo $payload | base64 --decode | jq
        {
          "aud": [
            "https://kubernetes.default.svc.cluster.local"
          ],
          "exp": 1777689454,
          "iat": 1746153454,
          "iss": "https://kubernetes.default.svc.cluster.local",
          "kubernetes.io": {
            "namespace": "istioinaction",
            "pod": {
              "name": "webapp-7685bcb84-hp2kl",
              "uid": "98444761-1f47-45ad-b739-da1b7b22013a"
            },
            "serviceaccount": {
              "name": "webapp",
              "uid": "5a27b23e-9ed6-46f7-bde0-a4e4684949c2"
            },
            "warnafter": 1746157061
          },
          "nbf": 1746153454,
          "sub": "system:serviceaccount:istioinaction:webapp"
        }
        
        # (옵션) brew install jwt-cli  # Linux 툴 추천 부탁드립니다.
        jwt decode $TOKEN
        Token header
        ------------
        {
          "alg": "RS256",
          "kid": "nKgUYnbjH9BmgEXYbu56GFoBxwDF_jF9Q6obIWvinAM"
        }
        
        Token claims
        ------------
        {
          "aud": [ # 이 토큰의 대상(Audience) : 토큰이 어떤 API나 서비스에서 사용될 수 있는지 정의 -> k8s api가 aud 가 일치하는지 검사하여 올바른 토큰인지 판단.
            "https://kubernetes.default.svc.cluster.local"
          ],
          "exp": 1777689454, # 토큰 만료 시간 Expiration Time (Unix timestamp, 초 단위) , date -r 1777689454 => (1년) Sat May  2 11:37:34 KST 2026
          "iat": 1746153454, # 토큰 발급 시간 Issued At (Unix timestamp), date -r 1746153454 => Fri May  2 11:37:34 KST 2025
          "iss": "https://kubernetes.default.svc.cluster.local", # Issuer, 토큰을 발급한 주체, k8s api가 발급
          "kubernetes.io": {
            "namespace": "istioinaction",
            "pod": {
              "name": "webapp-7685bcb84-hp2kl",
              "uid": "98444761-1f47-45ad-b739-da1b7b22013a" # 파드 고유 식별자
            },
            "serviceaccount": {
              "name": "webapp",
              "uid": "5a27b23e-9ed6-46f7-bde0-a4e4684949c2" # 서비스 어카운트 고유 식별자
            },
            "warnafter": 1746157061 # 이 시간 이후에는 새로운 토큰을 요청하라는 Kubernetes의 신호 (토큰 자동 갱신용) date -r 1746157061 (1시간) => Fri May  2 12:37:41 KST 2025
          },
          "nbf": 1746153454, # Not Before, 이 시간 이전에는 토큰이 유효하지 않음. 보통 iat와 동일하게 설정됩니다.
          "sub": "system:serviceaccount:istioinaction:webapp" # 토큰의 주체(Subject)
        }
        
        # sa 에 토큰 유효 시간 3600초 = 1시간 + 7초
        kubectl get pod -n istioinaction -l app=webapp -o yaml
        ...
            - name: kube-api-access-nt4qb
              projected:
                defaultMode: 420
                sources:
                - serviceAccountToken:
                    expirationSeconds: 3607
                    path: token
        ...
         

        TOKEN 값 변수 부여

        헤더, 페이로드 디코딩

        • 파일럿 에이전트는 토큰을 디코딩하고 이 페이로드 데이터를 사용해 SPIFFE ID(예 spiffe://cluster.local/ns/istioinaction/sa/default)를 생성
          • SPIFFE ID는 CSR안에서 URI 유형의 SAN 확장으로 사용
        • Istio CA로 보낸 요청토큰과 CSR이 모두 전송되며, CSR에 대한 응답으로 발급된 인증서 반환
        • CSR에 서명하기 전에 Istio CATokenReview API를 사용해 토큰이 쿠버네티스 API가 발급한 것이 맞는지 확인
        • 이는 SPIFFE 사양에서 약간 벗어난 것인데, SPIFFE 사양에서는 워크로드 엔드포인트(이스티오 에이전트)가 워크로드 증명을 수행해야 하기 때문
        • 검증을 통과하면 CSR에 서명하고, 결과 인증서가 파일럿 에이전트에 반환
          # tokenreviews 리소스 확인
          kubectl api-resources | grep -i token
          tokenreviews                                   authentication.k8s.io/v1               false        TokenReview
          
          kubectl explain tokenreviews.authentication.k8s.io
          ...
          DESCRIPTION:
               TokenReview attempts to authenticate a token to a known user. Note:
               TokenReview requests may be cached by the webhook token authenticator
               plugin in the kube-apiserver.
          ...
          
          # Kubernetes API 서버에 TokenReview API 를 호출하여 토큰이 여전히 유효한지 확인 : C(Create)
          ## 이때 사용되는 Kubernetes API 가 POST /apis/authentication.k8s.io/v1/tokenreviews
          ## 즉, istiod가 이 API를 호출하려면 tokenreviews.authentication.k8s.io 리소스에 create 권한이 필요. C(Create)
          kubectl rolesum istiod -n istio-system
          ...
          • [CRB] */istiod-clusterrole-istio-system ⟶  [CR] */istiod-clusterrole-istio-system
            Resource                                                                                                                                             Name               Exclude    Verbs    G L W C U P D DC  
            ...
            signers.certificates.k8s.io                                                                                                             [kubernetes.io/legacy-unknown]    [-]    [approve]  ✖ ✖ ✖ ✖ ✖ ✖ ✖ ✖   
            subjectaccessreviews.authorization.k8s.io                                                                                                            [*]                  [-]       [-]     ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖   
            tokenreviews.authentication.k8s.io                                                                                                                   [*]                  [-]       [-]     ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖   
            validatingwebhookconfigurations.admissionregistration.k8s.io                                                                                         [*]                  [-]       [-]     ✔ ✔ ✔ ✖ ✔ ✖ ✖ ✖   
          ...
          
          ## 추가 실습
          # istiod 컨테이너 내부에서 자신의 token으로 k8s TokenReview API 호출하는 실습
          
          # istiod 컨테이너 이름 확인
          kubectl -n istio-system get pod istiod-8d74787f-l68cp -o jsonpath='{.spec.containers[*].name}'
          discovery
          
          # istiod 컨테이너 내부 진입
          kubectl -n istio-system exec -it deploy/istiod -c discovery -- /bin/bash
          
          # 토큰, CA, API 서버 엔드포인트 변수 설정
          # 서비스 어카운트 토큰 경로
          TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
          # k8s root CA 경로
          CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
          # API 서버 엔드포인트
          APISERVER=https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}
          
          # TokenReview API 호출 (자신의 토큰으로)
          curl -sSk --cacert $CACERT \
            -H "Authorization: Bearer $TOKEN" \
            -H "Content-Type: application/json" \
            -d "{\"apiVersion\": \"authentication.k8s.io/v1\", \"kind\": \"TokenReview\", \"spec\": {\"token\": \"$TOKEN\"}}" \
            $APISERVER/apis/authentication.k8s.io/v1/tokenreviews
          
          
          # istio-proxy 배포 실행 로그 및 위 과정 실행 절차 확인
          # istio-proxy 로그 확인
          kubectl -n istioinaction logs deploy/webapp -c istio-proxy


          TokenReview 리소스 확인

          istiod가 이 API를 호출하려면 tokenreviews.authentication.k8s.io 리소스에 create 권한 있어야 함

          istiod 컨테이너 이름 확인 및 내부 진입, 변수 설정
          자신의 token으로 TokenReview API 호출
          로그 확인
        • 파일럿 에이전트는 SDS Secrets Discovery Service 를 통해 인증서와 키를 엔보이 프록시로 전달
        # 유닉스 도메인 소켓 listen 정보 확인
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ss -xpl
        Netid           State            Recv-Q           Send-Q                                                    Local Address:Port                      Peer Address:Port           Process                                        
        u_str           LISTEN           0                4096                                                etc/istio/proxy/XDS 13207                                * 0               users:(("pilot-agent",pid=1,fd=11))           
        u_str           LISTEN           0                4096                       ./var/run/secrets/workload-spiffe-uds/socket 13206                                * 0               users:(("pilot-agent",pid=1,fd=10))  
        
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ss -xp
        Netid State Recv-Q Send-Q                                Local Address:Port    Peer Address:Port   Process                             
        u_str ESTAB 0      0      ./var/run/secrets/workload-spiffe-uds/socket 21902              * 23737   users:(("pilot-agent",pid=1,fd=16))
        u_str ESTAB 0      0                               etc/istio/proxy/XDS 1079087            * 1080955 users:(("pilot-agent",pid=1,fd=8)) 
        ...
        
        # 유닉스 도메인 소켓 정보 확인
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- lsof -U
        COMMAND   PID        USER   FD   TYPE             DEVICE SIZE/OFF    NODE NAME
        pilot-age   1 istio-proxy    8u  unix 0x00000000bda7185a      0t0 1079087 etc/istio/proxy/XDS type=STREAM # 소켓 경로 및 스트림 타입
        pilot-age   1 istio-proxy   10u  unix 0x0000000009112f4b      0t0   13206 ./var/run/secrets/workload-spiffe-uds/socket type=STREAM # SPIFFE UDS (SPIFFE SVID 인증용)
        # TYPE 파일 유형 (unix → Unix Domain Socket)
        ## 8u → 8번 디스크립터, u = 읽기/쓰기
        ## 10u → 10번 디스크립터, u = 읽기/쓰기
        
        # 유닉스 도메인 소켓 파일 정보 확인
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets/workload-spiffe-uds/socket
        srw-rw-rw- 1 istio-proxy istio-proxy 0 May  1 23:23 /var/run/secrets/workload-spiffe-uds/socket
        
        # istio 인증서 확인 : 
        docker exec -it myk8s-control-plane istioctl proxy-config secret deploy/webapp.istioinaction
        RESOURCE NAME     TYPE           STATUS     VALID CERT     SERIAL NUMBER                               NOT AFTER                NOT BEFORE
        default           Cert Chain     ACTIVE     true           45287494908809645664587660443172732423      2025-05-03T16:13:14Z     2025-05-02T16:11:14Z
        ROOTCA            CA             ACTIVE     true           338398148201570714444101720095268162852     2035-04-29T07:46:14Z     2025-05-01T07:46:14Z
        
        docker exec -it myk8s-control-plane istioctl proxy-config secret deploy/webapp.istioinaction -o json
        ...
        
        echo "." | base64 -d  | openssl x509 -in /dev/stdin -text -noout
        
        
        # istio ca 관련
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets/istio
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- openssl x509 -in /var/run/secrets/istio/root-cert.pem -text -noout
        
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets/tokens
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/tokens/istio-token
        TOKEN=$(kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/tokens/istio-token)
        
        # Linux의 base64 -d는 표준 base64만 지원하므로, base64url → base64 변환과 패딩 추가 필요
        # 일반 base64와 달리, base64url은
        ## + 대신 -
        ## / 대신 _
        ## 패딩(=)이 생략되어 있음
        header=$(echo "$TOKEN" | cut -d '.' -f1 | tr '_-' '/+' | awk '{l=length($0)%4; if(l==2)print $0"=="; else if(l==3)print $0"="; else print $0;}')
        payload=$(echo "$TOKEN" | cut -d '.' -f2 | tr '_-' '/+' | awk '{l=length($0)%4; if(l==2)print $0"=="; else if(l==3)print $0"="; else print $0;}')
        signature=$(echo "$TOKEN" | cut -d '.' -f3 | tr '_-' '/+' | awk '{l=length($0)%4; if(l==2)print $0"=="; else if(l==3)print $0"="; else print $0;}')
        
        # 헤더 디코딩 
        echo $header | base64 --decode | jq
        # 페이로드 디코딩
        echo $payload | base64 --decode | jq
        
        
        # (옵션) brew install jwt-cli 
        jwt decode $TOKEN
        
        
        # (참고) k8s ca 관련
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- ls -l /var/run/secrets/kubernetes.io/serviceaccount
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- openssl x509 -in /var/run/secrets/kubernetes.io/serviceaccount/ca.crt -text -noout
        kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
        TOKEN=$(kubectl exec -it -n istioinaction deploy/webapp -c istio-proxy -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
        
        header=$(echo "$TOKEN" | cut -d '.' -f1 | tr '_-' '/+' | awk '{l=length($0)%4; if(l==2)print $0"=="; else if(l==3)print $0"="; else print $0;}')
        payload=$(echo "$TOKEN" | cut -d '.' -f2 | tr '_-' '/+' | awk '{l=length($0)%4; if(l==2)print $0"=="; else if(l==3)print $0"="; else print $0;}')
        signature=$(echo "$TOKEN" | cut -d '.' -f3 | tr '_-' '/+' | awk '{l=length($0)%4; if(l==2)print $0"=="; else if(l==3)print $0"="; else print $0;}')
        
        # 헤더 디코딩 
        echo $header | base64 --decode | jq
        # 페이로드 디코딩
        echo $payload | base64 --decode | jq
        
        # (옵션) brew install jwt-cli 
        jwt decode $TOKEN
        
        
        # (참고)
        kubectl port-forward deploy/webapp -n istioinaction 15000:15000
        open http://localhost:15000
        curl http://localhost:15000/certs

        유닉스 도메인 소켓 정보 확인 및 istio 인증서 확인

        istio ca 정보 확인

        istio ca TOKEN 디코딩 - 헤더,페이로드

        k8s ca 정보 확인

        k8s ca 정보 확인

        k8s ca TOKEN 디코딩 - 헤더,페이로드

        webapp의 인증서 정보 확인


        • 이제 프록시는 클라이언트에게 자신의 정체를 증명할 수 있으며 상호 인증 커넥션을 시작할 수 있음
        • Kubernetes에서 Istio로 SVID 발급 과정
          k8s에서 Istio로 SVID 발급

          1. Istio 프록시 컨테이너에 서비스 어카운트 토큰 할당
          2. 토큰과 CSR이 istiod로 전송
          3. istiod는 쿠버네티스 TokenReview API로 토큰 유효성 검사
          4. 성공 시, 인증서에 서명하고 응답으로 제공
          5. 파일럿 에이전트는 Envoy SDS를 통해 Envoy가 ID를 포함한 인증서를 사용하도록 설정
        • Istio가 워크로드 ID를 프로비저닝 하기 위한 SPIFFE 사양 구현 전체 과정을 알아봄
          • Istio 프록시 사이드카가 주입되는 모든 워크로드에서 자동으로 수행



  3. 요청 ID 이해하기 Understanding request identity
    • 들어가며 : 필터 메타데이터 - Principal, Namespace, Request principal, Request authentication claims
      필터 메타데이터에서 검증된 데이터 수집



      요청을 인증 및 인가하도록 서비스 프록시를 구성하는 리소스

      • 요청 ID는 요청의 필터 메타데이터에 저장된 값으로 표현
      • 이 필터 메타데이터에는 JWT 피어 인증서에서 추출한 사실 또는 클레임이 포함되어 있어 신뢰 가능
      • 앞서 JWT의 정보를 검증하기 위해 필요한 RequestAuthentication 리소스를 살펴봤다면,
        클라이언트 워크로드 정보(워크로드의 네임스페이스 등)을 인증하려면 워크로드들이 상호 인증해야 함
      • PeerAuthentication 리소스는 워크로드가 상호 인증만 사용하도록 강제 가능 (only mutual authentiation)

      • JWT를 검증하거나 워크로드가 상호 인증마치면, 포함된 정보필터 메타데이터로 저장
      • 필터 메타데이터에 저장되는 정보 중 일부
        • Principal 주체: PeerAuthentication 에서 정의한 워크로드 ID
        • Namespace 네임스페이스: PeerAuthentication에서 정의한 워크로드 네임스페이스
        • Request principal 요청 주체 : RequestAuthentication에서 정의한 최종 사용자 요청 주체
        • Request authentication claims 요청 인증 클레임 : 최종 사용자 토큰에서 추출한 최종 사용자 클레임
      • 서비스 프록시가 메타데이터를 수집해 표준 출력에 기록하도록 설정 가능 (관찰)

    • 3.1 RequestAuthentication 리소스로 수집한 메타데이터 Metadata collected by the RequestAuthentication resource (실습)
      • 기본적으로 엔보이 rbac 로거는 메타데이터를 로그에 출력하지 않음
        • 출력하려면 로깅 수준의 debug로 설정 필요
          docker exec -it myk8s-control-plane istioctl proxy-config log deploy/istio-ingressgateway -n istio-system --level rbac:debug
          ...
      • 사용 서비스 필요:- 실습 환경 초기화 후 워크로드로 트래픽 라우팅하도록 Ingress Gateway 설정
        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
      • 필터 메타데이터를 사용하는 RequestAuthentication 리소스와 AuthorizationPolicy 생성
        kubectl apply -f ch9/enduser/jwt-token-request-authn.yaml
        kubectl apply -f ch9/enduser/allow-all-with-jwt-to-webapp.yaml # :30000 포트 추가 필요, 아래 실습 설정 참고.
        kubectl get requestauthentication,authorizationpolicy -A
      • admin 토큰 사용 요청해보기 (Ingress Gateway 로깅)
        # 로깅
        kubectl logs -n istio-system -l app=istio-ingressgateway -f
        
        
        # admin 토큰을 사용하는 요청 : 필터 메타데이터 확인
        ADMIN_TOKEN=$(< ch9/enduser/admin.jwt)
        
        curl -H "Authorization: Bearer $ADMIN_TOKEN" \
             -sSl -o /dev/null -w "%{http_code}\n" webapp.istioinaction.io:30000/api/catalog
        ...
        dynamicMetadata: filter_metadata {
          key: "envoy.filters.http.jwt_authn"
          value {
            fields {
              key: "auth@istioinaction.io"
              value {
                struct_value {
                  fields {
                    key: "exp"
                    value {
                      number_value: 4745145071
                    }
                  }
                  fields {
                    key: "group"
                    value {
                      string_value: "admin"
                    }
                  }
                  fields {
                    key: "iat"
                    value {
                      number_value: 1591545071
                    }
                  }
                  fields {
                    key: "iss"
                    value {
                      string_value: "auth@istioinaction.io"
                    }
                  }
                  fields {
                    key: "sub"
                    value {
                      string_value: "218d3fb9-4628-4d20-943c-124281c80e7b"
                    }
                  }
                }
              }
            }
          }
        }
        filter_metadata {
          key: "istio_authn"
          value {
            fields {
              key: "request.auth.claims"
              value {
                struct_value {
                  fields {
                    key: "group"
                    value {
                      list_value {
                        values {
                          string_value: "admin"
                        }
                      }
                    }
                  }
                  fields {
                    key: "iss"
                    value {
                      list_value {
                        values {
                          string_value: "auth@istioinaction.io"
                        }
                      }
                    }
                  }
                  fields {
                    key: "sub"
                    value {
                      list_value {
                        values {
                          string_value: "218d3fb9-4628-4d20-943c-124281c80e7b"
                        }
                      }
                    }
                  }
                }
              }
            }
            fields {
              key: "request.auth.principal"
              value {
                string_value: "auth@istioinaction.io/218d3fb9-4628-4d20-943c-124281c80e7b"
              }
            }
            fields {
              key: "request.auth.raw_claims"
              value {
                string_value: "{\"iat\":1591545071,\"sub\":\"218d3fb9-4628-4d20-943c-124281c80e7b\",\"group\":\"admin\",\"exp\":4745145071,\"iss\":\"auth@istioinaction.io\"}"
              }
            }
          }
        }
        ...


        • 출력은 RequestAuthentication 필터가 최종 사용자 토큰의 클레임을 검증했고, 클레임을 필터 메타데이터로 저장했다는 것을 확인 가능
          • 이제 정책들은 이 필터 메타데이터를 기반으로 작동 가능
        • 다음 실습을 위해 RequestAuthentication, AuthorizationPolicy 삭제
          kubectl delete -f ch9/enduser/jwt-token-request-authn.yaml
          kubectl delete -f ch9/enduser/allow-all-with-jwt-to-webapp.yaml
    • 3.2 한 요청의 대략적인 흐름 Overview of the flow of one request
      • 워크로드가 목적지인 요청은 모두 다음 필터를 거침
        필터 메타데이터에서 검증된 데이터 수집
        요청을 인증 및 인가하도록 서비스 프록시를 구성하는 리소스
        1. JWT authentication filter 인증 필터
          • 인증 정책의 JWT 사양에 따라 JWT의 유효성을 검사하고 인증 클레임과 커스텀 클레임 같은 클레임을 추출해 필터 메타데이터로 저장하는 엔보이 필터
        2. PeerAuthentication filter 피어인증 필터
          • 서비스 인증 요구 사항을 강제하고 인증된 속성(소스 네임스페이스나 주체 같은 피어 ID)을 추출하는 엔보이 필터
        3. Authorization filter 인가 필터
          • 앞선 필터들이 수집한 필터 메타데이터를 확인하고 워크로드에 적용된 정책에 따라 요청에 권한을 부여하는 인가 엔진
        4. webapp 서비스에 도달해야 하는 요청의 시나리오 - 서비스에 대한 요청이 인증되고 인가되는 방식
          1. 요청JWT 인증 필터를 통과
            • 이 필터는 토큰에서 클레임을 추출해 필터 메타데이터에 저장
              • 이로써 요청에 ID가 주어짐
          2. 인그레스 게이트웨이와 webapp 간 피어 간 인증 수행
            • 피어 간 인증 필터는 클라이언트의 ID 데이터를 추출해 필터 메타데이터에 저장
          3. 인가 필터 실행 순서
            • 커스텀 인가 필터들 : 요청을 허용하거나 거부할지 추가로 평가
            • 거부 인가 필터들 : 요청을 허용하거나 거부할지 추가로 평가
            • 허용 인가 필터들 : 필터 조건에 맞으면 요청을 허용
            • 마지막 (포괄적) 인가 필터 : 앞서 요청을 처리한 필터가 없는 경우에만 실행

 


  • 들어가며 : Understanding request identity
    • 인가(Authorization): 인증된 주체가 리소스 접근, 편집, 삭제 같은 작업을 수행하도록 허용됐는지 정의하는 절차
    • 정책은 인증된 주체(’누가’)와 인가(’무엇’)를 결합해 형성되며, 누가 무슨 일을 할 수 있는지 정의
    • Istio에는 AuthorizationPolicy 리소스가 존재
      • 서비스 메시에 메시 범위, 네임스페이스 범위, 워크로드별 접근 정책 정의 (선언적 API)
      • 인가는 공격 범위를 단지 도난당한 ID가 접근할 수 있는 것으로 축소
        인가는 공격 범위를 단지 도난당한 ID가 접근할 수 있는 것으로 축소
    1. Istio에서 인가 이해하기 : AuthorizationPolicy - selector, rules(from, to, when), action
      필터 메타데이터에서 검증된 데이터 수집
      요청을 인증 및 인가하도록 서비스 프록시를 구성하는 리소스
      • 각 서비스와 함께 배포되는 서비스 프록시가 인가 또는 집행 enforcement 엔진
        • 서비스 프록시요청을 거절하거나 허용할지 여부를 판단하기 위한 정책을 모두 포함하고 있기 때문
      • 그러므로 Istio의 접근 제어는 대단히 효율적
        • 모든 결정이 프록시에서 직접 내려지기 때문
      • 프록시는 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
      • istiod가 새 AuthorizationPolicy 가 클러스터에 적용됐음을 확인 후
        (다른 Istio 리소스들처럼) 해당 리소스로 데이터 플레인 프록시를 처리하고 업데이트

      • 인가 정책의 속성 PROPERTIES OF AN AUTHORIZATION POLICY
        • AuthorizationPolicy 리소스 사양에서 정책을 설정하고 정의하는 필드는 세 가지
          • selector 필드는 정책을 적용할 워크로드 부분집합을 정의
          • action 필드는 이 정책이 허용(ALLOW)인지, 거부(DENY)인지, 커스텀(CUSTOM)인지 지정
            • action은 규칙 중 하나가 요청과 일치하는 경우에만 적용
          • rules 필드는 정책을 활성화할 요청을 식별하는 규칙 목록을 정의 (복잡한 정책이므로 상세 확인 필요)

      • 인가 정책 규칙 이해하기 UNDERSTANDING AUTHORIZATION POLICY RULES
        • 인가 정책 규칙은 커넥션은 출처 source 를 지정하며, 일치해야 규칙을 활성화하는 작업 operation 조건을 (원한다면) 지정할 수도 있음
        • 인가 정책은 규칙 중 하나의 출처와 작업 조건을 모두 만족시키는 경우에만 집행됨
        • 이 경우에만 정책이 활성화되고, 커넥션은 action 속성에 따라 허용되거나 거부됨

        • 단일 규칙의 필드
          • from 필드는 요청의 출처 source 를 다음 유형 중 하나로 지정
            • principals : 출처 ID(mTLS 예제에서 볼 수 있는 SPIFFE ID). 요청이 주체 principal 집합에서 온 것이 아니면 부정 속성인 notprincipals 가 적용된다. 이 기능이 작동하려면 서비스 상호 인증 필요
            • namespaces : 출처 네임스페이스와 비교할 네임스페이스 목록. 출처 네임스페이스는 참가자의 SVID에서 가져온다. 이런 이유로, 작동하려면 mTLS 활성화 필요
            • ipBlocks : 출처 IP 주소와 비교할 단일 IP 주소나 CIDR 범위 목록
          • to 필드는 요청의 작업을 지정하며, 호스트나 요청의 메서드 등 존재
          • when 필드는 규칙이 부합한 후 충족해야 하는 조건 목록을 지정
        • 공식 문서 https://istio.io/latest/docs/reference/config/security/authorization-policy/
    2. 작업 공간 설정하기 : 실습 환경 구성 확인 (실습)
      실습 환경 구성 : 9.2에서 이미 배포
      # 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

      webapp 은 HTTP 트래픽을 받아들인다. catalog 서비스에는 상호 인증이 필요하다.
      • 실습 환경 요약
        • sleep 워크로드는 default 네임스페이스에 배포했고, 평문 HTTP 요청을 만드는 데 사용
        • webapp 워크로드는 istioinaction 네임스페이스에 배포했고, default 네임스페이스에 있는 워크로드에서 미인증 요청을 받는 중
        • catalog 워크로드는 istioinaction 네임스페이스에 배포했고, 같은 네임스페이스의 인증된 워크로드로부터만 요청을 받는 중

    3. 워크로드에 정책 적용 시 동작 확인
      • 알아둬야 할 것 (모를 경우 문제 발생, 디버깅에 많은 시간 낭비)
        • 워크로드에 하나 이상의 ALLOW 인가 정책이 적용되면, 모든 트래픽에서 해당 워크로드로의 접근은 기본적으로 거부
        • 트래픽을 받아들이려면, ALLOW 정책이 최소 하나는 부합해야 함
        • ex) 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 리턴
          
          # AuthorizationPolicy 리소스 적용
          kubectl apply -f ch9/allow-catalog-requests-in-web-app.yaml
          kubectl get authorizationpolicy -n istioinaction
          
          # 
          docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/webapp.istioinaction --port 15006
          docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/webapp.istioinaction --port 15006 -o json > webapp-listener.json
          ...
                    {
                        "name": "envoy.filters.http.rbac",
                        "typedConfig": {
                            "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
                            "rules": {
                                "policies": {
                                    "ns[istioinaction]-policy[allow-catalog-requests-in-web-app]-rule[0]": {
                                        "permissions": [
                                            {
                                                "andRules": {
                                                    "rules": [
                                                        {
                                                            "orRules": {
                                                                "rules": [
                                                                    {
                                                                        "urlPath": {
                                                                            "path": {
                                                                                "prefix": "/api/catalog"
                                                                            }
                                                                        }
                                                                    }
                                                                ]
                                                            }
                                                        }
                                                    ]
                                                }
                                            }
                                        ],
                                        "principals": [
                                            {
                                                "andIds": {
                                                    "ids": [
                                                        {
                                                            "any": true
                                                        }
                                                    ]
                                                }
                                            }
                                        ]
                                    }
                                }
                            },
                            "shadowRulesStatPrefix": "istio_dry_run_allow_" #  실제로 차단하지 않고, 정책이 적용됐을 때 통계만 수집 , istio_dry_run_allow_로 prefix된 메트릭 생성됨
                        }
                    },
          ...
          
          # 로그 : 403 리턴 체크!
          docker exec -it myk8s-control-plane istioctl proxy-config log deploy/webapp -n istioinaction --level rbac:debug
          kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
          [2025-05-03T10:08:52.918Z] "GET /hello/world HTTP/1.1" 403 - rbac_access_denied_matched_policy[none] - "-" 0 19 0 - "-" "curl/8.5.0" "b272b991-7a79-9581-bb14-55a6ee705311" "webapp.istioinaction" "-" inbound|8080|| - 10.10.0.3:8080 10.10.0.13:50172 - -
          
          # 적용 후 확인
          kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog
          kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/hello/world # 403 리턴
          RBAC: access denied
          
          # 다음 실습을 위해 정책 삭제
          kubectl delete -f ch9/allow-catalog-requests-in-web-app.yaml


          • 첫 번째 호출: 요청 허용 - 경로가 일치하기 때문
          • 두 번째 호출: 요청 거부 - 정책이 요청을 허용하거나 거부하지 않았는데 왜 거부되는지?
            • ALLOW 정책을 워크로드에 적용했을 때만 적용되는 기본 거부 deny-by-default 동작
          • 워크로드에 ALLOW 정책이 있는 경우, 트래픽이 허용되려면 정책 하나는 반드시 부합해야 함
        • 정책 설정 과정을 단순화해 서비스마다 호출이 허용되는지, ALLOW 정책이 적용되는지를 스스로에게 되묻지 않으려면?
          • 들어오는 트래픽에 다른 정책이 적용되지 않을 때 활성화되는 전체 catch-all 거부 정책을 추가하는 것을 권장
            • 허용하려는 트래픽에 대해서만 생각하고, 그 트래픽용 정책만 생성하면 됨
              전체 거부 정책이 '명시적으로 지정하지 않으면 요청을 거부한다' 로 바꾸는 로직
    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
        ...
      • (참고) Catch-all authorization policies : 빈 규칙 rules 모든 요청을 허용 의미
        # cat ch9/policy-allow-all-mesh.yaml                         
        apiVersion: security.istio.io/v1beta1
        kind: AuthorizationPolicy
        metadata:
          name: allow-all
          namespace: istio-system
        spec: 
          rules: 
          - {}
    5. 특정 네임스페이스에서 온 요청 허용하기 Allowing requests originating from a single namespace
      • 특정 네임스페이스에서 시작한, 모든 서비스에 대한 트래픽을 허용하기
        • source.namespace 속성으로 가능
      • 한 네임스페이스에서 온 HTTP GET 트래픽을 허용하기
        # 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
        
        # AuthorizationPolicy 적용 확인
        kubectl get AuthorizationPolicy -A
        NAMESPACE       NAME                           AGE
        istio-system    deny-all                       11h
        istioinaction   webapp-allow-view-default-ns   11h
        
        docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/webapp.istioinaction --port 15006 -o json
        ...
                        {
                            "name": "envoy.filters.http.rbac",
                            "typedConfig": {
                                "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
                                "rules": {
                                    "policies": {
                                        "ns[istio-system]-policy[deny-all]-rule[0]": {
                                            "permissions": [
                                                {
                                                    "notRule": {
                                                        "any": true
                                                    }
                                                }
                                            ],
                                            "principals": [
                                                {
                                                    "notId": {
                                                        "any": true
                                                    }
                                                }
                                            ]
                                        },
                                        "ns[istioinaction]-policy[webapp-allow-view-default-ns]-rule[0]": {
                                            "permissions": [
                                                {
                                                    "andRules": {
                                                        "rules": [
                                                            {
                                                                "orRules": {
                                                                    "rules": [
                                                                        {
                                                                            "header": {
                                                                                "name": ":method",
                                                                                "exactMatch": "GET"
                                                                            }
                                                                        }
                                                                    ]
                                                                }
                                                            }
                                                        ]
                                                    }
                                                }
                                            ],
                                            "principals": [
                                                {
                                                    "andIds": {
                                                        "ids": [
                                                            {
                                                                "orIds": {
                                                                    "ids": [
                                                                        {
                                                                            "filterState": {
                                                                                "key": "io.istio.peer_principal",
                                                                                "stringMatch": {
                                                                                    "safeRegex": {
                                                                                        "regex": ".*/ns/default/.*"
        ...
        
        # 로그 확인
        kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
        
        # 호출 테스트
        kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog
        ...
      • sleep 서비스레거시 워크로드
      • 사이트카가 없으므로, ID도 없음
        • webapp 프록시는 요청이 default 네임스페이스의 워크로드에서 온 것인지 확인할 수 없음
      • 해결 방안
        1. sleep 서비스에 서비스 프록시 주입하기 → 실습 진행, 권장 방식
        2. webapp에서 미인증 요청 허용하기
      • 권장 방식에 따르면 ID를 부트스트랩하고 다른 워크로드와의 상호 인증을 수행해서 다른 워크로드가 요청의 출처와 네임스페이스를 확인 할 수 있음

      • 그러나 시연을 위해, 첫 번째 접근법이 불가능해(예를 들면, 팀 전체가 휴가 중이라서)
        어쩔 수 없이 두 번째 접근법(덜 안전한)을 사용해야 할 경우
        • 미인증 요청을 허용하는 것
      • 실습
        # default 네임스페이스에 istio-injection=enabled lable 추가
        kubectl label ns default istio-injection=enabled
        kubectl delete pod -l app=sleep
        
        # istio proxy 상태 확인
        docker exec -it myk8s-control-plane istioctl proxy-status
        NAME                                                   CLUSTER        CDS        LDS        EDS        RDS          ECDS         ISTIOD                    VERSION
        sleep-6f8cfb8c8f-wncwh.default                         Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED       NOT SENT     istiod-8d74787f-n4c7b     1.17.8
        ...
        
        # 호출 테스트 : webapp
        kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction # default -> webapp 은 성공
        ...
        
        kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog 
        error calling Catalog service
        
        docker exec -it myk8s-control-plane istioctl proxy-config log deploy/webapp -n istioinaction --level rbac:debug
        kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f # webapp -> catalog 는 deny-all 로 거부됨
        [2025-05-04T02:36:49.857Z] "GET /items HTTP/1.1" 403 - via_upstream - "-" 0 19 0 0 "-" "beegoServer" "669eb3d6-f59a-99e8-80cb-f1ff6c0faf99" "catalog.istioinaction:80" "10.10.0.16:3000" outbound|80||catalog.istioinaction.svc.cluster.local 10.10.0.14:33066 10.200.1.46:80 10.10.0.14:48794 - default
        [2025-05-04T02:36:49.856Z] "GET /api/catalog HTTP/1.1" 500 - via_upstream - "-" 0 29 1 1 "-" "curl/8.5.0" "669eb3d6-f59a-99e8-80cb-f1ff6c0faf99" "webapp.istioinaction" "10.10.0.14:8080" inbound|8080|| 127.0.0.6:38191 10.10.0.14:8080 10.10.0.17:59998 outbound_.80_._.webapp.istioinaction.svc.cluster.local default
        
        
        # 호출 테스트 : catalog
        kubectl logs -n istioinaction -l app=catalog -c istio-proxy -f
        kubectl exec deploy/sleep -- curl -sSL catalog.istioinaction/items # default -> catalog 은 성공
        
        
        # 다음 실습을 위해 default 네임스페이스 원복
        kubectl label ns default istio-injection-
        kubectl rollout restart deploy/sleep
        
        docker exec -it myk8s-control-plane istioctl proxy-status
        kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction # 거부 확인
    6. 미인증 레거시 워크로드에서 온 요청 허용하기 Allowing requests from non-authenticated legacy workload
      • 미인증 워크로드에서 온 요청을 허용하려면 from 필드를 삭제해야 함
      • 아래 정책을 webapp에만 적용하기 위해 app:webapp 셀렉터를 추가
      • 이렇게 하면 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에만 미인증 요청을 허용하기 위해 app:webapp 셀렉터를 추가
        kubectl apply -f ch9/allow-unauthenticated-view-default-ns.yaml
        kubectl get AuthorizationPolicy -A
        NAMESPACE       NAME                                           AGE
        istio-system    deny-all                                       12h
        istioinaction   webapp-allow-unauthenticated-view-default-ns   14s
        istioinaction   webapp-allow-view-default-ns                   11h
        
        # 여러개의 정책이 적용 시에 우선순위는?
        docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/webapp.istioinaction --port 15006 -o json | jq
        ...
                 "name": "envoy.filters.http.rbac",
                                "typedConfig": {
                                    "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
                                    "rules": {
                                        "policies": {
                                            "ns[istio-system]-policy[deny-all]-rule[0]": {
                                                "permissions": [
                                                    {
                                                        "notRule": {
                                                            "any": true
                                                        }
                                                    }
                                                ],
                                                "principals": [
                                                    {
                                                        "notId": {
                                                            "any": true
                                                        }
                                                    }
                                                ]
                                            },
                                            "ns[istioinaction]-policy[webapp-allow-unauthenticated-view-default-ns]-rule[0]": {
                                                "permissions": [
                                                    {
                                                        "andRules": {
                                                            "rules": [
                                                                {
                                                                    "orRules": {
                                                                        "rules": [
                                                                            {
                                                                                "header": {
                                                                                    "name": ":method",
                                                                                    "exactMatch": "GET"
                                                                                }
                                                                            }
                                                                        ]
                                                                    }
                                                                }
                                                            ]
                                                        }
                                                    }
                                                ],
                                                "principals": [
                                                    {
                                                        "andIds": {
                                                            "ids": [
                                                                {
                                                                    "any": true
                                                                }
                                                            ]
                                                        }
                                                    }
                                                ]
                                            },
                                            "ns[istioinaction]-policy[webapp-allow-view-default-ns]-rule[0]": {
                                                "permissions": [
                                                    {
                                                        "andRules": {
                                                            "rules": [
                                                                {
                                                                    "orRules": {
                                                                        "rules": [
                                                                            {
                                                                                "header": {
                                                                                    "name": ":method",
                                                                                    "exactMatch": "GET"
                                                                                }
                                                                            }
                                                                        ]
                                                                    }
                                                                }
                                                            ]
                                                        }
                                                    }
                                                ],
                                                "principals": [
                                                    {
                                                        "andIds": {
                                                            "ids": [
                                                                {
                                                                    "orIds": {
                                                                        "ids": [
                                                                            {
                                                                                "filterState": {
                                                                                    "key": "io.istio.peer_principal",
                                                                                    "stringMatch": {
                                                                                        "safeRegex": {
                                                                                            "regex": ".*/ns/default/.*"
                                                                                        }
        ...
        
        # 호출 테스트 : webapp
        kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction # default -> webapp 은 성공
        ...
        
        kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f # webapp -> catalog 는 deny-all 로 거부됨
        kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog 
        error calling Catalog service
        
        # (옵션) 호출 테스트 : catalog
        kubectl logs -n istioinaction -l app=catalog -c istio-proxy -f
        kubectl exec deploy/sleep -- curl -sSL catalog.istioinaction/items
        • webapp 은 sleep 서비스에서 요청을 허용했지만,
          메시 범위 전체 거부 정책이 catalog 서비스로의 후속 요청을 거부함
    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"]
      • 실습
        #
        kubectl apply -f ch9/catalog-viewer-policy.yaml
        kubectl get AuthorizationPolicy -A
        NAMESPACE       NAME                                           AGE
        istio-system    deny-all                                       13h
        istioinaction   catalog-viewer                                 10s
        istioinaction   webapp-allow-unauthenticated-view-default-ns   61m
        istioinaction   webapp-allow-view-default-ns                   12h
        
        #
        docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/catalog.istioinaction --port 15006 -o json
        ...
                    "principals": [
                        {
                            "andIds": {
                                "ids": [
                                    {
                                        "orIds": {
                                            "ids": [
                                                {
                                                    "filterState": {
                                                        "key": "io.istio.peer_principal",
                                                        "stringMatch": {
                                                            "exact": "spiffe://cluster.local/ns/istioinaction/sa/webapp"
                                                        }
        ...
        
        # 호출 테스트 : sleep --(미인증 레거시 허용)--> webapp --(principals webapp 허용)--> catalog
        kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction
        kubectl exec deploy/sleep -- curl -sSL webapp.istioinaction/api/catalog 
        kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
        kubectl logs -n istioinaction -l app=catalog -c istio-proxy -f
        
        # (옵션) 호출 테스트 : catalog
        kubectl exec deploy/sleep -- curl -sSL catalog.istioinaction/items
        ...
      • 더 중요한 점은, 워크로드의 ID 도용되더라도 피해가 가능한 한 최소 범위로 제한되도록 엄격한 인가 정책을 시행하고 있다는 것

    8. 정책의 조건부 적용 Conditional matching of policies
      • 가끔 어떤 정책은 특정 조건이 충족되는 경우에만 적용되기도 함
        • 사용자가 관리자일 때는 모든 작업을 허용하는 식
      • 인가 정책의 when 속성을 사용해 특정 조건만 정책 수행 (groups: admin)
        apiVersion: "security.istio.io/v1beta1"
        kind: "AuthorizationPolicy"
        metadata:
          name: "allow-mesh-all-ops-admin"
          namespace: istio-system
        spec:
          rules:
          - from:
            - source:
                requestPrincipals: ["auth@istioinaction.io/*"]
            when:
            - key: request.auth.claims[groups] # 이스티오 속성을 지정한다
              values: ["admin"] # 반드시 일치해야 하는 값의 목록을 지정한다
         
        • 이 정책은 다음 두 조건이 모두 충족될 때만 요청을 허용
          • 첫째, 토큰은 요청 주체 auth@istioinaction.io/* 가 발급한 것이어야 함
          • 둘째, JWT에 값이 ‘admin’인 group 클레임 claim이 포함돼 있어야 함
        • 또는 notValues 속성을 사용해 이 정책을 적용하지 않아야 하는 값들을 정의할 수도 있음
        • 조건에서 사용할 수 있는 Istio 속성 전체 목록 - https://istio.io/latest/docs/reference/config/security/conditions/
        • 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. 값 비교 표현식 이해하기 Understanding value-match expressions
      • 값은 항상 정확히 일치할 필요는 없음
      • Istio는 규칙을 더 다양하게 만들 수 있도록 간단한 비교 표현식 지원
        • Exact matching of values 일치. 예를 들어 GET은 값이 정확히 일치해야 함
        • Prefix matching of values 접두사 (매칭)비교. 예를 들어 /api/catlog* 는 /api/catalog/1 과 같이 접두사로 시작하는 모든 값에 부합함
        • Suffix matching of values 접미사 (매칭)비교. 예를 들어 *.istioinaction.io 는 login.istioinaction.io 와 같이 모든 서브도메인에 부합함
        • Presence matching 존재성 (매칭)비교. 모든 값에 부합하며 *로 표기 (필드가 존재해야 하지만, 값은 중요하지 않아 어떤 값이든 괜찮음을 의미)
      • 정책 규칙이 어떻게 평가되는지 이해하기 UNDERSTANDING HOW POLICY RULES ARE EVALUATED
        • 좀 더 복잡한 규칙이 어떤 요청에 적용되는지 구체적으로 분석하기 (정책 규칙을 이해하기 위함)
          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"]
        • 상기 인가 정책이 요청에 적용되려면, 첫 번째 규칙이나 두 번째 규칙에 해당해야 함

      • 첫 번째 규칙에 해당하는 경우 보기
          - from: # 소스들 Sources
            - source:
                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"]

         
        • 요청이 이 규칙에 해당하려면, 세 가지 속성에서 모두 부합 필요
          • source 목록에서 정의한 source 중 하나가 operation 목록에서 정의한 operation 과 맞아야 하고, 모든 조건이 부합해야 함
          • from 에서 정의한 source 가 to 에 정의한 operation 중 하나와 AND 연산되고, 둘 다 when 에서 지정한 조건들 모두와 AND 연산
        • operation 상세 확인
              to: # operations 들
              - operation: # 첫 번째 operation
                  methods: ["GET"]   # 첫 번째 operation에 해당하려면 일치해야 하는 두 속성
                  paths: ["/users*"] # 첫 번째 operation에 해당하려면 일치해야 하는 두 속성
              - operation: # 첫 번째 operation
                  methods: ["POST"]  # 두 번째 operation에 해당하려면 일치해야 하는 두 속성
                  paths: ["/data"]   # 두 번째 operation에 해당하려면 일치해야 하는 두 속성

          • 이 규칙에서 operation 이 부합하려면, 첫 번째나 두 번째 operation이 부합해야 함
          • operation 이 부합하려면 모든 속성 부합 필요 (모든 속성이 AND로 연결)
        • when 속성의 경우도 AND로 연결되기 때문에 모든 조건 부합 필요

    10. 인가 정책이 평가되는 순서 이해하기 Understanding the order in which authorization policies are evaluated
      https://istio.io/v1.17/docs/concepts/security/#implicit-enablement
      • 정책 복잡성 대두 - 한 워크로드에 많은 정책이 적용되고 순서를 이해하기 어려워질 경우
      • 많은 솔루션이 priority 필드를 사용해 순서를 정의
      • Istio는 정책 평가에 다른 접근법 사용
        1. CUSTOM policies are evaluated first. CUSTOM 정책이 가장 먼저 평가됨
          • 추후 외부 인가 서버와 통합할 때 CUSTOM 정책의 사례 확인 가능
        2. DENY policies are evaluated next. If no DENY policy is matched . . .
          • DENY 정책 평가: 일치하는 DENY 정책이 없으면…
        3. ALLOW policies are evaluated. If one matches, the request is allowed. Otherwise. . .
          • ALLOW 정책 평가: 일치하는 것이 있으면 허용된다. 그렇지 않으면…
        4. According to the presence or absence of a catch-all policy, we have two outcomes: 일반 정책의 존재 유무에 따라 두 가지 결과가 나타난다.
          1. When a catch-all policy is present, it determines whether the request is approved. 일반 정책이 존재하면, 일반 정책이 요청 승인 여부를 결정한다.
          2. 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. ALLOW 정책이 있지만 아무것도 해당되지 않으면 거부된다.
      • 조건에 따라 동작이 바뀌므로, Flowchart 가 이해가 더 쉬움
      • 흐름이 조금 복잡하지만, 일반 DENY 정책을 정의하면 휠씬 간단
      • 요청을 거부하는 CUSTOM과 DENY 정책이 없으면, 허용할 ALLOW 정책이 있는지만 확인

 


  • 사전 지식 : Service Account Token Volume Projection, Admission Control, JWT(JSON Web Token), OIDC
    • Service Account Token Volume Projection : '서비스 계정 토큰'의 시크릿 기반 볼륨 대신 'projected volume' 사용
    • Service Account Token (SAT) Volume Projection - 링크
      • 서비스 계정 토큰을 이용해서 서비스와 서비스, 즉 파드(pod)와 파드(pod)의 호출에서 자격 증명으로 사용할 수 있을까?
      • 불행히도 기본 서비스 계정 토큰으로는 사용하기에 부족함이 있음
        • 토큰을 사용하는 대상(audience), 유효 기간(expiration) 등 토큰의 속성을 지정할 필요가 있기 때문
      • Service Account Token Volume Projection 기능을 사용하면 이러한 부족한 점들을 해결할 수 있음
        apiVersion: v1
        kind: Pod
        metadata:
          name: nginx
        spec:
          containers:
          - image: nginx
            name: nginx
            volumeMounts:
            - mountPath: /var/run/secrets/tokens
              name: vault-token
          serviceAccountName: build-robot
          volumes:
          - name: vault-token
            projected:
              sources:
              - serviceAccountToken:
                  path: vault-token
                  expirationSeconds: 7200
                  audience: vault
    • Bound Service Account Token Volume 바인딩된 서비스 어카운트 토큰 볼륨 - 링크 영어
      • FEATURE STATE: Kubernetes v1.22 [stable]
      • 서비스 어카운트 어드미션 컨트롤러는 토큰 컨트롤러에서 생성한 만료되지 않은 서비스 계정 토큰에 시크릿 기반 볼륨 대신 다음과 같은 프로젝티드 볼륨을 추가한다.
        - name: kube-api-access-<random-suffix>
          projected:
            defaultMode: 420 # 420은 rw- 로 소유자는 읽고쓰기 권한과 그룹내 사용자는 읽기만, 보통 0644는 소유자는 읽고쓰고실행 권한과 나머지는 읽고쓰기 권한
            sources:
              - serviceAccountToken:
                  expirationSeconds: 3607
                  path: token
              - configMap:
                  items:
                    - key: ca.crt
                      path: ca.crt
                  name: kube-root-ca.crt
              - downwardAPI:
                  items:
                    - fieldRef:
                        apiVersion: v1
                        fieldPath: metadata.namespace
                      path: namespace
      • 프로젝티드 볼륨은 세 가지로 구성된다. PSAT (Projected Service Account Tokens)
        1. kube-apiserver로부터 TokenRequest API를 통해 얻은 서비스어카운트토큰(ServiceAccountToken).
          • 서비스어카운트토큰은 기본적으로 1시간 뒤에, 또는 파드가 삭제될 때 만료된다.
          • 서비스어카운트토큰은 파드에 연결되며 kube-apiserver를 위해 존재한다.
        2. kube-apiserver에 대한 연결을 확인하는 데 사용되는 CA 번들을 포함하는 컨피그맵(ConfigMap).
        3. 파드의 네임스페이스를 참조하는 DownwardA

         

      • Configure a Pod to Use a Projected Volume for Storage : 시크릿 컨피그맵 downwardAPI serviceAccountToken의 볼륨 마운트를 하나의 디렉터리에 통합 - 링크
      • 프로젝션 볼륨을 사용하여 여러 기존 볼륨 소스를 동일한 디렉터리에 마운트하는 방법
        • 현재 secret, configMap, downwardAPI serviceAccountToken 볼륨을 프로젝션할 수 있음
      • 명심할 것: serviceAccountToken 은 볼륨 타입이 아님
        apiVersion: v1
        kind: Pod
        metadata:
          name: test-projected-volume
        spec:
          containers:
          - name: test-projected-volume
            image: busybox:1.28
            args:
            - sleep
            - "86400"
            volumeMounts:
            - name: all-in-one
              mountPath: "/projected-volume"
              readOnly: true
          volumes:
          - name: all-in-one
            projected:
              sources:
              - secret:
                  name: user
              - secret:
                  name: pass

        # Create the Secrets:
        ## Create files containing the username and password:
        echo -n "admin" > ./username.txt
        echo -n "1f2d1e2e67df" > ./password.txt
        
        ## Package these files into secrets:
        kubectl create secret generic user --from-file=./username.txt
        kubectl create secret generic pass --from-file=./password.txt
        
        # 파드 생성
        kubectl apply -f https://k8s.io/examples/pods/storage/projected.yaml
        
        # 파드 확인
        kubectl get pod test-projected-volume -o yaml | kubectl neat
        ...
        volumes:
          - name: all-in-one
            projected:
              defaultMode: 420
              sources:
              - secret:
                  name: user
              - secret:
                  name: pass
          - name: kube-api-access-n6n9v
            projected:
              defaultMode: 420
              sources:
              - serviceAccountToken:
                  expirationSeconds: 3607
                  path: token
              - configMap:
                  items:
                  - key: ca.crt
                    path: ca.crt
                  name: kube-root-ca.crt
              - downwardAPI:
                  items:
                  - fieldRef:
                      apiVersion: v1
                      fieldPath: metadata.namespace
                    path: namespace
        
        # 시크릿 확인
        kubectl exec -it test-projected-volume -- ls /projected-volume/
        password.txt  username.txt
        
        kubectl exec -it test-projected-volume -- cat /projected-volume/username.txt ;echo
        admin
        
        kubectl exec -it test-projected-volume -- cat /projected-volume/password.txt ;echo
        1f2d1e2e67df
        
        # 삭제
        kubectl delete pod test-projected-volume && kubectl delete secret user pass
      • k8s api 접근 단계
        • AuthNAuthZAdmisstion Control 권한이 있는 사용자에 한해서 관리자(Admin)가 특정 행동을 제한(validate) 혹은 변경(mutate) - 링크 Slack
        • AuthN & AuthZ - MutatingWebhook - Object schema validation - ValidatingWebhook → etcd
          https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/
        • Admission Control도 Webhook으로 사용자에게 API가 열려있고, 사용자는 자신만의 Admission Controller를 구현할 수 있으며, 이를 Dynamic Admission Controller라고 부르고, 크게 MutatingWebhookValidatingWebhook 로 나뉨
        • MutatingWebhook은 사용자가 요청한 request에 대해서 관리자가 임의로 값을 변경하는 작업
        • ValidatingWebhook은 사용자가 요청한 request에 대해서 관리자기 허용을 막는 작업
          #
          kubectl get mutatingwebhookconfigurations
          NAME                              WEBHOOKS   AGE
          aws-load-balancer-webhook         3          98m
          kube-prometheus-stack-admission   1          96m
          pod-identity-webhook              1          175m
          vpc-resource-mutating-webhook     1          175m
          
          #
          kubectl get validatingwebhookconfigurations
          NAME                              WEBHOOKS   AGE
          aws-load-balancer-webhook         3          97m
          kube-prometheus-stack-admission   1          96m
          vpc-resource-validating-webhook   2          175m
      • JWT : Bearer type - JWT(JSON Web Token) X.509 Certificatelightweight JSON 버전
        • Bearer type: 서버에서 지정한 어떠한 문자열도 입력할 수 있음 (하지만 굉장히 허술한 느낌을 받음)
        • 이를 보완하고자 쿠버네티스에서 Bearer 토큰을 전송할 때 주로 JWT (JSON Web Token) 토큰을 사용
        • JWT는 X.509 Certificate와 마찬가지로 private key를 이용하여 토큰을 서명하고 public key를 이용하여 서명된 메세지를 검증
          • 해당 토큰이 쿠버네티스를 통해 생성된 valid한 토큰임을 인증 가능
          • X.509 Certificate의 lightweight JSON 버전이라고 생각하면 편리
        • jwtJSON 형태로 토큰 형식을 정의한 스펙이며, 쿠버네티스에서 뿐만 아니라 다양한 웹 사이트에서 인증, 권한 허가, 세션관리 등의 목적으로 사용
          • Header: 토큰 형식와 암호화 알고리즘을 선언
          • Payload: 전송하려는 데이터를 JSON 형식으로 기입
          • Signature: Header와 Payload의 변조 가능성을 검증
        • 각 파트는 base64 URL 인코딩이 되어서 .으로 합쳐짐
          https://research.securitum.com/jwt-json-web-token-security/

          https://coffeewhale.com/kubernetes/authentication/http-auth/2020/05/03/auth02/


          https://jwt.io/
        • (심화 참고) JWT 소개 추천 영상 - 생활코딩 , 코딩애플
      • OIDC : 사용자를 인증해 사용자에게 액세스 권한을 부여할 수 있게 해주는 프로토콜 ⇒ [커피고래]님 블로그 OpenID Connect - 링크
        • OAuth 2.0 : 권한허가 처리 프로토콜, 다른 서비스에 접근할 수 있는 권한을 획득하거나 반대로 다른 서비스에게 권한을 부여할 수 있음 - 생활코딩
          • 위임 권한 부여 Delegated Authorization, 사용자 인증 보다는 제한된 사람에게(혹은 시스템) 제한된 권한을 부여하는가, 예) 페이스북 posting 권한
          • Access Token : 발급처(OAuth 2.0), 서버의 리소스 접근 권한
        • OpenID : 비영리기관인 OpenID Foundation에서 추진하는 개방형 표준 및 분산 인증 Authentication 프로토콜, 사용자 인증 및 사용자 정보 제공(id token) - 링크
          • ID Token : 발급처(OpenID Connect), 유저 프로필 정보 획득
        • OIDC OpenID Connect = OpenID 인증 + OAuth2.0 인가, JSON 포맷을 이용한 RESful API 형식으로 인증 - 링크
          • iss: 토큰 발행자
          • sub: 사용자를 구분하기 위한 유니크한 구분자
          • email: 사용자의 이메일
          • iat: 토큰이 발행되는 시간을 Unix time으로 표기한 것
          • exp: 토큰이 만료되는 시간을 Unix time으로 표기한 것
          • aud: ID Token이 어떤 Client를 위해 발급된 것인지
        • IdP Open Identify Provider : 구글, 카카오와 같이 OpenID 서비스를 제공하는 신원 제공자.
          • OpenID Connect에서 IdP의 역할을 OAuth가 수행 - 링크
        • RP Relying Party : 사용자를 인증하기 위해 IdP에 의존하는 주체

 

  1. JSON 웹 토큰이란? - wiki (실습)
    • Istio에서 JWT를 사용해 최종 사용자 인증 및 인가를 지원
    • JWT는 클라이언트르 서버에 인증하는 데 사용하는 간단한 클레임 표현

    • JWT 구성 요소 3가지
      • 헤더 : 유형 및 해싱 알고리듬으로 구성
      • 페이로드 : 사용자 클레임 포함
      • 서명 : JWT의 진위 여부를 파악하는 데 사용
    • JWT는 HTTP 요청으로 사용하기에 매우 적합
      • 헤더, 페이로드, 서명이 점(.)으로 구분되고 Base64 URL로 인코딩되기 때문
    • 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는 어떻게 발행되고 검증되는가? HOW IS A JWT ISSUED AND VALIDATED?
      • JWT(JSON 웹 토큰)는 인증 서버에서 발급되는데, 인증 서버는 토큰을 서명하는 비밀 키와 검증하기 위한 공개 키를 갖고 있음
      • 공개 키JWKS JSON Web Key Set, JSON 웹 키셋 라고 하며, well-known HTTP 엔드포인트노출
      • 서비스는 이 엔드포인트에서 공개 키를 가져와 인증 서버가 발급한 토큰을 검증할 수 있음
      • 인증 서버 솔루션 구현
        1. 애플리케이션 백엔드 프레임워크에서 구현
        2. OpenIAM 혹은 Keycloak 등의 서비스로, 자체적으로 구현
        3. Auth0, Okta 등의 서비스형 ID Identity-as-a-Service 솔루션으로 구현
      • JWKS는 서명을 복호화하는 데 사용하는 공개 키를 포함
      • 서명을 복호화하고 토큰 데이터의 해시값과 비교하는데, 일치하면 토큰 클레임을 신뢰 가능
      • 서버가 토큰을 검증할 때 JWKS 사용 방법
        서버는 클라이언트가 제시한 토큰을 검증하기 위해 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에 변조가 없었음을 보장하므로 신뢰할 수 있음
  2. 인그레스 게이트웨이에서의 최종 사용자 인증 및 인가 End-user authentication and authorization at the ingress gateway
    • Istio 워크로드가 JWT로 최종 사용자 요청을 인증하고 인가하도록 설정할 수 있음
    • 최종 사용자: ID 제공자에게 인증받고 ID클레임을 나타내는 토큰발급받은 사용자
    • 최종 사용자 인가보통은 Istio 인그레스 게이트웨이에서 수행 (모든 워크로드 수준에서 수행은 가능)
    • 장점
      • 성능 향상: 유효하지 않은 요청을 조기에 거부
      • 요청에서 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
  3. 9.4.3 RequestAuthentication으로 JWT 검증하기 Validating JWTs with RequestAuthentication
    • 들어가며
      필터 메타데이터에서 검증된 데이터 수집
      요청을 인증 및 인가하도록 서비스 프록시를 구성하는 리소스
      • RequestAuthentication 리소스의 주목적은 JWT를 검증하고, 유효한 토큰의 클레임추출하고, 이 클레임을 필터 메타데이터에 저장하는 것
      • 이 필터 메타데이터는 인가 정책이 조치를 취하는 근거로 사용
      • 필터 메타데이터: 서비스 프록시에서 필터 간 요청을 처리하는 동안 사용할 수 있는 키-값 쌍의 모음
        • Istio 사용자로서 이것은 대부분 구현 세부 사항에 해당
        • ex) 클레임 group: admin 이 있는 요청이 검증되면 이 값은 필터 메타데이터로 저장되며,
          필터 메타데이터는 인가 정책이 요청을 허용하거나 거부하는 데 사용됨
      • 최종 사용자 요청에 따른 결과 (3가지)
        • 유효한 토큰을 갖고 있는 요청은 클러스터로 받아들여지며, 이들의 클레임은 필터 메타데이터 형태로 정책에 전달
        • 유효하지 않은 토큰을 갖고 있는 요청은 거부
        • 토큰이 없는 요청은 클러스터로 받아들여지지만 요청 ID가 없음
          (즉, 어떤 클레임도 필터 메타데이터에 저장되지 않음)
      • JWT가 있는 요청과 없는 요청 차이
        • JWT가 있는 요청: RequestAuthentication 필터로 검증되고 JWT 클레임이 커넥션 필터 메타데이터에 저장
        • JWT가 없는 요청: 커넥션 필터 메타데이터에 클레임이 없음
      • 여기서 암시하는 중요한 세부 사항: RequestAuthentication 리소스 그 자체는 인가를 적용하지 않는다(’인가를 강제하지 않는다’)는 것
        • 토큰 검증과 claim 추출을 통해 인증의 유효성을 검증하고 인가에서 활용할 정보를 저장하는 역할
      • 즉, 인가를 위해서는 여전히 AuthorizationPolicy 가 필요


      • RequestAuthentication 리소스 만들기 CREATING A REQUESTAUTHENTICATION RESOURCE
        • 다음 RequestAuthentication 리소스는 Istio의 인그레스 게이트웨이에 적용된다.
        • 이는 인그레스 게이트웨이가 auth@istioinaction.io 에서 발급한 토큰을 검증하도록 설정
          # cat ch9/enduser/jwt-token-request-authn.yaml 
          apiVersion: "security.istio.io/v1beta1"
          kind: "RequestAuthentication"
          metadata:
            name: "jwt-token-request-authn"
            namespace: istio-system # 적용할 네임스페이스
          spec:
            selector:
              matchLabels:
                app: istio-ingressgateway
            jwtRules:
            - issuer: "auth@istioinaction.io" # 발급자 Expected issuer
              jwks: | # 특정 JWKS로 검증
                { "keys":[ {"e":"AQAB","kid":"CU-ADJJEbH9bXl0tpsQWYuo4EwlkxFUHbeJ4ckkakCM","kty":"RSA","n":"zl9VRDbmVvyXNdyoGJ5uhuTSRA2653KHEi3XqITfJISvedYHVNGoZZxUCoiSEumxqrPY_Du7IMKzmT4bAuPnEalbY8rafuJNXnxVmqjTrQovPIerkGW5h59iUXIz6vCznO7F61RvJsUEyw5X291-3Z3r-9RcQD9sYy7-8fTNmcXcdG_nNgYCnduZUJ3vFVhmQCwHFG1idwni8PJo9NH6aTZ3mN730S6Y1g_lJfObju7lwYWT8j2Sjrwt6EES55oGimkZHzktKjDYjRx1rN4dJ5PR5zhlQ4kORWg1PtllWy1s5TSpOUv84OPjEohEoOWH0-g238zIOYA83gozgbJfmQ"}]}
                
          #
          kubectl apply -f ch9/enduser/jwt-token-request-authn.yaml
          kubectl get requestauthentication -A
          
          #
          docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system
          docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system --port 8080 -o json
          ...
                  "httpFilters": [
                          {
                              "name": "istio.metadata_exchange",
                              "typedConfig": {
                                  "@type": "type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm",
                                  "config": {
                                      "vmConfig": {
                                          "runtime": "envoy.wasm.runtime.null",
                                          "code": {
                                              "local": {
                                                  "inlineString": "envoy.wasm.metadata_exchange"
                                              }
                                          }
                                      },
                                      "configuration": {
                                          "@type": "type.googleapis.com/envoy.tcp.metadataexchange.config.MetadataExchange"
                                      }
                                  }
                              }
                          },
                          {
                              "name": "envoy.filters.http.jwt_authn",
                              "typedConfig": {
                                  "@type": "type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication",
                                  "providers": {
                                      "origins-0": {
                                          "issuer": "auth@istioinaction.io",
                                          "localJwks": {
                                              "inlineString": "{ \"keys\":[ {\"e\":\"AQAB\",\"kid\":\"CU-ADJJEbH9bXl0tpsQWYuo4EwlkxFUHbeJ4ckkakCM\",\"kty\":\"RSA\",\"n\":\"zl9VRDbmVvyXNdyoGJ5uhuTSRA2653KHEi3XqITfJISvedYHVNGoZZxUCoiSEumxqrPY_Du7IMKzmT4bAuPnEalbY8rafuJNXnxVmqjTrQovPIerkGW5h59iUXIz6vCznO7F61RvJsUEyw5X291-3Z3r-9RcQD9sYy7-8fTNmcXcdG_nNgYCnduZUJ3vFVhmQCwHFG1idwni8PJo9NH6aTZ3mN730S6Y1g_lJfObju7lwYWT8j2Sjrwt6EES55oGimkZHzktKjDYjRx1rN4dJ5PR5zhlQ4kORWg1PtllWy1s5TSpOUv84OPjEohEoOWH0-g238zIOYA83gozgbJfmQ\"}]}\n"
                                          },
                                          "payloadInMetadata": "auth@istioinaction.io"
                                      }
                                  },
                                  "rules": [
                                      {
                                          "match": {
                                              "prefix": "/"
                                          },
                                          "requires": {
                                              "requiresAny": {
                                                  "requirements": [
                                                      {
                                                          "providerName": "origins-0"
                                                      },
                                                      {
                                                          "allowMissing": {}
                                                      }
                                                  ]
                                              }
                                          }
                                      }
                                  ],
                                  "bypassCorsPreflight": true
                              }
                          },
                          {
                              "name": "istio_authn",
                              "typedConfig": {
                                  "@type": "type.googleapis.com/istio.envoy.config.filter.http.authn.v2alpha1.FilterConfig",
                                  "policy": {
                                      "origins": [
                                          {
                                              "jwt": {
                                                  "issuer": "auth@istioinaction.io"
                                              }
                                          }
                                      ],
                                      "originIsOptional": true,
                                      "principalBinding": "USE_ORIGIN"
                                  },
                                  "skipValidateTrustDomain": true
          ...
      • 유효한 발행자의 토큰이 있는 요청은 받아들여진다 REQUESTS WITH TOKENS FROM VALID ISSUERS ARE ACCEPTED
        • 유효한 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
      • 유효하지 않은 발행자의 토큰이 있는 요청은 거부된다 REQUESTS WITH TOKENS FROM INVALID ISSUERS ARE REJECTED
        • 유효하지 않은 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
          [2025-05-04T06:36:22.089Z] "GET /api/catalog HTTP/1.1" 401 - jwt_authn_access_denied{Jwt_issuer_is_not_configured} - "-" 0 28 1 - "172.18.0.1" "curl/8.7.1" "2e183b2e-0968-971d-adbc-6b149171912b" "webapp.istioinaction.io:30000" "-" outbound|80||webapp.istioinaction.svc.cluster.local - 10.10.0.5:8080 172.18.0.1:65436 - -
      • 토큰이 없는 요청은 클러스터로 받아들여진다 REQUESTS WITHOUT TOKENS ARE ADMITTED INTO THE CLUSTER
        • 토큰 없이 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가 없는 요청 거부하기 DENYING REQUESTS WITHOUT JWTS
        • JWT가 없는 요청 거부하려면 명시적으로 거부하는 AuthorizationPolicy 리소스 생성 필요
          • requestPrincipals 속성이 없는 source 에서 온 모든 요청에 적용되며, (action 속성에 지정된 대로) 요청을 거부
        • requestPrincipals초기화 방식 - JWT의 발행자 issuer 와 주체 subject 클레임을 ‘iss/sub’ 형태로 결합한 것
        • 클레임RequestPrincipals 리소스로 인증되고, AuthorizationPolicy 필터 등 다른 필터가 사용할 수 있도록 커넥션 메타데이터가공
          # cat ch9/enduser/app-gw-requires-jwt.yaml # vi/vim, vscode 에서 포트 30000 추가
          apiVersion: security.istio.io/v1beta1
          kind: AuthorizationPolicy
          metadata:
            name: app-gw-requires-jwt
            namespace: istio-system
          spec:
            selector:
              matchLabels:
                app: istio-ingressgateway
            action: DENY
            rules:
            - from:
              - source:
                  notRequestPrincipals: ["*"] # 요청 주체에 값이 없는 source는 모두 해당된다
              to:
              - operation:
                  hosts: ["webapp.istioinaction.io:30000"] # 이 규칙은 이 특정 호스트에만 적용된다
                  ports: ["30000"]
          
          #
          kubectl apply -f ch9/enduser/app-gw-requires-jwt.yaml
          
          #
          kubectl get AuthorizationPolicy -A
          NAMESPACE      NAME                  AGE
          istio-system   app-gw-requires-jwt   2m14s
          
          docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system --port 8080 -o json
          ...
                    {
                        "name": "envoy.filters.http.rbac",
                        "typedConfig": {
                            "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
                            "rules": {
                                "action": "DENY",
                                "policies": {
                                    "ns[istio-system]-policy[app-gw-requires-jwt]-rule[0]": {
                                        "permissions": [
                                            {
                                                "andRules": {
                                                    "rules": [
                                                        {
                                                            "orRules": {
                                                                "rules": [
                                                                    {
                                                                        "header": {
                                                                            "name": ":authority",
                                                                            "stringMatch": {
                                                                                "exact": "webapp.istioinaction.io:30000",
                                                                                "ignoreCase": true
                                                                            }
                                                                        }
                                                                    }
                                                                ]
                                                            }
                                                        }
                                                    ]
                                                }
                                            }
                                        ],
                                        "principals": [
                                            {
                                                "andIds": {
                                                    "ids": [
                                                        {
                                                            "notId": {
                                                                "orIds": {
                                                                    "ids": [
                                                                        {
                                                                            "metadata": {
                                                                                "filter": "istio_authn",
                                                                                "path": [
                                                                                    {
                                                                                        "key": "request.auth.principal"
                                                                                    }
                                                                                ],
                                                                                "value": {
                                                                                    "stringMatch": {
                                                                                        "safeRegex": {
                                                                                            "regex": ".+"
          ...
          
          
          # 호출 1
          curl -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog
          403
          
          # 호출 2
          curl -H "Authorization: Bearer $USER_TOKEN" \
               -sSl -o /dev/null -w "%{http_code}" webapp.istioinaction.io:30000/api/catalog
          
          # 로그
          kubectl logs -n istio-system -l app=istio-ingressgateway -f
          [2025-05-04T07:04:01.791Z] "GET /api/catalog HTTP/1.1" 403 - rbac_access_denied_matched_policy[ns[istio-system]-policy[app-gw-requires-jwt]-rule[0]] - "-" 0 19 0 - "172.18.0.1" "curl/8.7.1" "41678cf6-6ef8-986e-beb4-4e5af46e7a26" "webapp.istioinaction.io:30000" "-" outbound|80||webapp.istioinaction.svc.cluster.local - 10.10.0.5:8080 172.18.0.1:65424 - -

          • 토큰 없이 요청을 보내고, 요청 주체가 없기 때문에 인가하는 데 실패하는 것을 확인



      • JWT 클레임에 기반한 다양한 접근 수준 DIFFERENT LEVELS OF ACCESS BASED ON JWT CLAIMS
        • 유저별로 다른 접근 정책 설정
          • 일반 사용자가 API에서 데이터를 읽는 것은 허용하지만 새 데이터를 쓰거나 기존 데이터를 바꾸는 것은 금지
          • 관리자에게는 모든 권한을 허용
          • 토큰들은 클레임이 다름
            # 일반 사용자 토큰 : 'group: user' 클레임
            jwt decode $(cat ch9/enduser/user.jwt)
            ...
            {
              "exp": 4745145038,
              "group": "user",
              "iat": 1591545038,
              "iss": "auth@istioinaction.io",
              "sub": "9b792b56-7dfa-4e4b-a83f-e20679115d79"
            }
            
            # 관리자 토큰 : 'group: admin' 클레임
            jwt decode $(cat ch9/enduser/admin.jwt)
            ...
            {
              "exp": 4745145071,
              "group": "admin",
              "iat": 1591545071,
              "iss": "auth@istioinaction.io",
              "sub": "218d3fb9-4628-4d20-943c-124281c80e7b"
            }
        • 일반 사용자가 webapp 에서 데이터를 읽을 수 있게 허용하도록 AuthorizationPolicy 리소스 설정
          # cat ch9/enduser/allow-all-with-jwt-to-webapp.yaml # vi/vim, vscode 에서 포트 30000 추가
          apiVersion: security.istio.io/v1beta1
          kind: AuthorizationPolicy
          metadata:
            name: allow-all-with-jwt-to-webapp
            namespace: istio-system
          spec:
            selector:
              matchLabels:
                app: istio-ingressgateway
            action: ALLOW
            rules:
            - from:
              - source:
                  requestPrincipals: ["auth@istioinaction.io/*"] # 최종 사용자 요청 주체를 표현 Represents the end-user request principal
              to:
              - operation:
                  hosts: ["webapp.istioinaction.io:30000"]
                  methods: ["GET"]
        • 관리자에게 모든 작업을 허용하는 AuthorizationPolicy 리소스 설정
          # cat ch9/enduser/allow-mesh-all-ops-admin.yaml
          apiVersion: "security.istio.io/v1beta1"
          kind: "AuthorizationPolicy"
          metadata:
            name: "allow-mesh-all-ops-admin"
            namespace: istio-system
          spec:
            selector:
              matchLabels:
                app: istio-ingressgateway
            action: ALLOW
            rules:
            - from:
              - source:
                  requestPrincipals: ["auth@istioinaction.io/*"]
              when:
              - key: request.auth.claims[group]
                values: ["admin"] # 이 클레임을 포함한 요청만 허용.
        • 실습
          #
          kubectl apply -f ch9/enduser/allow-all-with-jwt-to-webapp.yaml
          kubectl apply -f ch9/enduser/allow-mesh-all-ops-admin.yaml
          
          #
          kubectl get authorizationpolicy -A
          NAMESPACE      NAME                           AGE
          istio-system   allow-all-with-jwt-to-webapp   5s
          istio-system   allow-mesh-all-ops-admin       5s
          istio-system   app-gw-requires-jwt            34m
          
          #
          docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system --port 8080 -o json
          ...
                          "policies": {
                              "ns[istio-system]-policy[allow-all-with-jwt-to-webapp]-rule[0]": {
                                  "permissions": [
                                      {
                                          "andRules": {
                                              "rules": [
                                                  {
                                                      "orRules": {
                                                          "rules": [
                                                              {
                                                                  "header": {
                                                                      "name": ":authority",
                                                                      "stringMatch": {
                                                                          "exact": "webapp.istioinaction.io:30000",
                                                                          "ignoreCase": true
          ...
                              "ns[istio-system]-policy[allow-mesh-all-ops-admin]-rule[0]": {
                                  "permissions": [
                                      {
                                          "andRules": {
                                              "rules": [
                                                  {
                                                      "any": true
                                                  }
                                              ]
                                          }
                                      }
                                  ],
                                  "principals": [
                                      {
                                          "andIds": {
                                              "ids": [
                                                  {
                                                      "orIds": {
                                                          "ids": [
                                                              {
                                                                  "metadata": {
                                                                      "filter": "istio_authn",
                                                                      "path": [
                                                                          {
                                                                              "key": "request.auth.principal"
                                                                          }
                                                                      ],
                                                                      "value": {
                                                                          "stringMatch": {
                                                                              "prefix": "auth@istioinaction.io/"
                                                                          }
                                                                      }
                                                                  }
                                                              }
                                                          ]
                                                      }
                                                  },
                                                  {
                                                      "orIds": {
                                                          "ids": [
                                                              {
                                                                  "metadata": {
                                                                      "filter": "istio_authn",
                                                                      "path": [
                                                                          {
                                                                              "key": "request.auth.claims"
                                                                          },
                                                                          {
                                                                              "key": "group"
                                                                          }
                                                                      ],
                                                                      "value": {
                                                                          "listMatch": {
                                                                              "oneOf": {
                                                                                  "stringMatch": {
                                                                                      "exact": "admin"
          ...
          
          # 수집된 메타데이터를 관찰하고자 서비스 프록시에 rbac 로거 설정
          ## 기본적으로 envoy rbac 로거는 메타데이터를 로그에 출력하지 않는다. 출력을 위해 로깅 수준을 debug 로 설정하자
          docker exec -it myk8s-control-plane istioctl proxy-config log deploy/istio-ingressgateway -n istio-system --level rbac:debug
          
          
          # 일반유저 : [GET]과 [POST] 호출
          USER_TOKEN=$(< ch9/enduser/user.jwt)
          
          curl -H "Authorization: Bearer $USER_TOKEN" \
               -sSl -o /dev/null -w "%{http_code}\n" webapp.istioinaction.io:30000/api/catalog
          
          curl -H "Authorization: Bearer $USER_TOKEN" \
               -XPOST webapp.istioinaction.io:30000/api/catalog \
               --data '{"id": 2, "name": "Shoes", "price": "84.00"}'
          
          # 로그
          kubectl logs -n istio-system -l app=istio-ingressgateway -f
          ...
          , dynamicMetadata: filter_metadata {
            key: "envoy.filters.http.jwt_authn"
            value {
              fields {
                key: "auth@istioinaction.io"
                value {
                  struct_value {
                    fields {
                      key: "exp"
                      value {
                        number_value: 4745145038
                      }
                    }
                    fields {
                      key: "group"
                      value {
                        string_value: "user"
                      }
                    }
          ...
          [2025-05-04T07:39:27.597Z] "POST /api/catalog HTTP/1.1" 403 - rbac_access_denied_matched_policy[none] - "-" 0 19 1 - "172.18.0.1" "curl/8.7.1" "677a3c73-20a1-935a-b039-e2a8beae9d1b" "webapp.istioinaction.io:30000" "-" outbound|80||webapp.istioinaction.svc.cluster.local - 10.10.0.5:8080 172.18.0.1:57196 - -
          
          
          # 관리자 : [GET]과 [POST] 호출
          ADMIN_TOKEN=$(< ch9/enduser/admin.jwt)
          
          curl -H "Authorization: Bearer $ADMIN_TOKEN" \
               -sSl -o /dev/null -w "%{http_code}\n" webapp.istioinaction.io:30000/api/catalog
          
          curl -H "Authorization: Bearer $ADMIN_TOKEN" \
               -XPOST webapp.istioinaction.io:30000/api/catalog \
               --data '{"id": 2, "name": "Shoes", "price": "84.00"}'
          
          # 로그
          kubectl logs -n istio-system -l app=istio-ingressgateway -f

          • 관리자가 catalog 에 새 아이템을 만들 수 있도록 허용하고 있음을 확인

       

 


  • 들어가며 : 외부 인가 서비스 호출
    • Istio는 Envoy의 기본 RBAC 기능을 사용해 인가를 구현
    • 인가에 좀 더 정교한 커스텀 메커니즘이 필요할 경우: 외부 인가 서비스를 호출하도록 Istio의 서비스 프록시 설정 가능
      외부 서버에서 요청을 인가받도록 CUSTOM 정책 사용하기
    • 그림 9.13에서 서비스 프록시에 들어온 요청은 프록시가 외부 인가(ExtAuthz) 서비스를 호출하는 동안 잠시 멈춤
    • 외부 인가 서비스는 애플리케이션 사이드카로 메시 안에 존재하거나 메시 바깥에 존재할 수 있음
    • 외부 인가는 엔보이CheckRequest API를 구현해야 함 - Code
    • 이 API를 구현하는 외부 인가 서비스의 예를 들면 다음과 같음
    • 외부 인가 서비스는 프록시가 인가를 집행하는 데 사용하는 ‘허용’이나 ‘거부’ 메시지를 반환
    • ExtAuthz performance tradeoffs 외부 인가 성능 트레이드오프
      • 요청 경로 중에 외부 인가 서비스를 호출하기 때문에 이 방법을 사용할 때는 지연 시간 증가에 대비해야 함
      • Istio 내장 인가 기능은 대체로 충분하고 유연하게 작동하지만, 완벽히 통제해야 한다면 외부 인가 서비스를 호출하면서 생기는 성능 트레이드오프 평가 필요
      • 외부 인가 서비스를 애플리케이션 사이드카로 배포해 네트워크 오버헤드 최소화 가능
        https://istio.io/latest/docs/tasks/security/authorization/authz-custom/

 

  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 인지만 검사
      • 이 헤더가 요청에 들어 있으면 요청 허용되고, 들어 있지 않으면 요청은 거부
      • 요청의 다른 속성을 평가하도록 외부 인가 서비스를 직접 작성하거나, 기존 서비스 중 하나를 골라 사용 가능
  2. 이스티오에 외부 인가 설정하기 Configuring Istio for ExtAuthz
    • Istio가 새로운 외부 인가 서비스를 인식하도록 설정
      • istio-system 네임스페이스의 istio configmap 설정 변경
        • meshconfig 설정에서 extensionProviders 설정
    • 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 헤더를 전달
      • 예제 외부 인가 서비스에서는 이 헤더인가 결과를 결정하는 데 사용
      • 외부 인가 기능을 사용하기 위한 마지막 단계
        • 이 기능을 사용하도록 AuthorizationPolicy 리소스를 설정하는 것
  3. 커스텀 AuthorizationPolicy 리소스 사용하기 Using a custom AuthorizationPolicy resource
    • 앞의 실습에서는  action 이 DENY 혹은 ALLOW 인 AuthorizationPolicy 리소스 생성

    • action 이 CUSTOMAuthorizationPolicy 생성 후 외부 인가 서비스 지정해보기
      # 아래 AuthorizationPolicy 는 istioinaction 네임스페이스에 webapp 워크로드에 적용되며, 
      # sample-ext-authz-http 이라는 외부 인가 서비스에 위임한다.
      cat << EOF | kubectl apply -f -
      apiVersion: security.istio.io/v1beta1
      kind: AuthorizationPolicy
      metadata:
        name: ext-authz
        namespace: istioinaction
      spec:
        selector:
          matchLabels:
            app: webapp
        action: CUSTOM    # custom action 사용
        provider:
          name: sample-ext-authz-http  # meshconfig 이름과 동일해야 한다
        rules:
        - to:
          - operation:
              paths: ["/*"]  # 인가 정책을 적용할 경로
      EOF
      
      #
      kubectl get AuthorizationPolicy -A
      NAMESPACE       NAME        AGE
      istioinaction   ext-authz   98s
    • 호출 확인
      #
      docker exec -it myk8s-control-plane istioctl proxy-config log deploy/webapp -n istioinaction --level rbac:debug
      kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
      kubectl logs -n istioinaction -l app=ext-authz -c ext-authz -f
      
      
      # 헤더 없이 호출
      kubectl -n default exec -it deploy/sleep -- curl webapp.istioinaction/api/catalog
      denied by ext_authz for not found header `x-ext-authz: allow` in the request
      
      kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
      2025-05-04T08:33:04.765006Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:114      checking request: requestedServerName: , sourceIP: 10.10.0.18:55834, directRemoteIP: 10.10.0.18:55834, remoteIP: 10.10.0.18:55834,localAddress: 10.10.0.20:8080, ssl: none, headers: ':authority', 'webapp.istioinaction'
      ':path', '/api/catalog'
      ':method', 'GET'
      ':scheme', 'http'
      'user-agent', 'curl/8.5.0'
      'accept', '*/*'
      'x-forwarded-proto', 'http'
      'x-request-id', 'ffd44f00-19ff-96b7-868b-8f6b09bd447d'
      , dynamicMetadata:      thread=31
      2025-05-04T08:33:04.765109Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:130      shadow denied, matched policy istio-ext-authz-ns[istioinaction]-policy[ext-authz]-rule[0]thread=31
      2025-05-04T08:33:04.765170Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:167      no engine, allowed by default      thread=31
      [2025-05-04T08:33:04.764Z] "GET /api/catalog HTTP/1.1" 403 UAEX ext_authz_denied - "-" 0 76 5 4 "-" "curl/8.5.0" "ffd44f00-19ff-96b7-868b-8f6b09bd447d" "webapp.istioinaction" "-" inbound|8080|| - 10.10.0.20:8080 10.10.0.18:55834 - -
      
      kubectl logs -n istioinaction -l app=ext-authz -c ext-authz -f
      2025/05/04 08:35:26 [HTTP][denied]: GET webapp.istioinaction/api/catalog, headers: map[Content-Length:[0] X-B3-Parentspanid:[58148c96f61496a3] X-B3-Sampled:[1] X-B3-Spanid:[960b8d911e81c217] X-B3-Traceid:[ce6c5622c32fd238a934fbf1aa4a9de0] X-Envoy-Expected-Rq-Timeout-Ms:[600000] X-Envoy-Internal:[true] X-Forwarded-Client-Cert:[By=spiffe://cluster.local/ns/istioinaction/sa/default;Hash=491c5bf23be281a5c0c2e798eba242461dfdb7b178d4a4cd842f9eedb05ae47d;Subject="";URI=spiffe://cluster.local/ns/istioinaction/sa/webapp] X-Forwarded-For:[10.10.0.20] X-Forwarded-Proto:[https] X-Request-Id:[964138e3-d955-97c9-b9a5-dfc88cc7f9c5]], body: []
      
      
      # 헤더 적용 호출
      kubectl -n default exec -it deploy/sleep -- curl -H "x-ext-authz: allow" webapp.istioinaction/api/catalog
      
      kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
      2025-05-04T08:37:40.618775Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:114        checking request: requestedServerName: , sourceIP: 10.10.0.18:36150, directRemoteIP: 10.10.0.18:36150, remoteIP: 10.10.0.18:36150,localAddress: 10.10.0.20:8080, ssl: none, headers: ':authority', 'webapp.istioinaction'
      ':path', '/api/catalog'
      ':method', 'GET'
      ':scheme', 'http'
      'user-agent', 'curl/8.5.0'
      'accept', '*/*'
      'x-ext-authz', 'allow'
      'x-forwarded-proto', 'http'
      'x-request-id', 'b446ddf8-fb2e-9dd7-ba01-6e31fac717da'
      , dynamicMetadata:      thread=30
      2025-05-04T08:37:40.618804Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:130        shadow denied, matched policy istio-ext-authz-ns[istioinaction]-policy[ext-authz]-rule[0] thread=30
      2025-05-04T08:37:40.618816Z     debug   envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:167        no engine, allowed by default     thread=30
      [2025-05-04T08:37:40.622Z] "GET /items HTTP/1.1" 200 - via_upstream - "-" 0 502 2 2 "-" "beegoServer" "b446ddf8-fb2e-9dd7-ba01-6e31fac717da" "catalog.istioinaction:80" "10.10.0.19:3000" outbound|80||catalog.istioinaction.svc.cluster.local 10.10.0.20:60848 10.200.1.165:80 10.10.0.20:45874 - default
      [2025-05-04T08:37:40.618Z] "GET /api/catalog HTTP/1.1" 200 - via_upstream - "-" 0 357 6 4 "-" "curl/8.5.0" "b446ddf8-fb2e-9dd7-ba01-6e31fac717da" "webapp.istioinaction" "10.10.0.20:8080" inbound|8080|| 127.0.0.6:43721 10.10.0.20:8080 10.10.0.18:36150 - default
      
      kubectl logs -n istioinaction -l app=ext-authz -c ext-authz -f
      2025/05/04 08:36:34 [HTTP][allowed]: GET webapp.istioinaction/api/catalog, headers: map[Content-Length:[0] X-B3-Parentspanid:[f9bc85c800aaaa05] X-B3-Sampled:[1] X-B3-Spanid:[bf6cc58161f7ca25] X-B3-Traceid:[af1c826a362ce0382e219cd21afe1fe7] X-Envoy-Expected-Rq-Timeout-Ms:[600000] X-Envoy-Internal:[true] X-Ext-Authz:[allow] X-Forwarded-Client-Cert:[By=spiffe://cluster.local/ns/istioinaction/sa/default;Hash=491c5bf23be281a5c0c2e798eba242461dfdb7b178d4a4cd842f9eedb05ae47d;Subject="";URI=spiffe://cluster.local/ns/istioinaction/sa/webapp] X-Forwarded-For:[10.10.0.20] X-Forwarded-Proto:[https] X-Request-Id:[c9b43ce7-25d4-94ae-b684-1565ad36f533]], body: []
    • 결론
      • PeerAuthentication피어인증을 정의하는 데 사용하며, 엄격한 인증 요구 사항을 적용하면 트래픽암호화돼 도청할 수 없음
        • PERMISSIVE 정책은 이스티오 워크로드가 암호화된 트래픽과 평문 트래픽을 모두 수용할 수 있게 해서 다운타임 없이 천천히 마이그레이션할 수 있도록 함
      • AuthorizationPolicy 는 워크로드 ID 인증서나 최종 사용자 JWT에서 추출한 검증 가능한 메타데이터를 근거로 서비스 사이의 요청이나 최종 사용자의 요청을 인가(허용, 차단)하는 데 사용
      • RequestAuthentication 은 JWT가 포함된 최종 사용자 요청을 인증하는 데 사용
      • AuthorizationPolicy 에서 CUSTOM action을 사용하면 외부 인가 서비스를 통합 가능