Published on

EKS에서 AWS SDK의 IMDS 접근을 확실히 차단하는 법

Authors

서론

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는 다음과 같은 순서(간소화)로 자격 증명을 탐색합니다.

  1. 환경 변수: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
  2. 공유 설정/크리덴셜 파일
  3. 웹 아이덴티티(예: IRSA): AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE
  4. ECS/컨테이너 메타데이터
  5. 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_ARN
  • AWS_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을 가져가도” 할 수 있는 일이 제한됩니다.

운영에서의 추천 조합(현실적인 결론)

환경별로 정리하면 다음 조합이 가장 많이 쓰입니다.

  1. 기본값(강력 권장)
    • IRSA 정상화 + AWS_EC2_METADATA_DISABLED=true + 노드 Role 최소권한
  2. CNI가 Cilium/Calico 등이고 정책 운영이 가능
    • 위 조합 + Pod 단위 IMDS egress deny(가장 깔끔)
  3. 정책 운영이 어렵고 “어떤 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 조정, 필요 시 노드 레벨 차단까지 올리는 것이 운영적으로 가장 안전합니다.