Skip to content

EC2 IMDSv2 Hop Limit 완벽 가이드 - Docker 컨테이너에서의 IAM Role 사용

목차

  1. IMDS란 무엇인가
  2. IMDSv1 vs IMDSv2
  3. Hop Limit의 동작 원리
  4. Docker 컨테이너에서의 문제
  5. 실제 사례: CPT-Bot Bedrock 인증 실패
  6. 해결 방법
  7. 보안 고려사항
  8. 트러블슈팅 가이드

IMDS란 무엇인가

Instance Metadata Service

EC2 인스턴스 내부에서 169.254.169.254 (link-local 주소)로 접근할 수 있는 메타데이터 서비스다. 인스턴스 자신에 대한 정보를 조회할 수 있다.

EC2 Instance
  ├── Instance ID, AMI ID, Region
  ├── Network (MAC, IP, VPC, Subnet)
  ├── IAM Role 임시 자격 증명 (Access Key, Secret Key, Session Token)
  └── User Data, Tags 등

왜 중요한가

AWS SDK는 자격 증명을 찾을 때 Credential Provider Chain을 순서대로 탐색한다:

1. 환경변수 (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
2. 공유 자격 증명 파일 (~/.aws/credentials)
3. ECS 컨테이너 자격 증명 (ECS Task Role)
4. EC2 IMDS (IAM Instance Profile) ← 여기
5. SSO 등

IAM Role을 EC2에 연결하면, IMDS를 통해 임시 자격 증명을 자동으로 발급받고 갱신한다. Access Key를 하드코딩할 필요가 없어진다.

bash
# IMDS에서 IAM Role 자격 증명 조회
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

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

응답:

json
{
  "AccessKeyId": "ASIA...",
  "SecretAccessKey": "...",
  "Token": "...(session token)...",
  "Expiration": "2026-04-01T12:00:00Z"
}

IMDSv1 vs IMDSv2

IMDSv1 (레거시)

단순 HTTP GET 요청으로 메타데이터에 접근 가능하다.

bash
curl http://169.254.169.254/latest/meta-data/instance-id
# → i-0abc123def456

보안 취약점: SSRF(Server-Side Request Forgery) 공격에 취약하다. 웹 애플리케이션의 SSRF 취약점을 이용해 IAM Role의 임시 자격 증명을 탈취할 수 있다.

# 공격 시나리오
공격자 → 웹앱 (SSRF 취약점)
  → GET http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name
  → IAM 자격 증명 탈취

실제로 2019년 Capital One 해킹 사건에서 이 방법이 사용되었다.

IMDSv2 (토큰 기반)

PUT 요청으로 세션 토큰을 먼저 발급받아야 한다. 이후 모든 요청에 토큰을 헤더로 포함해야 한다.

bash
# 1단계: 토큰 발급 (PUT 메서드 필수)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

# 2단계: 토큰으로 메타데이터 조회
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/instance-id

왜 안전한가:

  • SSRF 공격은 대부분 GET/POST를 사용하며, PUT 메서드를 지원하지 않는 경우가 많다
  • 토큰 발급 시 IP 패킷의 TTL(Time To Live) 을 검증한다 → 이것이 Hop Limit이다

Hop Limit의 동작 원리

IP 패킷의 TTL(Time To Live)

모든 IP 패킷에는 TTL 필드가 있다. 패킷이 네트워크 홉(라우터)을 지날 때마다 TTL이 1씩 감소하고, 0이 되면 패킷이 폐기된다.

[IP Header]
┌─────────────────────────────────┐
│ Version │ IHL │ ... │ TTL │ ... │
│    4    │  5  │     │ 64  │     │
└─────────────────────────────────┘

                    매 홉마다 -1

IMDSv2의 Hop Limit 메커니즘

IMDSv2 토큰 발급 시, IMDS는 응답 패킷의 IP TTL을 hop limit 값으로 설정한다. 기본 hop limit은 1이다.

[Hop Limit = 1인 경우]

EC2 Host → IMDS (169.254.169.254)
  TTL: 64 → 도착 시 TTL 확인
  응답 TTL: 1 → 호스트에서 바로 수신 ✅ (홉 0개)

Docker Container (bridge) → veth → docker0 → Host → IMDS
  TTL: 64 → 도착 시 TTL 확인
  응답 TTL: 1 → docker0 통과 시 TTL=0 → 패킷 폐기 ❌ (홉 1개)

정확히 말하면, IMDS가 PUT 요청의 TTL을 검사한다:

PUT 토큰 요청:
  호스트에서 전송:    TTL = 64 → IMDS 도착 시 TTL = 64 (홉 0) ✅
  컨테이너에서 전송:  TTL = 64 → bridge 통과 → IMDS 도착 시 TTL = 63 (홉 1) ❌
                     hop limit(1)보다 많은 홉을 거쳤으므로 토큰 발급 거부

네트워크 홉 시각화

[Hop Limit = 1] (기본값)

EC2 인스턴스
┌──────────────────────────────────────────────┐
│                                              │
│  ┌─────────────┐     ┌─────────┐            │
│  │  Container   │────▶│ docker0 │──── 홉 1   │
│  │  (bridge)    │     │ bridge  │            │
│  └─────────────┘     └────┬────┘            │
│                           │                  │
│                      ┌────▼────┐             │
│                      │  Host   │──── 홉 0    │
│                      │ Network │             │
│                      └────┬────┘             │
│                           │                  │
│                      ┌────▼──────────┐       │
│                      │ IMDS          │       │
│                      │ 169.254.169.254│       │
│                      │ hop limit = 1 │       │
│                      └───────────────┘       │
│                                              │
│  Host에서: 홉 0 ≤ 1 ✅ 토큰 발급            │
│  Container에서: 홉 1 ≤ 1 ❌ 토큰 거부        │
└──────────────────────────────────────────────┘


[Hop Limit = 2]

  Host에서: 홉 0 ≤ 2 ✅
  Container (bridge)에서: 홉 1 ≤ 2 ✅ 토큰 발급
  외부 네트워크에서: 홉 2+ > 2 ❌ 여전히 차단

Docker 컨테이너에서의 문제

Docker 네트워크 모드별 IMDS 접근성

네트워크 모드홉 수Hop Limit=1Hop Limit=2
host0✅ 접근 가능✅ 접근 가능
bridge (기본)1❌ 접근 불가✅ 접근 가능
overlay1+❌ 접근 불가✅/❌ 상황에 따라
macvlan1+❌ 접근 불가❌ 접근 불가 (대부분)

bridge 네트워크의 패킷 경로

bash
# 컨테이너 내부에서 traceroute로 확인
docker run --rm alpine traceroute -n 169.254.169.254

# 결과 예시:
traceroute to 169.254.169.254, 30 hops max
 1  172.17.0.1  0.042 ms docker0 bridge (홉 1)
 2  169.254.169.254  0.284 ms IMDS (홉 2)

host 네트워크의 패킷 경로

bash
docker run --rm --network host alpine traceroute -n 169.254.169.254

# 결과 예시:
traceroute to 169.254.169.254, 30 hops max
 1  169.254.169.254  0.125 ms 직접 도달 (홉 0, 같은 네트워크)

실제 사례: CPT-Bot Bedrock 인증 실패

상황

EC2에 Docker Compose로 배포된 Slack 봇이 AWS Bedrock Claude API 호출 시 인증 오류 발생.

인프라 구성

EC2 (Amazon Linux 2023)
├── IAM Role: cronjob-instance-role (연결됨)
├── IMDS: v2 (hop limit = 1)

├── Docker Container: bot-slack (bridge 네트워크)
│   ├── .env에 하드코딩된 AWS_ACCESS_KEY_ID (만료됨)
│   └── IAM Role 사용 불가 (IMDS 접근 불가)

└── Docker Container: bot-web (bridge 네트워크)
    └── 동일한 문제

자격 증명 탐색 과정

Go AWS SDK v2: config.LoadDefaultConfig()

  ├─ 1순위: 환경변수 → godotenv가 .env 로드 → AWS_ACCESS_KEY_ID 발견
  │   → 만료된 키 사용 → 403 UnrecognizedClientException ❌

  ├─ 2순위: ~/.aws/credentials → 컨테이너에 없음 (skip)

  └─ 3순위: IMDS → bridge 네트워크, hop limit=1 → 접근 불가 (skip)

에러 로그

json
{
  "level": "error",
  "error": "bedrock API 호출 실패: operation error Bedrock Runtime: InvokeModel, https response error StatusCode: 403, RequestID: fae22d15-..., api error UnrecognizedClientException: The security token included in the request is invalid.",
  "message": "스레드 분석 실패"
}

검증 과정

bash
# 1. EC2 호스트에서 IMDS 확인 → IAM Role 존재
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/
# → cronjob-instance-role

# 2. 컨테이너 내부에서 IMDS 접근 시도 → 실패
docker exec bot-slack wget -qO- \
  --method=PUT --header="X-aws-ec2-metadata-token-ttl-seconds: 21600" \
  http://169.254.169.254/latest/api/token
# → IMDS_UNREACHABLE

# 3. 컨테이너 환경변수 확인 → AWS 키 없음 (docker env에)
docker inspect bot-slack --format '{{json .Config.Env}}'
# → ["CPT_BOT_MODE=slack", "PATH=...", "TZ=Asia/Seoul"]
# AWS 키는 이미지 내장 .env 파일에만 존재

해결 방법

방법 1: network_mode: host (가장 간단)

컨테이너가 호스트 네트워크 스택을 직접 사용한다. 네트워크 홉이 없으므로 hop limit=1에서도 IMDS 접근 가능.

yaml
# docker-compose.yaml
services:
  bot-slack:
    image: cptbot-slack-bot:v2.0.3
    network_mode: host
    environment:
      - CPT_BOT_MODE=slack

장점:

  • 설정이 간단하다
  • AWS 인프라 변경 불필요
  • 네트워크 성능이 약간 더 좋다

단점:

  • 컨테이너 네트워크 격리가 사라진다
  • 포트 충돌 가능성 (bot-slack은 포트를 사용하지 않으므로 무관)

방법 2: IMDS Hop Limit을 2로 변경

bash
# 인스턴스 ID 확인
INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/instance-id)

