EC2 IMDSv2 Hop Limit 완벽 가이드 - Docker 컨테이너에서의 IAM Role 사용
목차
- IMDS란 무엇인가
- IMDSv1 vs IMDSv2
- Hop Limit의 동작 원리
- Docker 컨테이너에서의 문제
- 실제 사례: CPT-Bot Bedrock 인증 실패
- 해결 방법
- 보안 고려사항
- 트러블슈팅 가이드
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를 하드코딩할 필요가 없어진다.
# 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>응답:
{
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"Token": "...(session token)...",
"Expiration": "2026-04-01T12:00:00Z"
}IMDSv1 vs IMDSv2
IMDSv1 (레거시)
단순 HTTP GET 요청으로 메타데이터에 접근 가능하다.
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 요청으로 세션 토큰을 먼저 발급받아야 한다. 이후 모든 요청에 토큰을 헤더로 포함해야 한다.
# 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 │ │
└─────────────────────────────────┘
↑
매 홉마다 -1IMDSv2의 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=1 | Hop Limit=2 |
|---|---|---|---|
host | 0 | ✅ 접근 가능 | ✅ 접근 가능 |
bridge (기본) | 1 | ❌ 접근 불가 | ✅ 접근 가능 |
overlay | 1+ | ❌ 접근 불가 | ✅/❌ 상황에 따라 |
macvlan | 1+ | ❌ 접근 불가 | ❌ 접근 불가 (대부분) |
bridge 네트워크의 패킷 경로
# 컨테이너 내부에서 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 네트워크의 패킷 경로
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)에러 로그
{
"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": "스레드 분석 실패"
}검증 과정
# 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 접근 가능.
# 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로 변경
# 인스턴스 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에 자격 증명 주입 (현재 방식)
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로 설정한 이유는 최소 권한 원칙이다:
- 컨테이너 탈출 방지: 컨테이너 내부의 악성 코드가 호스트의 IAM Role을 사용하는 것을 막는다
- SSRF 방어 강화: IMDSv2의 PUT + 토큰 메커니즘에 추가적인 네트워크 레벨 보호를 제공한다
- 다중 테넌시 보호: 하나의 EC2에서 여러 고객의 컨테이너를 실행할 때 격리를 보장한다
Hop Limit을 2로 올릴 때의 트레이드오프
[보안 경계 변화]
Hop Limit=1:
신뢰 경계: EC2 호스트 프로세스만
컨테이너: IMDS 접근 불가 (안전)
Hop Limit=2:
신뢰 경계: EC2 호스트 + 모든 Docker 컨테이너
컨테이너: IMDS 접근 가능 (편리하지만 보안 범위 확대)허용해도 괜찮은 경우:
- EC2에서 자체 워크로드만 실행 (다중 테넌시가 아닌 경우)
- 모든 컨테이너가 동일한 IAM Role 권한이 필요한 경우
주의해야 하는 경우:
- 여러 팀/서비스의 컨테이너가 혼재된 경우
- 최소 권한 원칙을 엄격히 적용해야 하는 환경
IMDSv2 강제 적용
항상 IMDSv2를 강제해야 한다. IMDSv1이 활성화되어 있으면 hop limit이 무의미하다.
# IMDSv2만 허용 (v1 비활성화)
aws ec2 modify-instance-metadata-options \
--instance-id $INSTANCE_ID \
--http-tokens required # "optional"이면 v1도 허용트러블슈팅 가이드
1. IMDS 접근 가능 여부 확인
# 호스트에서
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 연결 확인
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 접근 테스트
# 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 확인
# 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에서 어떤 자격 증명이 사용되는지 확인
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 확인