- Published on
EKS에서 AWS SDK의 IMDS 접근을 확실히 차단하는 법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
EKS에서 애플리케이션이 AWS SDK를 통해 자격 증명(credential)을 얻는 과정은 보통 IRSA(IAM Roles for Service Accounts) 를 기대합니다. 그런데 운영 중 종종 다음과 같은 “불길한 징후”를 만납니다.
- IRSA 설정을 했는데도 특정 Pod/컨테이너가 IMDS(Instance Metadata Service, 169.254.169.254) 로 요청을 보냄
- 결과적으로 노드 인스턴스 프로파일(노드 IAM Role) 의 권한을 가져가 버려 권한 경계가 무너짐
- CloudTrail에 “의도치 않은” 권한으로 API 호출이 찍히거나, 보안 감사에서 IMDS 접근이 발견됨
이 글은 “왜 SDK가 IMDS로 떨어지는지”를 짚고, EKS에서 IMDS 접근을 차단하는 가장 현실적인 방법을 Pod 레벨부터 노드/플랫폼 레벨까지 정리합니다. (정답은 하나가 아니라, 환경/제약에 따라 조합이 달라집니다.)
관련해서 EKS에서 권한/IRSA 문제를 10분 안에 진단하는 글도 함께 보면 좋습니다: EKS ExternalSecret 미동작 - IRSA·KMS·권한 10분 진단
문제의 본질: AWS SDK Credential Provider Chain
대부분의 AWS SDK는 다음과 같은 순서(간소화)로 자격 증명을 탐색합니다.
- 환경 변수:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN - 공유 설정/크리덴셜 파일
- 웹 아이덴티티(예: IRSA):
AWS_ROLE_ARN,AWS_WEB_IDENTITY_TOKEN_FILE - ECS/컨테이너 메타데이터
- EC2 IMDS(169.254.169.254)
IRSA가 “항상” 우선되려면, 컨테이너에 웹 아이덴티티 관련 환경 변수/토큰 파일이 정상 주입되고 SDK가 이를 인식해야 합니다. 하지만 아래 같은 이유로 체인이 끝까지 내려가 IMDS를 두드립니다.
- 서비스어카운트에 role annotation이 없거나, 잘못된 SA를 사용
automountServiceAccountToken: false또는 토큰 마운트 경로가 꼬임- SDK 버전이 오래되어 IRSA(웹 아이덴티티)를 제대로 지원하지 않음
- 앱 코드/라이브러리에서 특정 provider(IMDS 등)를 강제
- initContainer/sidecar 등 일부 컨테이너만 IRSA 환경이 누락
- 특정 언어 런타임에서 “기본 체인” 대신 다른 로직을 사용
핵심은 두 가지입니다.
- 원인 제거(= IRSA가 확실히 동작하게 만들기)
- 방어선 추가(= 그래도 IMDS로 못 가게 네트워크/노드 레벨에서 차단)
운영에서는 둘 다 필요합니다.
1단계: 먼저 IRSA가 정말 적용됐는지 확인
IMDS를 차단하기 전에, “왜 IMDS로 떨어졌는지”를 빠르게 확인해야 합니다. 다음은 최소 점검 체크리스트입니다.
서비스어카운트와 Pod 연결 확인
kubectl -n <ns> get pod <pod> -o jsonpath='{.spec.serviceAccountName}{"\n"}'
kubectl -n <ns> get sa <sa> -o yaml | sed -n '1,120p'
서비스어카운트에 다음과 같은 annotation이 있어야 합니다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp
namespace: prod
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/prod-myapp-irsa
Pod에 IRSA 환경 변수가 주입됐는지 확인
kubectl -n <ns> exec -it <pod> -- env | egrep 'AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION|AWS_DEFAULT_REGION'
kubectl -n <ns> exec -it <pod> -- ls -l /var/run/secrets/eks.amazonaws.com/serviceaccount/
정상이라면 보통 아래가 보입니다.
AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
SDK가 IMDS를 치는지 재현/관측
컨테이너에서 직접 IMDS로의 연결 가능 여부를 확인합니다.
kubectl -n <ns> exec -it <pod> -- sh -lc 'curl -sS --max-time 1 http://169.254.169.254/latest/meta-data/ || echo blocked'
여기서 응답이 오면(blocked가 아니면) “IMDS로 갈 수 있는 네트워크 경로가 열려 있다”는 뜻입니다. 이제 차단을 설계합니다.
2단계: Pod 단위로 IMDS(169.254.169.254) 이그레스 차단
가장 선호되는 방식은 Kubernetes NetworkPolicy로 Pod egress를 제한하는 것입니다. 단, 전제가 있습니다.
- 클러스터 CNI가 NetworkPolicy를 지원해야 함
- 예: Calico, Cilium 등
- AWS VPC CNI 단독은 기본 NetworkPolicy가 제한적이거나 별도 구성이 필요합니다(환경에 따라 다름)
(권장) NetworkPolicy로 169.254.169.254 차단
아래 정책은 “기본은 허용하되, IMDS만 차단”이 아니라 Kubernetes NetworkPolicy 특성상 allow-list 형태로 구성하는 경우가 많습니다. 운영에서는 보통 다음 두 패턴 중 하나를 씁니다.
- 패턴 A: 네임스페이스/워크로드 egress를 전반적으로 제한하고 필요한 목적지만 허용
- 패턴 B: Cilium 같은 CNI의 확장 기능로 특정 IP만 deny
Cilium을 쓴다면 CiliumNetworkPolicy로 IMDS만 정확히 deny하기가 쉽습니다. 예시는 다음과 같습니다.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: deny-imds
namespace: prod
spec:
endpointSelector:
matchLabels:
app: myapp
egressDeny:
- toCIDR:
- 169.254.169.254/32
Calico를 쓴다면 GlobalNetworkPolicy/NetworkPolicy로 deny 규칙을 구성할 수 있습니다(환경별 CRD/버전에 따라 문법이 달라질 수 있음).
> 포인트: “IMDS만 차단”이 가능한 CNI를 쓰면 가장 깔끔합니다. 그렇지 않다면 egress allow-list로 전환하는 과정이 필요합니다.
테스트
정책 적용 후 같은 curl 테스트를 반복합니다.
kubectl -n prod exec -it deploy/myapp -- sh -lc 'curl -sS --max-time 1 http://169.254.169.254/latest/meta-data/ || echo blocked'
blocked가 찍히면 성공입니다.
3단계: 노드 레벨에서 IMDS 접근을 차단(가장 강력한 방어선)
NetworkPolicy가 없거나(혹은 전면 egress 제한이 부담스럽거나), “어떤 Pod가 떠도 IMDS는 무조건 금지”가 목표라면 노드 레벨 차단이 강력합니다.
노드 레벨 차단은 크게 두 가지 축이 있습니다.
- IMDS 자체를 잠그기(EC2 설정)
- 노드에서 169.254.169.254로의 라우팅/iptables 차단
3-1) EC2 IMDSv2 강제 + Hop limit 축소
EKS 노드가 EC2라면 Launch Template/ASG에서 IMDS 설정을 조정할 수 있습니다.
HttpTokens=required(IMDSv2 강제)HttpPutResponseHopLimit=1
특히 Hop limit=1은 컨테이너 네트워크를 통해 IMDS가 “홉을 넘어” 접근되는 것을 줄이는 데 도움이 됩니다. 다만 네트워킹 구현/경로에 따라 결과가 달라질 수 있어, 반드시 실제 Pod에서 curl로 검증해야 합니다.
AWS CLI 예시(인스턴스 단위 수정):
aws ec2 modify-instance-metadata-options \
--instance-id i-xxxxxxxx \
--http-tokens required \
--http-put-response-hop-limit 1
운영에서는 인스턴스 단위 변경보다 Launch Template에 반영해 새 노드부터 일관되게 적용하는 편이 안전합니다.
3-2) iptables로 169.254.169.254/32 드롭
노드에서 IMDS로 나가는 트래픽을 강제로 끊는 방식입니다. 단, EKS의 노드 OS(예: Bottlerocket)나 관리 방식에 따라 적용 방법이 달라집니다.
일반적인 리눅스 노드에서의 개념 예시는 다음과 같습니다.
sudo iptables -I OUTPUT -d 169.254.169.254/32 -j REJECT
sudo iptables -I FORWARD -d 169.254.169.254/32 -j REJECT
주의할 점:
- 노드 재부팅/교체 시 규칙이 사라질 수 있어 부트스트랩(user-data) 또는 DaemonSet으로 영속화해야 합니다.
- 잘못된 체인/우선순위로 넣으면 예기치 않은 네트워크 장애를 만들 수 있습니다.
- 관리형/불변 OS(Bottlerocket 등)에서는 별도 메커니즘이 필요합니다.
Bottlerocket을 쓰는 환경이라면 노드 상태/로그 수집과 함께 변경 전략을 세우는 게 좋습니다: EKS Bottlerocket 노드 NotReady일 때 로그 수집법
4단계: 애플리케이션/SDK 레벨에서 “IMDS 비활성화” 옵션 사용
네트워크로 막는 것이 가장 확실하지만, 앱 레벨에서도 “IMDS를 아예 시도하지 않게” 만들 수 있습니다. 특히 SDK가 IMDS 타임아웃을 길게 잡고 있으면, 장애 시 지연(latency) 원인이 되기도 합니다.
공통: AWS_EC2_METADATA_DISABLED
많은 SDK에서 다음 환경 변수로 IMDS 사용을 끌 수 있습니다.
env:
- name: AWS_EC2_METADATA_DISABLED
value: "true"
이 설정은 “IMDS로 떨어지는 것” 자체를 예방하고, 실패 시 빠르게 다음 provider로 넘어가게 합니다.
Java SDK v2 예시
Java에서는 시스템 프로퍼티/환경 변수로 IMDS를 끄거나, credential provider를 명시하는 방식이 안전합니다.
import software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
import software.amazon.awssdk.services.s3.S3Client;
public class App {
public static void main(String[] args) {
var creds = WebIdentityTokenFileCredentialsProvider.create();
var s3 = S3Client.builder()
.credentialsProvider(creds)
.build();
System.out.println(s3.listBuckets());
}
}
핵심은 “기본 체인에 맡기지 않고” IRSA(WebIdentity) provider를 명시해 IMDS로 내려갈 여지를 줄이는 것입니다.
Node.js SDK v3 예시
Node.js도 기본 provider 체인을 쓰면 상황에 따라 IMDS를 시도할 수 있으니, 가능한 경우 IRSA 기반 provider를 명시하거나 IMDS 비활성화를 병행합니다.
import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";
// IRSA 환경에서는 기본적으로 WebIdentity를 사용하지만,
// 운영 안전성을 위해 AWS_EC2_METADATA_DISABLED=true를 함께 권장.
const s3 = new S3Client({});
const out = await s3.send(new ListBucketsCommand({}));
console.log(out);
Kubernetes 매니페스트에 다음을 추가합니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: prod
spec:
template:
spec:
serviceAccountName: myapp
containers:
- name: app
image: myrepo/myapp:latest
env:
- name: AWS_EC2_METADATA_DISABLED
value: "true"
5단계: “노드 IAM Role을 못 쓰게” 권한 경계 재설계
IMDS 차단은 직접적인 예방책이지만, 방어는 다층이어야 합니다. 현실적으로 어떤 이유로든 IMDS가 열릴 수 있고, 그때 피해를 줄이는 방법은 노드 IAM Role을 최소 권한으로 강하게 제한하는 것입니다.
권장 방향:
- 노드 인스턴스 프로파일에는 정말 노드 운영에 필요한 권한만 부여
- 애플리케이션 권한은 반드시 IRSA Role로만 부여
- 가능하면 노드 Role에서 민감 서비스(S3 write, KMS decrypt, SecretsManager get 등)를 제거
이렇게 하면 “IMDS를 통해 노드 Role을 가져가도” 할 수 있는 일이 제한됩니다.
운영에서의 추천 조합(현실적인 결론)
환경별로 정리하면 다음 조합이 가장 많이 쓰입니다.
- 기본값(강력 권장)
- IRSA 정상화 +
AWS_EC2_METADATA_DISABLED=true+ 노드 Role 최소권한
- IRSA 정상화 +
- CNI가 Cilium/Calico 등이고 정책 운영이 가능
- 위 조합 + Pod 단위 IMDS egress deny(가장 깔끔)
- 정책 운영이 어렵고 “어떤 Pod도 IMDS 금지”가 목표
- 위 조합 + 노드 레벨에서 IMDSv2 강제 + hop limit=1 + (필요 시) iptables 차단
EKS에서 다른 유형의 장애를 빠르게 진단하는 글도 참고하면, 실제 운영 트러블슈팅 체계가 잡힙니다: EKS에서 Karpenter 노드가 안 늘 때 10분 진단
빠른 검증용 체크리스트
배포 후 아래를 “통과”하면 IMDS로 떨어지는 사고 가능성이 크게 줄어듭니다.
- Pod에서
AWS_ROLE_ARN,AWS_WEB_IDENTITY_TOKEN_FILE확인됨 - Pod에서
curl http://169.254.169.254/...가 타임아웃/차단됨 -
AWS_EC2_METADATA_DISABLED=true적용됨 - 노드 IAM Role이 최소 권한이며, 앱 권한은 IRSA로만 부여됨
- CloudTrail에서 노드 Role로 앱 API 호출이 더 이상 발생하지 않음
마무리
EKS에서 AWS SDK가 IMDS로 떨어지는 현상은 “IRSA가 항상 이긴다”는 기대가 깨질 때 발생합니다. 그래서 해결도 한 가지가 아니라, **IRSA 정상화(원인 제거) + IMDS 네트워크 차단(방어선) + 노드 Role 최소화(피해 축소)**의 조합으로 접근해야 합니다.
가장 먼저는 Pod에서 IMDS 접근이 실제로 가능한지 curl로 확인하고, 가능한 경우 CNI 정책(deny 169.254.169.254/32)로 차단하세요. 정책이 어렵다면 IMDSv2 강제와 hop limit 조정, 필요 시 노드 레벨 차단까지 올리는 것이 운영적으로 가장 안전합니다.