# hop limit 변경
aws ec2 modify-instance-metadata-options \
  --instance-id $INSTANCE_ID \
  --http-put-response-hop-limit 2 \
  --http-endpoint enabled

장점:

  • Docker bridge 네트워크 유지 (격리 유지)
  • 모든 컨테이너에 일괄 적용

단점:

  • AWS API 호출 필요 (IAM 권한)
  • 보안 범위가 넓어짐 (같은 EC2의 모든 컨테이너가 IMDS 접근 가능)

방법 3: .env에 자격 증명 주입 (현재 방식)

yaml
services:
  bot-slack:
    image: cptbot-slack-bot:v2.0.3
    env_file: .env  # AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 포함
    environment:
      - CPT_BOT_MODE=slack

장점:

  • 인프라 변경 불필요
  • 즉시 적용 가능

단점:

  • 키가 파일에 평문으로 저장됨
  • 키 로테이션 시 수동 업데이트 필요
  • 키 만료 시 서비스 장애 (이번 사례와 동일)

방법 4: ECS로 마이그레이션 (장기적 권장)

ECS Task Role을 사용하면 컨테이너별로 IAM Role을 할당할 수 있다. IMDS가 아닌 ECS 컨테이너 자격 증명 엔드포인트를 사용하므로 hop limit 문제가 없다.

