Service_Mesh/Istio
[Istio-5주차] 마이크로서비스 통신 보안 (이론, 실습)
lati-tech
2025. 5. 11. 04:53
사전 지식 - 추가 정보 : 암호학 (정리 중)
더보기

대칭키 암호화 - https://www.lgcns.com/blog/cns-tech/security/16037/
배경 : 암호(대칭키/비대칭키), 전자서명, 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)

- 대칭키 암호화 방식
- 쌍방이 공유하는 하나의 비밀키를 사용하여 암호화하고 복호하는 방식으로서, 대칭(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
- des3 암호화 알고리즘을 이용하여 파일내용 암호화
- 블록 암호화 방식(Block cipher)
- 평문을 일정길이 단위로 분할한 블록들에 대하여 암호화 하는 방식
- DES, 3DES, IDEA, RC4/5 등이 있음
- (a)는 DES방식으로, 평문을 64비트의 블록으로 분할한 후, 각 블록에 대하여 56비트의 키로 암호화하여, 64비트의 암호문 블록을 생성
- 만약 평문이 64비트로 분할되지 않는 경우, 필요한 비트 수 만큼 패딩하여 암호화
- (b)는 두 종류의 56비트 키와 세 번에 걸친 DES 과정을 수행하여 보다 안전한 암호문을 생성
- 공유 비밀키 암호화 고려사항
- 보내는 사람과 받는 사람에 대한 신원 확인을 할 수 있는가? ⇒ 서명
- 누군가 중간에 데이터를 가로채서, 임의의 값을 추가하여 변조(수정)하여 전달 할 경우 어떻게 무결성을 확인 할 수 있는가? ⇒ 해시알고리즘
- 온라인 상에서, 키 값을 어떻게 안전하게 전달할 것인가?
- 보내는 사람과 받는 사람에 대한 신원 확인을 할 수 있는가? ⇒ 서명
단방향 알고리즘 (해시) 알아보기 - 생활코딩 , 노마드코더
- 파일 무결성 확인 방법 - 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 기본적인 안전 보장 방법
- 서비스 간 및 최종 사용자 인증
- 서비스 메시 내 서비스에 대한 접근 제어
9.1 애플리케이션 네트워크 보안의 필요성
- 들어가며: 인증, 인가, 전송 중 데이터 암호화
- 애플리케이션 보안: 인가 받지 않은 사용자가 오염시키거나 훔치거나 접근해서는 안 되는 귀중한 애플리케이션 데이터를 보호하는 데 기여하는 모든 행동
- 사용자 데이터 보호 방법
- 사용자 인증 및 인가: 리소스 접근을 허가하기 전 감사 절차
- 전송 중 데이터 암호화: 데이터를 요청한 클라이언트로 가면서 여러 네트워크 장치를 거쳐가는 동안 데이터 도청을 방지
- 사용자 데이터 보호 방법
- 애플리케이션 보안: 인가 받지 않은 사용자가 오염시키거나 훔치거나 접근해서는 안 되는 귀중한 애플리케이션 데이터를 보호하는 데 기여하는 모든 행동
인증, 인가 차이점
인증(authentication): 클라이언트나 서버가 자신의 정체를 입증하는 절차. 아는 것(패스워드)이나 갖고 있는 것(장치, 인증서) 또는 자기 자신(지문 같은 고유 특성)을 이용
인가(authorization): 이미 인증된 사용자가 리소스의 생성이나 조회, 갱신, 삭제 같은 작업을 수행하는 것을 허용하거나 거부하는 절차
- 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는 서비스들이 서로 인증하는 데 사용
- 9.1.2 최종 사용자 인증 End-user authentication - JWT 등 자격증명
- 최종 사용자 인증: 사용자의 개인 데이터를 저장하는 애플리케이션의 핵심
- 최종 사용자 인증 프로토콜 대부분은 사용자를 인증 서버로 리다이렉션하는 것이 핵심
- 과정
- 사용자 → 인증 서버에서 로그인 성공 → 사용자 정보를 담고 있는 자격 증명(HTTP 쿠키나 JWT 등으로 저장) 발급 받음 → 사용자가 자격 증명 서비스에 제시(인증을 위함) → 서비스에서 검증 → 접근 허용
- 서비스는 어떤 종류든 접근을 허용하기 전에 자격 증명을 발급한 인증 서버에 자격 증명을 검증
- 9.1.3 인가 Authorization - 작업 수행 승인/거부
- 인가: 사용자(호출자)가 인증된 후 진행
- 과정
- 호출자가 ‘누구’인지 서버에서 식별 → 서버는 해당 ID가 ‘어떤’ 작업을 수행할 수 있도록 허용돼 있는지 권한 확인 → 승인하거나 거부
- 예시
- 웹 애플리케이션에서 인가는 사용자가 리소스를 생성, 조회, 업데이트, 삭제할 수 있는지 여부를 확인
- Istio의 인가 기능
- 서비스 인증과 ID 모델을 기반으로 함
- 세분화된 인가 기능
- 서비스와 서비스 사이
- 최종 사용자와 서비스 사이
- 9.1.4 모놀리스와 마이크로서비스의 보안 비교 Comparison of security in monoliths and microservices
- 마이크로서비스 / 모놀리스 - 서비스 간 인증, 인가 차이점
- 모놀리스
- 상대적으로 커넥션 소수
- 보편적으로 가상머신 혹은 물리 머신 같은 더 정적인 인프라에서 실행
- 정적인 인프라에서 실행하면 (고정) IP 주소를 ID 확인 근거로 심기 좋아, 인증용 인증서에서 흔하게 사용 (네트워크 방화벽 규칙에 사용)
정적 인프라 - IP를 신뢰의 근거로 사용
- 마이크로서비스
- 커넥션 및 요청 다수 (네트워크를 오가는 보호 대상 다수)
- 정적 환경에서는 서비스 운영 불가
- 쉽게 수백, 수천 개의 서비스로 불어남
- 수많은 서버로 스케줄링되고 수명이 짧음
- 서비스가 반드시 같은 네트워크에서 실행되지 않음 (클라우드 프로바이더에 걸쳐 있거나 온프레미스에서도 실행)
- 위와 같은 이유로 IP 주소를 사용 등의 전통적인 방법들은 ID로 활용하기 적합하지 않음
마이크로서비스 - 클라우드와 온프레미스에서 실행되는 상호 연결 다수 - Istio는 ID를 제공하고자 SPIFFE specification 스펙을 사용
- SPIFFE: 고도로 동적이고 이질적인 환경에서 워크로드에 ID를 제공하기 위한 일련의 오픈소스 표준
- SPIFFE: 고도로 동적이고 이질적인 환경에서 워크로드에 ID를 제공하기 위한 일련의 오픈소스 표준
- 모놀리스
- 마이크로서비스 / 모놀리스 - 서비스 간 인증, 인가 차이점
- 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를 컨트롤 플레인이 워크로드마다 생성
- SVID를 컨트롤 플레인이 워크로드마다 생성
- SPIFFE ID
- 9.1.6 Istio 보안 요약 Istio security in a nutshell - PeerAuthentication , RequestAuthentication , AuthorizationPolicy
- Istio 보안 이해하기: Istio가 정의한 커스텀 리소스로 프록시 설정 과정 (서비스 메시 운영자 관점)
- PeerAuthentication 리소스: 서비스 간의 트래픽을 인증하도록 프록시 설정
- 인증 성공 시 - 프록시는 상대 peer의 인증서에 인코딩된 정보를 추출해 요청 인가에 사용 가능하도록 함
- RequestAuthentication 리소스: 프록시가 최종 사용자의 자격 증명을 발급 서버에 확인해 인증하도록 설정
- 인증 성공 시 - 이 역시 자격 증명에 인코딩된 정보를 추출해 요청 인가에 사용 가능하도록 함.
- AuthorizationPolicy 리소스: 앞선 두 리소스에 따라 추출한 정보를 토대로 프록시가 요청을 인가하거나 거부하도록 구성
메타데이터에서 검증된 데이터 수집 - 요청을 인증 및 인가하도록 서비스 프록시를 구성하는 리소스
- PeerAuthentication 과 RequestAuthentication 리소스: 요청을 인증하도록 프록시를 구성
- 자격 증명(SVID나 JWT)에 인코딩된 정보: 특정 시점에 추출하여 필터 메타데이터로 저장
- 필터 메타데이터: 커넥션 ID
- AuthorizationPolicy 리소스: 그 커넥션 ID에 기반해 요청을 허가할지 거부할지를 결정
요청을 인증 및 인가하도록 서비스 프록시를 구성하는 리소스 - PeerAuthentication: 서비스-to-서비스 인증 설정, 인가를 위한 피어 정보 추출
- RequestAuthentication: End-user 인증 설정, 인가를 위한 유저 정보 추출
- AuthorizationPolicy: PeerAuthentication, RequestAuthentication 에서 추출한 피어/유저 정보에 기초하여 권한 판단을 위한 인가 정책을 설정
- Istio 보안 아키텍처
- Istio CA는 키와 인증서를 관리하며, 인증서의 SAN은 SPIFFE 형식
- Istiod는 메시의 모든 사이드카에 인증 및 인가 보안 정책을 배포
- 사이드카는 Istiod에서 배포한 보안 정책에 의거하여 인증 및 인가를 강제 시행
- PeerAuthentication 리소스: 서비스 간의 트래픽을 인증하도록 프록시 설정
- Istio 보안 이해하기: Istio가 정의한 커스텀 리소스로 프록시 설정 과정 (서비스 메시 운영자 관점)
Istio 1.18 공식 문서 - Security 정리
9.2 자동 상호 TLS (Auto mTLS)
- 들어가며 : 인증서 발급/갱신 자동화, 추가 작업(인증, 인가)
- 사이드카 프록시가 주입된 서비스 사이의 트래픽은 기본적으로 암호화되고 상호 인증
- 인증서를 발급하고 로테이션하는 절차를 자동화하는 것은 매우 중요 - 역사적으로 사람이 관리 시 오류 발생
- 인적 실수로 인한 서비스 중단은 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/
- MTLS 절차 : 서버측도 클라이언트측에 대한 인증서를 확인 및 액세스 권한 확인
- TLS - 암호화방식 인증서 Handshake
- 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 리소스로 평문 트래픽 금지 가능
- 키알리 확인: 네임스페이스(default, istioinaction 선택), Show Legend 클릭 후 아이콘 확인, unknown → webapp 구간은 평문 통신
- 9.2.2 이스티오의 PeerAuthentication 리소스 이해하기
- 들어가며
- PeerAuthentication 리소스를 사용 시
- 워크로드가 mTLS를 엄격하게 요구하거나 평문 트래픽을 허용하고 받아들이게 설정할 수 있음
- 각각 STRICT 혹은 PERMISSIVE 인증 모드를 사용
- 상호 mutual 인증 모드는 다양한 범위에서 구성 가능
- Mesh-wide PeerAuthentication 정책: 서비스 메시의 모든 워크로드에 적용
- Namespace-wide PeerAuthentication 정책: 네임스페이스 내 모든 워크로드에 적용
- Workload-specific PeerAuthentication 정책: 정책에서 명시한 셀렉터에 부합하는 모든 워크로드에 적용
- 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 상호 인증을 계속 유지
- 이렇게 하면 보안이 뚫렸을 때 공격 표면을 더 좁힐 수 있음
- 좀 더 보안을 강화하기
- istioinaction 네임스페이스의 워크로드가 레거시 워크로드(sleep 서비스와 같이 메시의 일부가 아닌)로부터 평문 트래픽을 받아들이도록 허용
- 네임스페이스 범위 정책을 사용 시
- 워크로드별 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 반환) webapp 요청 시 성공 코드 반환(PERMISSIVE), catalog 직접 요청 시 실패, 상호 인증 필요 (STRICT - default)로그 확인 - istio-proxy (catalog) - 404 반환
- webapp은 성공 응답을 반환 (메시 범위 정책으로 엄격한 기본값을 적용)
- 그러나 일부 서비스(뒤처진 것들)에는 그 서비스들이 메시로 옮겨질 때까지 상호 인증이 아닌 트래픽도 허용되도록 워크로드별 정책을 사용
webapp은 HTTP 트래픽 허용. catalog 서비스에는 상호 인증 필요
istiod는 PeerAuthentication 리소스 생성을 수신하고, 이 리소스를 엔보이용 설정으로 변환하며, LDS(Listener Discovery Service)를 사용해 서비스 프록시에 적용
구성된 정책들은 들어오는 요청마다 평가됨
- webapp 만 목표로 하기 위해 워크로드 selector를 지정해 PeerAuthentication 정책을 업데이트
- 두 가지 추가적인 상호 인증 모드 TWO ADDITIONAL MUTUAL AUTHENTICATION MODES
- 대부분의 경우 STRICT 나 PERMISSIVE 모드를 사용하지만, 두 가지 모드가 더 있음
- UNSET : 부모의 PeerAuthentication 정책을 상속
- DISABLE : 트래픽을 터널링하지 않고 그냥 보냄
- 상호 인증 트래픽, 평문 트래픽 등 워크로드로 터널링할 트래픽 유형을 지정하거나,
요청을 프록시로 보내지 않고 애플리케이션으로 바로 포워딩 가능
- 대부분의 경우 STRICT 나 PERMISSIVE 모드를 사용하지만, 두 가지 모드가 더 있음
- 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
- 이 권한을 켜려면 istioctl로 속성 values.global.proxy.privileged=true 로 설정해 이스티오 설치를 업데이트해야 함
- Istio 프록시에는 기본적으로 tcpdump가 설치되어 있음
- 워크로드 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를 알고 있으므로 수행할 수 있는 작업을 정의할 수 있음을 의미
- 참가자 간 peer-to-peer 인증을 용의하게 하는 모든 구성 요소를 앞의 과정을 통해 검증함
- 들어가며
부록 C Istio 보안: SPIFFE
- 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 핸드셰이크 단계 - 클라이언트가 자신이 지원하는 TLS 버전과 암호화 수단을 포함한 ClientHello 로 핸드셰이크를 시작
- 서버는 ServerHello 와 자신의 X.509 인증서로 응답
- 인증서에는 서버의 ID 정보와 공개 키 포함
- 클라이언트는 서버의 인증서 데이터가 변조되지 않았음을 확인 후 신뢰 체인 검증
- 검증에 성공하면, 클라이언트는 서버에 비밀 키를 전송
- 비밀 키: 임의로 생성한 문자열을 서버의 공개 키로 암호화한 것
- 서버는 자신의 개인 키로 비밀 키를 복호화하고, 복호화된 비밀 키로 ‘finished’ 메시지를 암호화해 클라이언트로 전송
- 클라이언트도 비밀 키로 암호화한 ‘finished’ 메시지를 서버에 보내면 TLS 핸드셰이크 완료
- TLS 핸드셰이크의 결실은 클라이언트가 서버를 인증했고 대칭 키를 안전하게 교환했다는 것
- 대칭 키: 해당 커넥션에서 클라이언트와 서버를 오가는 트래픽을 암호화하는데 사용
- TLS 핸드셰이크 방식은 비대칭 암호화보다 성능이 더 좋음
- 최종 사용자에게 이런 절차는 브라우저가 투명하게 수행하는 것
- 주소 표시줄에 녹색 자물쇠로 표시돼 수신자가 인증됐고 트래픽이 암호화돼 수신자만 복호화할 수 있다는 것을 확인 가능
- 서버에서 최종 사용자를 인증하는 것은 구현하기 나름이며 여러 가지 방법이 있음
- 인증의 핵심은 비밀번호를 알고 있는 사용자가 세션 쿠키나 JWT(JSON Web Token)를 받는 것
- JWT는 수명이 짧고 사용자의 후속 요청을 서버에 인증하기 위한 정보를 포함하는 것이 이상적
- Istio는 JWT를 사용하는 최종 사용자 인증을 지원
- TLS 프로토콜은 TLS 핸드셰이크 절차에서 X.509 인증서를 기본 메커니즘으로 사용
- 들어가며
- 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가 쿠버네티스 서비스 어카운트를 사용해 워크로드를 식별하는 경로를 정의하는 방법을 실습에서 확인
- SPIFFE ID는 RFC 3986 호환 URI로, spiffe://trust-domain/path 형식을 따름
- 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를 가져오고 로테이션하는 데 사용
- 워크로드 API와 보안 통신을 시작하고 유지한다. 이 보안 통신은 SVID를 가져오고 로테이션하는 데 사용
- 워크로드 증명 attestation
- 워크로드에 ID를 발급하는 단계의 개요
워크로드에 ID를 발급하기 - 워크로드 엔드포인트는 워크로드의 무결성을 확인하고(즉, 워크로드 증명을 수행하고) SPIFFIE ID가 인코딩된 CSR을 생성
- 워크로드 엔드포인트는 서명을 위해 워크로드 API에 CSR을 제출
- 워크로드 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는 SVID를 X.509 인증서로 구현
- 구현 방법: SAN Subject Alternative Name 확장에 SPIFFE ID를 URI로 인코딩
- X.509 인증서 사용에 따른 이점: 워크로드가 서로 간의 트래픽을 상호 인증하고 암호화할 수 있음
자신의 SVID를 가져오고 보안 통신을 시작하는 워크로드
- Istio가 SPIFFE 사양을 구현함으로써, 모든 워크로드가 각자의 ID를 공급받고 그 ID를 증거로 인증서를 받는다는 것이 자동으로 보장됨
- 이런 인증서는 상호 인증과 모든 서비스 간 통신을 암호화하는 데 사용
- 그러므로 이 기능을 자동 상호 TLS라고 지칭
- SVID (SPIFFE 검증할 수 있는 ID 문서)
- 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를 발급하는 워크로드는 서비스 프록시
- 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 CA는 TokenReview 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 발급
- Istio 프록시 컨테이너에 서비스 어카운트 토큰 할당
- 토큰과 CSR이 istiod로 전송
- istiod는 쿠버네티스 TokenReview API로 토큰 유효성 검사
- 성공 시, 인증서에 서명하고 응답으로 제공
- 파일럿 에이전트는 Envoy SDS를 통해 Envoy가 ID를 포함한 인증서를 사용하도록 설정
- Istio가 워크로드 ID를 프로비저닝 하기 위한 SPIFFE 사양 구현 전체 과정을 알아봄
- Istio 프록시 사이드카가 주입되는 모든 워크로드에서 자동으로 수행
- Istio 프록시 사이드카가 주입되는 모든 워크로드에서 자동으로 수행
- 파일럿 에이전트는 토큰을 디코딩하고 이 페이로드 데이터를 사용해 SPIFFE ID(예 spiffe://cluster.local/ns/istioinaction/sa/default)를 생성
- 들어가며
- 요청 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 ...
- 출력하려면 로깅 수준의 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
- 출력은 RequestAuthentication 필터가 최종 사용자 토큰의 클레임을 검증했고, 클레임을 필터 메타데이터로 저장했다는 것을 확인 가능
- 기본적으로 엔보이 rbac 로거는 메타데이터를 로그에 출력하지 않음
- 3.2 한 요청의 대략적인 흐름 Overview of the flow of one request
- 워크로드가 목적지인 요청은 모두 다음 필터를 거침
필터 메타데이터에서 검증된 데이터 수집 요청을 인증 및 인가하도록 서비스 프록시를 구성하는 리소스 - JWT authentication filter 인증 필터
- 인증 정책의 JWT 사양에 따라 JWT의 유효성을 검사하고 인증 클레임과 커스텀 클레임 같은 클레임을 추출해 필터 메타데이터로 저장하는 엔보이 필터
- PeerAuthentication filter 피어인증 필터
- 서비스 인증 요구 사항을 강제하고 인증된 속성(소스 네임스페이스나 주체 같은 피어 ID)을 추출하는 엔보이 필터
- Authorization filter 인가 필터
- 앞선 필터들이 수집한 필터 메타데이터를 확인하고 워크로드에 적용된 정책에 따라 요청에 권한을 부여하는 인가 엔진
- webapp 서비스에 도달해야 하는 요청의 시나리오 - 서비스에 대한 요청이 인증되고 인가되는 방식
- 요청이 JWT 인증 필터를 통과
- 이 필터는 토큰에서 클레임을 추출해 필터 메타데이터에 저장
- 이로써 요청에 ID가 주어짐
- 이 필터는 토큰에서 클레임을 추출해 필터 메타데이터에 저장
- 인그레스 게이트웨이와 webapp 간 피어 간 인증 수행
- 피어 간 인증 필터는 클라이언트의 ID 데이터를 추출해 필터 메타데이터에 저장
- 인가 필터 실행 순서
- 커스텀 인가 필터들 : 요청을 허용하거나 거부할지 추가로 평가
- 거부 인가 필터들 : 요청을 허용하거나 거부할지 추가로 평가
- 허용 인가 필터들 : 필터 조건에 맞으면 요청을 허용
- 마지막 (포괄적) 인가 필터 : 앞서 요청을 처리한 필터가 없는 경우에만 실행
- 요청이 JWT 인증 필터를 통과
- JWT authentication filter 인증 필터
- 워크로드가 목적지인 요청은 모두 다음 필터를 거침
- 들어가며 : 필터 메타데이터 - Principal, Namespace, Request principal, Request authentication claims
9.3 서비스 간 트래픽 인가하기
- 들어가며 : Understanding request identity
- 인가(Authorization): 인증된 주체가 리소스 접근, 편집, 삭제 같은 작업을 수행하도록 허용됐는지 정의하는 절차
- 정책은 인증된 주체(’누가’)와 인가(’무엇’)를 결합해 형성되며, 누가 무슨 일을 할 수 있는지 정의
- Istio에는 AuthorizationPolicy 리소스가 존재
- 서비스 메시에 메시 범위, 네임스페이스 범위, 워크로드별 접근 정책 정의 (선언적 API)
- 인가는 공격 범위를 단지 도난당한 ID가 접근할 수 있는 것으로 축소
인가는 공격 범위를 단지 도난당한 ID가 접근할 수 있는 것으로 축소
- 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 필드는 정책을 활성화할 요청을 식별하는 규칙 목록을 정의 (복잡한 정책이므로 상세 확인 필요)
- AuthorizationPolicy 리소스 사양에서 정책을 설정하고 정의하는 필드는 세 가지
- 인가 정책 규칙 이해하기 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 필드는 규칙이 부합한 후 충족해야 하는 조건 목록을 지정
- from 필드는 요청의 출처 source 를 다음 유형 중 하나로 지정
- 공식 문서 https://istio.io/latest/docs/reference/config/security/authorization-policy/
- 각 서비스와 함께 배포되는 서비스 프록시가 인가 또는 집행 enforcement 엔진
- 작업 공간 설정하기 : 실습 환경 구성 확인 (실습)
실습 환경 구성 : 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 네임스페이스에 배포했고, 같은 네임스페이스의 인증된 워크로드로부터만 요청을 받는 중
- 실습 환경 요약
- 워크로드에 정책 적용 시 동작 확인
- 알아둬야 할 것 (모를 경우 문제 발생, 디버깅에 많은 시간 낭비)
- 워크로드에 하나 이상의 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 거부 정책을 추가하는 것을 권장
- 허용하려는 트래픽에 대해서만 생각하고, 그 트래픽용 정책만 생성하면 됨
전체 거부 정책이 '명시적으로 지정하지 않으면 요청을 거부한다' 로 바꾸는 로직
- 허용하려는 트래픽에 대해서만 생각하고, 그 트래픽용 정책만 생성하면 됨
- 들어오는 트래픽에 다른 정책이 적용되지 않을 때 활성화되는 전체 catch-all 거부 정책을 추가하는 것을 권장
- 알아둬야 할 것 (모를 경우 문제 발생, 디버깅에 많은 시간 낭비)
- 전체 정책으로 기본적으로 모든 요청 거부하기 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: - {}
- 특정 네임스페이스에서 온 요청 허용하기 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 네임스페이스의 워크로드에서 온 것인지 확인할 수 없음
- 해결 방안
- sleep 서비스에 서비스 프록시 주입하기 → 실습 진행, 권장 방식
- 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 # 거부 확인
- 특정 네임스페이스에서 시작한, 모든 서비스에 대한 트래픽을 허용하기
- 미인증 레거시 워크로드에서 온 요청 허용하기 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 서비스로의 후속 요청을 거부함
- webapp 은 sleep 서비스에서 요청을 허용했지만,
- 특정 서비스 어카운트에서 온 요청 허용하기 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 도용되더라도 피해가 가능한 한 최소 범위로 제한되도록 엄격한 인가 정책을 시행하고 있다는 것
- 정책의 조건부 적용 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에서 온다는 차이점 존재
- 이 정책은 다음 두 조건이 모두 충족될 때만 요청을 허용
- 가끔 어떤 정책은 특정 조건이 충족되는 경우에만 적용되기도 함
- 값 비교 표현식 이해하기 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로 연결되기 때문에 모든 조건 부합 필요
- 요청이 이 규칙에 해당하려면, 세 가지 속성에서 모두 부합 필요
- 인가 정책이 평가되는 순서 이해하기 Understanding the order in which authorization policies are evaluated
https://istio.io/v1.17/docs/concepts/security/#implicit-enablement - 정책 복잡성 대두 - 한 워크로드에 많은 정책이 적용되고 순서를 이해하기 어려워질 경우
- 많은 솔루션이 priority 필드를 사용해 순서를 정의
- Istio는 정책 평가에 다른 접근법 사용
- CUSTOM policies are evaluated first. CUSTOM 정책이 가장 먼저 평가됨
- 추후 외부 인가 서버와 통합할 때 CUSTOM 정책의 사례 확인 가능
- DENY policies are evaluated next. If no DENY policy is matched . . .
- DENY 정책 평가: 일치하는 DENY 정책이 없으면…
- ALLOW policies are evaluated. If one matches, the request is allowed. Otherwise. . .
- ALLOW 정책 평가: 일치하는 것이 있으면 허용된다. 그렇지 않으면…
- According to the presence or absence of a catch-all policy, we have two outcomes: 일반 정책의 존재 유무에 따라 두 가지 결과가 나타난다.
- When a catch-all policy is present, it determines whether the request is approved. 일반 정책이 존재하면, 일반 정책이 요청 승인 여부를 결정한다.
- When a catch-all policy is absent, the request is: 일반 정책이 없으면, 요청은 다음과 같다.
- Allowed if there are no ALLOW policies, or it’s ALLOW 정책이 없으면 허용된다.
- Rejected when there are ALLOW policies but none matches. ALLOW 정책이 있지만 아무것도 해당되지 않으면 거부된다.
- CUSTOM policies are evaluated first. CUSTOM 정책이 가장 먼저 평가됨
- 조건에 따라 동작이 바뀌므로, Flowchart 가 이해가 더 쉬움
- 흐름이 조금 복잡하지만, 일반 DENY 정책을 정의하면 휠씬 간단
- 요청을 거부하는 CUSTOM과 DENY 정책이 없으면, 허용할 ALLOW 정책이 있는지만 확인
9.4 최종 사용자 인증 및 인가
- 사전 지식 : 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)
- kube-apiserver로부터 TokenRequest API를 통해 얻은 서비스어카운트토큰(ServiceAccountToken).
- 서비스어카운트토큰은 기본적으로 1시간 뒤에, 또는 파드가 삭제될 때 만료된다.
- 서비스어카운트토큰은 파드에 연결되며 kube-apiserver를 위해 존재한다.
- kube-apiserver에 대한 연결을 확인하는 데 사용되는 CA 번들을 포함하는 컨피그맵(ConfigMap).
- 파드의 네임스페이스를 참조하는 DownwardA
- kube-apiserver로부터 TokenRequest API를 통해 얻은 서비스어카운트토큰(ServiceAccountToken).
- 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 접근 단계
- AuthN → AuthZ → Admisstion 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라고 부르고, 크게 MutatingWebhook 과 ValidatingWebhook 로 나뉨
- 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 Certificate의 lightweight JSON 버전
- Bearer type: 서버에서 지정한 어떠한 문자열도 입력할 수 있음 (하지만 굉장히 허술한 느낌을 받음)
- 이를 보완하고자 쿠버네티스에서 Bearer 토큰을 전송할 때 주로 JWT (JSON Web Token) 토큰을 사용
- JWT는 X.509 Certificate와 마찬가지로 private key를 이용하여 토큰을 서명하고 public key를 이용하여 서명된 메세지를 검증
- 해당 토큰이 쿠버네티스를 통해 생성된 valid한 토큰임을 인증 가능
- X.509 Certificate의 lightweight JSON 버전이라고 생각하면 편리
- jwt는 JSON 형태로 토큰 형식을 정의한 스펙이며, 쿠버네티스에서 뿐만 아니라 다양한 웹 사이트에서 인증, 권한 허가, 세션관리 등의 목적으로 사용
- 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에 의존하는 주체
- OAuth 2.0 : 권한허가 처리 프로토콜, 다른 서비스에 접근할 수 있는 권한을 획득하거나 반대로 다른 서비스에게 권한을 부여할 수 있음 - 생활코딩
- 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 및 인가를 판단할 수 있음
- 예시) 토큰이 사용자 그룹에 있는 주체에 속한다고 할 때 서비스는 이 정보를 사용해 이 주체의 접근 수준을 결정할 수 있음
- 클레임을 신뢰하려면 토큰이 검증될 수 있어야 함
- 이 데이터는 주체 subject 에 대한 클레임을 표현
- JWT는 어떻게 발행되고 검증되는가? HOW IS A JWT ISSUED AND VALIDATED?
- JWT(JSON 웹 토큰)는 인증 서버에서 발급되는데, 인증 서버는 토큰을 서명하는 비밀 키와 검증하기 위한 공개 키를 갖고 있음
- 공개 키는 JWKS JSON Web Key Set, JSON 웹 키셋 라고 하며, well-known HTTP 엔드포인트에 노출됨
- 서비스는 이 엔드포인트에서 공개 키를 가져와 인증 서버가 발급한 토큰을 검증할 수 있음
- 인증 서버 솔루션 구현
- 애플리케이션 백엔드 프레임워크에서 구현
- OpenIAM 혹은 Keycloak 등의 서비스로, 자체적으로 구현
- Auth0, Okta 등의 서비스형 ID Identity-as-a-Service 솔루션으로 구현
- JWKS는 서명을 복호화하는 데 사용하는 공개 키를 포함
- 서명을 복호화하고 토큰 데이터의 해시값과 비교하는데, 일치하면 토큰 클레임을 신뢰 가능
- 서버가 토큰을 검증할 때 JWKS 사용 방법
서버는 클라이언트가 제시한 토큰을 검증하기 위해 JWKS를 가져옴 - 인증서버는 “토큰 서명”을 위한 private key 와 “토큰 검증”을 위한 public key를 가지고 있음
- 인증서버에서 private key 로 서명한 JWT (JSON Web Token) 을 발급
- 인증서버의 public key 는 JWKS (JSON Web Key Set) 형태의 HTTP 엔드포인트로 제공
- 서비스는 인증서버에서 발급된 JWT 를 검증하기 위해 필요한 public key 를 JWKS 에서 찾음
- public key 로 JWT 서명을 복호화 하여 얻은 해시값과 JWT 토큰 데이터의 해시값을 비교
- 해시값이 동일할 경우 토큰 claim에 변조가 없었음을 보장하므로 신뢰할 수 있음
- 인그레스 게이트웨이에서의 최종 사용자 인증 및 인가 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
- 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
- 유효한 JWT로 요청
- 유효하지 않은 발행자의 토큰이 있는 요청은 거부된다 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 - -
- 유효하지 않은 JWT로 요청
- 토큰이 없는 요청은 클러스터로 받아들여진다 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
- 응답 코드 요청이 클러스터로 받아들여짐
- 응답 코드 요청이 클러스터로 받아들여짐
- 토큰이 없는 요청은 거부될 것으로 예상하였으나 받아들여짐 (!!)
- 실제로는 애플리케이션의 프론트엔드에 서비스를 제공하는 등 요청에 토큰이 없는 시나리오가 많음
- 이런 이유로, 토큰이 없는 요청을 거부하려면 약간의 추가 작업이 필요 (다음에 설명)
- 토큰 없이 curl 요청 실행
- 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가 없는 요청 거부하려면 명시적으로 거부하는 AuthorizationPolicy 리소스 생성 필요
- 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 에 새 아이템을 만들 수 있도록 허용하고 있음을 확인
- 유저별로 다른 접근 정책 설정
- 들어가며
9.5 커스텀 외부 인가 서비스와 통합하기
- 들어가며 : 외부 인가 서비스 호출
- Istio는 Envoy의 기본 RBAC 기능을 사용해 인가를 구현
- 인가에 좀 더 정교한 커스텀 메커니즘이 필요할 경우: 외부 인가 서비스를 호출하도록 Istio의 서비스 프록시 설정 가능
외부 서버에서 요청을 인가받도록 CUSTOM 정책 사용하기 - 그림 9.13에서 서비스 프록시에 들어온 요청은 프록시가 외부 인가(ExtAuthz) 서비스를 호출하는 동안 잠시 멈춤
- 외부 인가 서비스는 애플리케이션 사이드카로 메시 안에 존재하거나 메시 바깥에 존재할 수 있음
- 외부 인가는 엔보이의 CheckRequest API를 구현해야 함 - Code
- 이 API를 구현하는 외부 인가 서비스의 예를 들면 다음과 같음
- Open Policy Agent (https://www.openpolicyagent.org/docs/latest/envoy-tutorial-istio)
- Signal Sciences (www.signalsciences.com/blog/integrations-envoy-proxy-support)
- Gloo Edge Ext Auth (https://docs.solo.io/gloo-edge/latest/guides/security/auth/extauth)
- Istio sample Ext Authz (https://github.com/istio/istio/tree/release-1.9/samples/extauthz)
- 외부 인가 서비스는 프록시가 인가를 집행하는 데 사용하는 ‘허용’이나 ‘거부’ 메시지를 반환
- ExtAuthz performance tradeoffs 외부 인가 성능 트레이드오프
- 요청 경로 중에 외부 인가 서비스를 호출하기 때문에 이 방법을 사용할 때는 지연 시간 증가에 대비해야 함
- Istio 내장 인가 기능은 대체로 충분하고 유연하게 작동하지만, 완벽히 통제해야 한다면 외부 인가 서비스를 호출하면서 생기는 성능 트레이드오프 평가 필요
- 외부 인가 서비스를 애플리케이션 사이드카로 배포해 네트워크 오버헤드 최소화 가능
https://istio.io/latest/docs/tasks/security/authorization/authz-custom/
- 외부 인가 실습 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 인지만 검사
- 이 헤더가 요청에 들어 있으면 요청은 허용되고, 들어 있지 않으면 요청은 거부
- 요청의 다른 속성을 평가하도록 외부 인가 서비스를 직접 작성하거나, 기존 서비스 중 하나를 골라 사용 가능
- 실습 환경 초기화
- 이스티오에 외부 인가 설정하기 Configuring Istio for ExtAuthz
- Istio가 새로운 외부 인가 서비스를 인식하도록 설정
- istio-system 네임스페이스의 istio configmap 설정 변경
- meshconfig 설정에서 extensionProviders 설정
- istio-system 네임스페이스의 istio configmap 설정 변경
- configmap 수정으로 새 외부 인가 서비스 설정 추가
# includeHeadersInCheck (DEPRECATED) KUBE_EDITOR="nano" kubectl edit -n istio-system cm istio -------------------------------------------------------- ... extensionProviders: - name: "sample-ext-authz-http" envoyExtAuthzHttp: service: "ext-authz.istioinaction.svc.cluster.local" port: "8000" includeRequestHeadersInCheck: ["x-ext-authz"] ... -------------------------------------------------------- # 확인 kubectl describe -n istio-system cm istio
- Istio가 envoyExtAuthz 서비스의 HTTP 구현체인 새 확장 sample-ext-authz-http 를 인식하도록 설정함
- 이 서비스는 ext-authz.istioinaction.svc.cluster.local 에 위치하는 것으로 정의 (앞 실습의 쿠버네티스 서비스에 맞춘 것)
- 외부 인가 서비스에 전달할 헤더를 구성할 수 있는데, 이 설정에서는 x-ext-authz 헤더를 전달
- 예제 외부 인가 서비스에서는 이 헤더를 인가 결과를 결정하는 데 사용
- 외부 인가 기능을 사용하기 위한 마지막 단계
- 이 기능을 사용하도록 AuthorizationPolicy 리소스를 설정하는 것
- Istio가 envoyExtAuthz 서비스의 HTTP 구현체인 새 확장 sample-ext-authz-http 를 인식하도록 설정함
- Istio가 새로운 외부 인가 서비스를 인식하도록 설정
- 커스텀 AuthorizationPolicy 리소스 사용하기 Using a custom AuthorizationPolicy resource
- 앞의 실습에서는 action 이 DENY 혹은 ALLOW 인 AuthorizationPolicy 리소스 생성
- action 이 CUSTOM 인 AuthorizationPolicy 생성 후 외부 인가 서비스 지정해보기
# 아래 AuthorizationPolicy 는 istioinaction 네임스페이스에 webapp 워크로드에 적용되며, # sample-ext-authz-http 이라는 외부 인가 서비스에 위임한다. cat << EOF | kubectl apply -f - apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: ext-authz namespace: istioinaction spec: selector: matchLabels: app: webapp action: CUSTOM # custom action 사용 provider: name: sample-ext-authz-http # meshconfig 이름과 동일해야 한다 rules: - to: - operation: paths: ["/*"] # 인가 정책을 적용할 경로 EOF # kubectl get AuthorizationPolicy -A NAMESPACE NAME AGE istioinaction ext-authz 98s
- 호출 확인
# docker exec -it myk8s-control-plane istioctl proxy-config log deploy/webapp -n istioinaction --level rbac:debug kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f kubectl logs -n istioinaction -l app=ext-authz -c ext-authz -f # 헤더 없이 호출 kubectl -n default exec -it deploy/sleep -- curl webapp.istioinaction/api/catalog denied by ext_authz for not found header `x-ext-authz: allow` in the request kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f 2025-05-04T08:33:04.765006Z debug envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:114 checking request: requestedServerName: , sourceIP: 10.10.0.18:55834, directRemoteIP: 10.10.0.18:55834, remoteIP: 10.10.0.18:55834,localAddress: 10.10.0.20:8080, ssl: none, headers: ':authority', 'webapp.istioinaction' ':path', '/api/catalog' ':method', 'GET' ':scheme', 'http' 'user-agent', 'curl/8.5.0' 'accept', '*/*' 'x-forwarded-proto', 'http' 'x-request-id', 'ffd44f00-19ff-96b7-868b-8f6b09bd447d' , dynamicMetadata: thread=31 2025-05-04T08:33:04.765109Z debug envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:130 shadow denied, matched policy istio-ext-authz-ns[istioinaction]-policy[ext-authz]-rule[0]thread=31 2025-05-04T08:33:04.765170Z debug envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:167 no engine, allowed by default thread=31 [2025-05-04T08:33:04.764Z] "GET /api/catalog HTTP/1.1" 403 UAEX ext_authz_denied - "-" 0 76 5 4 "-" "curl/8.5.0" "ffd44f00-19ff-96b7-868b-8f6b09bd447d" "webapp.istioinaction" "-" inbound|8080|| - 10.10.0.20:8080 10.10.0.18:55834 - - kubectl logs -n istioinaction -l app=ext-authz -c ext-authz -f 2025/05/04 08:35:26 [HTTP][denied]: GET webapp.istioinaction/api/catalog, headers: map[Content-Length:[0] X-B3-Parentspanid:[58148c96f61496a3] X-B3-Sampled:[1] X-B3-Spanid:[960b8d911e81c217] X-B3-Traceid:[ce6c5622c32fd238a934fbf1aa4a9de0] X-Envoy-Expected-Rq-Timeout-Ms:[600000] X-Envoy-Internal:[true] X-Forwarded-Client-Cert:[By=spiffe://cluster.local/ns/istioinaction/sa/default;Hash=491c5bf23be281a5c0c2e798eba242461dfdb7b178d4a4cd842f9eedb05ae47d;Subject="";URI=spiffe://cluster.local/ns/istioinaction/sa/webapp] X-Forwarded-For:[10.10.0.20] X-Forwarded-Proto:[https] X-Request-Id:[964138e3-d955-97c9-b9a5-dfc88cc7f9c5]], body: [] # 헤더 적용 호출 kubectl -n default exec -it deploy/sleep -- curl -H "x-ext-authz: allow" webapp.istioinaction/api/catalog kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f 2025-05-04T08:37:40.618775Z debug envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:114 checking request: requestedServerName: , sourceIP: 10.10.0.18:36150, directRemoteIP: 10.10.0.18:36150, remoteIP: 10.10.0.18:36150,localAddress: 10.10.0.20:8080, ssl: none, headers: ':authority', 'webapp.istioinaction' ':path', '/api/catalog' ':method', 'GET' ':scheme', 'http' 'user-agent', 'curl/8.5.0' 'accept', '*/*' 'x-ext-authz', 'allow' 'x-forwarded-proto', 'http' 'x-request-id', 'b446ddf8-fb2e-9dd7-ba01-6e31fac717da' , dynamicMetadata: thread=30 2025-05-04T08:37:40.618804Z debug envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:130 shadow denied, matched policy istio-ext-authz-ns[istioinaction]-policy[ext-authz]-rule[0] thread=30 2025-05-04T08:37:40.618816Z debug envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:167 no engine, allowed by default thread=30 [2025-05-04T08:37:40.622Z] "GET /items HTTP/1.1" 200 - via_upstream - "-" 0 502 2 2 "-" "beegoServer" "b446ddf8-fb2e-9dd7-ba01-6e31fac717da" "catalog.istioinaction:80" "10.10.0.19:3000" outbound|80||catalog.istioinaction.svc.cluster.local 10.10.0.20:60848 10.200.1.165:80 10.10.0.20:45874 - default [2025-05-04T08:37:40.618Z] "GET /api/catalog HTTP/1.1" 200 - via_upstream - "-" 0 357 6 4 "-" "curl/8.5.0" "b446ddf8-fb2e-9dd7-ba01-6e31fac717da" "webapp.istioinaction" "10.10.0.20:8080" inbound|8080|| 127.0.0.6:43721 10.10.0.20:8080 10.10.0.18:36150 - default kubectl logs -n istioinaction -l app=ext-authz -c ext-authz -f 2025/05/04 08:36:34 [HTTP][allowed]: GET webapp.istioinaction/api/catalog, headers: map[Content-Length:[0] X-B3-Parentspanid:[f9bc85c800aaaa05] X-B3-Sampled:[1] X-B3-Spanid:[bf6cc58161f7ca25] X-B3-Traceid:[af1c826a362ce0382e219cd21afe1fe7] X-Envoy-Expected-Rq-Timeout-Ms:[600000] X-Envoy-Internal:[true] X-Ext-Authz:[allow] X-Forwarded-Client-Cert:[By=spiffe://cluster.local/ns/istioinaction/sa/default;Hash=491c5bf23be281a5c0c2e798eba242461dfdb7b178d4a4cd842f9eedb05ae47d;Subject="";URI=spiffe://cluster.local/ns/istioinaction/sa/webapp] X-Forwarded-For:[10.10.0.20] X-Forwarded-Proto:[https] X-Request-Id:[c9b43ce7-25d4-94ae-b684-1565ad36f533]], body: []
- 결론
- PeerAuthentication 은 피어 간 인증을 정의하는 데 사용하며, 엄격한 인증 요구 사항을 적용하면 트래픽이 암호화돼 도청할 수 없음
- PERMISSIVE 정책은 이스티오 워크로드가 암호화된 트래픽과 평문 트래픽을 모두 수용할 수 있게 해서 다운타임 없이 천천히 마이그레이션할 수 있도록 함
- AuthorizationPolicy 는 워크로드 ID 인증서나 최종 사용자 JWT에서 추출한 검증 가능한 메타데이터를 근거로 서비스 사이의 요청이나 최종 사용자의 요청을 인가(허용, 차단)하는 데 사용
- RequestAuthentication 은 JWT가 포함된 최종 사용자 요청을 인증하는 데 사용
- AuthorizationPolicy 에서 CUSTOM action을 사용하면 외부 인가 서비스를 통합 가능
- PeerAuthentication 은 피어 간 인증을 정의하는 데 사용하며, 엄격한 인증 요구 사항을 적용하면 트래픽이 암호화돼 도청할 수 없음
- 앞의 실습에서는 action 이 DENY 혹은 ALLOW 인 AuthorizationPolicy 리소스 생성