EKS 클러스터에 대한 API 서버 요청 시 계정 미소유의 소스 IP 주소로 GetCallerIdentity가 호출되는 이유는 무엇인가요?
EKS 클러스터에 대해서 쿠버네티스 API 서버 요청을 수행할 경우, 계정이 소유하지 않은 퍼블릭 IP 주소 소스가 GetCallerIdentity 이벤트에 기록되는 원인을 분석합니다.
현상
kubectl 등의 쿠버네티스 클라이언트로 EKS 클러스터의 쿠버네티스 API 서버(kube-apiserver)에 요청을 전달하고 CloudTrail 이벤트 기록을 조회하면, API 서버 요청을 전달한 IAM 사용자/역할로 호출된 GetCallerIdentity 이벤트가 CloudTrail에 기록됩니다. 이러한 유형의 GetCallerIdentity 이벤트는 쿠버네티스 클라이언트(IAM 사용자/역할)가 직접 GetCallerIdentity API를 호출한 적이 없음에도 기록되며, 해당 이벤트의 소스 IP 주소(sourceIPAddress)는 사용자 계정이 소유하지 않은 퍼블릭 IP 주소입니다.
예를 들어 CloudShell에서 kubectl get nodes를 호출하면, CloudTrail 이벤트 기록에 GetCallerIdentity 이벤트가 조회되며, userIdentity는 AWS 웹 콘솔에 로그인한 IAM 사용자/역할로, sourceIPAddress는 미상의 퍼블릭 IP 주소로 기록됩니다. 해당 퍼블릭 IP 주소는 탄력적 IP(EIP), 탄력적 네트워크 인터페이스(ENI) 등으로 조회되지 않는 계정 미소유의 IP 주소입니다.
# CloudShell aws eks update-kubeconfig --name 클러스터명 kubectl get nodes
CloudTrail에 기록된 GetCallerIdentity 이벤트 예시는 아래와 같습니다.
{ "eventVersion": "1.11", "userIdentity": { "type": "AssumedRole", "principalId": "액세스키:SESSION_NAME", "arn": "arn:aws:sts::계정ID:assumed-role/역할명/세션명", "accountId": "계정ID", "accessKeyId": "액세스키", "sessionContext": { "sessionIssuer": { "type": "Role", "principalId": "액세스키", "arn": "arn:aws:iam::계정ID:role/역할명", 쿠버네티스 API 서버 요청자(클라이언트)의 IAM 사용자/역할 ARN "accountId": "계정ID", "userName": "사용자명" }, "attributes": { "creationDate": "20**-**-**T**:**:**Z", "mfaAuthenticated": "false" } } }, "eventTime": "20**-**-**T**:**:**Z", "eventSource": "sts.amazonaws.com", "eventName": "GetCallerIdentity", "awsRegion": "리전명", "sourceIPAddress": "3.38.216.95", AWS 계정 미소유의 퍼블릭 IP 주소 "userAgent": "aws-sdk-go/1.55.8 (go1.26.5; linux; amd64)", "requestParameters": null, "responseElements": null, "additionalEventData": { "ExtendedRequestId": "******************************", "RequestDetails": { "endpointType": "regional", "awsServingRegion": "리전명" } }, "requestID": "********-****-****-****-************", "eventID": "********-****-****-****-************", "readOnly": true, "eventType": "AwsApiCall", "managementEvent": true, "recipientAccountId": "계정ID", "eventCategory": "Management", "tlsDetails": { "tlsVersion": "TLSv1.3", "cipherSuite": "TLS_AES_128_GCM_SHA256", "clientProvidedHostHeader": "sts.리전명.amazonaws.com", "keyExchange": "**************" }, "sessionCredentialFromConsole": "true" }
원인
EKS 컨트롤 플레인의 IAM 인증 방식
EKS 모범 사례에 설명된 바와 같이, EKS 컨트롤 플레인은 AWS가 관리하는 별도의 내부 계정, 별도의 내부 VPC에서 최소 2개 이상의 마스터 인스턴스로 운영됩니다. API 서버, ETCD 등의 쿠버네티스 핵심 컴포넌트는 마스터 인스턴스에서 실행되며, 해당 AWS 리소스를 사용자가 직접 조회하거나 제어할 수는 없습니다.
한편, EKS 클러스터는 쿠버네티스 API 서버 인증을 위해 IAM Authenticator (aws-iam-authenticator)를 사용합니다. 쿠버네티스 클라이언트가 API 서버에 요청을 전송할 때 첨부하는 토큰은 미리 서명된(presigned) GetCallerIdentity 요청 URL(sts.리전.amazonaws.com)의 인코딩 값을 포함합니다. 마스터 인스턴스는 이 토큰을 수신받아 인코딩된 URL로 STS(Security Token Service)에 직접 GetCallerIdentity를 호출해 토큰을 전송한 쿠버네티스 클라이언트가 어떤 IAM 사용자/역할인지 검증합니다.
쿠버네티스 클라이언트(IAM 사용자/역할)가 IAM Authenticator에게 전달할 쿠버네티스 토큰을 생성/처리하는 작업 순서는 일반적으로 아래와 같습니다. aws eks get-token CLI 명령을 사용하면 이러한 토큰 생성/처리 작업이 자동으로 수행되기 때문에 쿠버네티스 클라이언트가 토큰을 직접 생성할 필요가 없습니다.
-
쿠버네티스 클라이언트(IAM 사용자/역할)의 AWS Credentials를 조회합니다.
-
AWS Credentials를 URL 쿼리 파라미터에 포함하여 GetCallerIdentity 작업에 대한 미리 서명된(presigned) URL을 생성합니다. (AWS Signature Version 4)
https://sts.리전.amazonaws.com ?Action=GetCallerIdentity &Version=2011-06-15 &X-Amz-Algorithm=AWS4-HMAC-SHA256 &X-Amz-Credential=********%2F********%2F********%2Fsts%2Faws4_request &X-Amz-Date=********** &X-Amz-Expires=60 &X-Amz-SignedHeaders=host%3Bx-k8s-aws-id &X-Amz-Security-Token=******** &X-Amz-Signature=********
- Base64URLSafe 방식으로 미리 서명된(presigned) URL을 인코딩합니다. 인코딩한 URL의 마지막에서 "=" 문자열(패딩)을 제거합니다. 이후 URL 앞에 k8s-aws-v1. 접두사를 추가하여 쿠버네티스 토큰을 완성합니다.
# 쿠버네티스 토큰 최종 형태
k8s-aws-v1.****************************************
- 쿠버네티스 API 서버 요청 시 Authorization 헤더에 "Bearer 토큰"을 추가하여 쿠버네티스 토큰을 전달하면, 이를 전달받은 IAM Authenticator가 토큰에 포함된 URL을 디코딩하여 GetCallerIdentity를 호출하고 IAM 사용자/역할 정보가 검증됩니다.
# 쿠버네티스 API 서버 요청 헤더
{
'Authorization': 'Bearer k8s-aws-v1.****************************************'
}
즉, 쿠버네티스 API 서버 요청이 이루어지면 쿠버네티스 클라이언트의 IAM 정보를 획득할 수 있는 GetCallerIdentity 요청 URL이 토큰을 통해 마스터 인스턴스에 전달되고, 마스터 인스턴스는 토큰에 포함된 GetCallerIdentity 요청 URL을 사용해 IAM 정보를 획득하고 검증합니다. 이와 같은 작업으로 인해 CloudTrail에 GetCallerIdentity 이벤트가 기록되는 것이며, userIdentity는 IAM 정보를 확인해야 하는 쿠버네티스 클라이언트(요청자), sourceIPAddress는 GetCallerIdentity API를 실제로 호출하는 마스터 인스턴스의 NAT 퍼블릭 IP 주소로 기록됩니다.
| 작업 순서 | 쿠버네티스 클라이언트(IAM 사용자/역할) | 쿠버네티스 API 서버(마스터 인스턴스) |
|---|---|---|
| 01 | GetCallerIdentity 요청 URL 인코딩 | - |
| 02 | 인코딩된 URL을 포함해 토큰 생성 | - |
| 03 | 토큰을 요청 헤더에 포함해 API 서버에 전달 | - |
| 04 | - | 요청 헤더에서 토큰 획득 |
| 05 | - | 토큰에서 인코딩된 URL 획득 |
| 06 | - | 디코딩을 통해 GetCallerIdentity 요청 URL 획득 |
| 07 | - | 요청 URL로 GetCallerIdentity 호출 |
| 08 | - | GetCallerIdentity 응답 결과를 통해 쿠버네티스 클라이언트의 IAM 사용자/역할 정보 획득 |
| 09 | - | 클라이언트 IAM 사용자/역할에 충분한 쿠버네티스 작업 권한이 있는지 검증 (RBAC) |
| 10 | - | 검증 결과에 따라 클라이언트 IAM 사용자/역할이 요청한 쿠버네티스 작업 수행/거부 |
토큰 디코딩으로 GetCallerIdentity 요청 URL 확인하기
쿠버네티스 API 서버 인증에 사용되는 토큰이 GetCallerIdentity 요청 URL을 인코딩한 데이터라는 사실은 다음과 같이 직접 토큰을 디코딩하여 확인할 수 있습니다.
TOKEN=$(aws eks get-token --query status.token --output text --cluster-name 클러스터명) URL=$(echo "$TOKEN" | cut -d'.' -f2 | awk '{ext=length($0)%4; if(ext==1) $0=$0"==="; else if(ext==2) $0=$0"=="; else if(ext==3) $0=$0"="; print $0}' | tr '_-' '/+' | base64 --decode) # 토큰 디코딩 결과 출력 echo $URL # GetCallerIdentity 호출하여 쿠버네티스 클라이언트(요청자) IAM 정보 확인 curl -H "x-k8s-aws-id: 클러스터명" $URL
위 명령을 실행하면 아래와 같이 쿠버네티스 클라이언트(요청자)의 IAM 정보를 포함한 GetCallerIdentity 응답을 확인할 수 있습니다.
<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/"> <GetCallerIdentityResult> <Arn>arn:aws:sts::123456789012:assumed-role/example-role/example-session</Arn> <UserId>**************:example-session</UserId> <Account>123456789012</Account> </GetCallerIdentityResult> <ResponseMetadata> <RequestId>********-****-****-****-*************</RequestId> </ResponseMetadata> </GetCallerIdentityResponse>
노드 상태 점검
IAM 사용자/역할의 쿠버네티스 API 서버 요청과 무관하게, API 서버가 각 노드의 상태를 주기적으로 점검할 때에도 동일한 원리로 GetCallerIdentity 이벤트가 기록됩니다. 이 경우 노드가 Lease 리소스를 업데이트하기 위해 쿠버네티스 API 서버 요청을 수행하기 때문에 GetCallerIdentity 이벤트의 userIdentity에는 노드의 인스턴스 ID가, sourceIPAddress에는 마스터 인스턴스의 NAT 퍼블릭 IP 주소가 기록됩니다. 즉, IAM 사용자/역할이 쿠버네티스 API 요청을 명시적으로 수행하지 않더라도 각 노드의 정기적인 상태 점검(Lease 리소스 업데이트)으로 인해 GetCallerIdentity 이벤트가 계속 기록되고, 계정 미소유의 퍼블릭 IP 주소가 sourceIPAddress로 기록됩니다.
퍼블릭 IP 주소 검증 방법
CloudTrail에 기록되는 퍼블릭 IP 주소가 실제로 AWS 소유인지, 어떤 서비스에 속하는지는 ip-ranges.json을 통해 확인할 수 있습니다.
curl -O https://ip-ranges.amazonaws.com/ip-ranges.json jq -r '.prefixes[] | select(.service=="EC2") | .ip_prefix' < ip-ranges.json
NAT 게이트웨이 리소스는 내부적으로 EC2 서비스(ec2.amazonaws.com)로 분류되기 때문에, 컨트롤 플레인 소속의 마스터 인스턴스가 사용하는 NAT 퍼블릭 IP는 항상 EC2 서비스의 CIDR 범위에 포함됩니다.
특정 리전으로 한정해 전체 목록을 조회하려면 리전과 서비스를 함께 필터링할 수 있습니다.
jq -r '.prefixes[] | select(.region=="리전") | select(.service=="EC2") | .ip_prefix' < ip-ranges.json
관련 콘텐츠
AWS 공식업데이트됨 일 년 전