AWS SDK Credential Chain:
  → 환경변수 AWS_CONTAINER_CREDENTIALS_RELATIVE_URI (ECS가 자동 설정)
  → http://169.254.170.2/... (ECS 전용 엔드포인트, IMDS와 다름)

방법 비교

host 모드hop limit=2.env 키 주입ECS 마이그레이션
설정 난이도낮음중간낮음높음
보안보통보통낮음높음
키 로테이션자동자동수동자동
네트워크 격리없음유지유지유지
컨테이너별 Role불가불가가능 (키 분리)가능

보안 고려사항

Hop Limit=1이 기본인 이유

AWS가 hop limit을 1로 설정한 이유는 최소 권한 원칙이다:

  1. 컨테이너 탈출 방지: 컨테이너 내부의 악성 코드가 호스트의 IAM Role을 사용하는 것을 막는다
  2. SSRF 방어 강화: IMDSv2의 PUT + 토큰 메커니즘에 추가적인 네트워크 레벨 보호를 제공한다
  3. 다중 테넌시 보호: 하나의 EC2에서 여러 고객의 컨테이너를 실행할 때 격리를 보장한다

Hop Limit을 2로 올릴 때의 트레이드오프

[보안 경계 변화]

Hop Limit=1:
  신뢰 경계: EC2 호스트 프로세스만
  컨테이너: IMDS 접근 불가 (안전)

Hop Limit=2:
  신뢰 경계: EC2 호스트 + 모든 Docker 컨테이너
  컨테이너: IMDS 접근 가능 (편리하지만 보안 범위 확대)

허용해도 괜찮은 경우:

  • EC2에서 자체 워크로드만 실행 (다중 테넌시가 아닌 경우)
  • 모든 컨테이너가 동일한 IAM Role 권한이 필요한 경우

주의해야 하는 경우:

  • 여러 팀/서비스의 컨테이너가 혼재된 경우
  • 최소 권한 원칙을 엄격히 적용해야 하는 환경

IMDSv2 강제 적용

항상 IMDSv2를 강제해야 한다. IMDSv1이 활성화되어 있으면 hop limit이 무의미하다.

bash
# IMDSv2만 허용 (v1 비활성화)
aws ec2 modify-instance-metadata-options \
  --instance-id $INSTANCE_ID \
  --http-tokens required  # "optional"이면 v1도 허용

트러블슈팅 가이드

1. IMDS 접근 가능 여부 확인

bash
# 호스트에서
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" 2>&1)
echo "Token: $TOKEN"

# 토큰이 비어있으면 → IMDS 비활성화 또는 네트워크 문제
# 401 응답 → IMDSv1으로 시도 중 (v2가 필수인 상태)

2. IAM Role 연결 확인

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

# 빈 응답 → IAM Role이 연결되지 않음
# role-name 출력 → 정상

3. 컨테이너에서 IMDS 접근 테스트

bash
# bridge 네트워크 컨테이너에서 테스트
docker run --rm alpine sh -c '
  TOKEN=$(wget -qO- --method=PUT \
    --header="X-aws-ec2-metadata-token-ttl-seconds: 21600" \
    http://169.254.169.254/latest/api/token 2>/dev/null)
  if [ -z "$TOKEN" ]; then
    echo "IMDS 접근 불가 (hop limit 문제 가능성)"
  else
    echo "IMDS 접근 가능, Token: $TOKEN"
    wget -qO- --header="X-aws-ec2-metadata-token: $TOKEN" \
      http://169.254.169.254/latest/meta-data/iam/security-credentials/
  fi
'

4. 현재 Hop Limit 확인

bash
# AWS CLI로 확인
aws ec2 describe-instances --instance-id $INSTANCE_ID \
  --query "Reservations[].Instances[].MetadataOptions"

# 출력 예시:
# {
#   "HttpEndpoint": "enabled",
#   "HttpTokens": "required",        ← IMDSv2 강제
#   "HttpPutResponseHopLimit": 1     ← hop limit
# }

5. AWS SDK 자격 증명 디버깅

go
// Go에서 어떤 자격 증명이 사용되는지 확인
cfg, _ := config.LoadDefaultConfig(context.Background())
creds, _ := cfg.Credentials.Retrieve(context.Background())
fmt.Printf("Provider: %s\n", creds.Source)
fmt.Printf("AccessKeyID: %s\n", creds.AccessKeyID[:8]+"...")
// Source가 "EC2RoleProvider"면 IMDS 사용 중
// Source가 "EnvConfigCredentials"면 환경변수 사용 중

요약 플로우차트

컨테이너에서 AWS API 호출 실패?

  ├─ 403 Forbidden / UnrecognizedClientException
  │   └─ 자격 증명이 만료되었거나 무효
  │       ├─ 하드코딩된 키 사용 중? → 키 갱신 또는 IAM Role로 전환
  │       └─ IMDS 사용 중? → 토큰 만료 확인 (자동 갱신 실패)

  ├─ IMDS 접근 불가 (timeout)
  │   ├─ hop limit 확인 → 1이면 2로 변경 또는 host 모드 사용
  │   ├─ IMDS 활성화 여부 확인
  │   └─ 보안그룹/네트워크 ACL 확인 (169.254.169.254 차단 여부)

  └─ No credentials found
      ├─ IAM Role 연결 여부 확인
      ├─ 환경변수 확인 (AWS_ACCESS_KEY_ID)
      └─ ~/.aws/credentials 확인

참고 자료