ECS on EC2 환경에서, 권한이 낮은 컨테이너가 컨테이너 탈출 없이 같은 호스트의 다른 task에 부여된 IAM 크리덴셜을 탈취할 수 있다. 비결은 ECS 에이전트와 컨트롤 플레인 사이의 비문서화된 ACS(Agent Communication Service) WebSocket 프로토콜을 위조하는 것. CloudTrail에는 피해 task의 이름으로 기록되어 탐지가 극도로 어렵다. AWS는 이것을 “보안 이슈가 아니다”라고 답했다. CVE 미할당. 패치 없음. 기본 설정에서 동작. 2025년 8월 Black Hat USA에서 공개, PoC가 GitHub에 공개되어 있다.
┌──────────────────────── EKS의 크리덴셜 유통 ────────────────────────┐
│ │
│ [Pod] → AWS SDK → STS:AssumeRoleWithWebIdentity (IRSA, 직접 호출) │
│ [Pod] → 169.254.170.23:80 → Pod Identity Agent → STS (Agent 경유) │
│ │
│ 크리덴셜 획득: Pod가 직접 or Agent 경유 │
│ 격리 단위: Pod별 SA 토큰 │
└────────────────────────────────────────────────────────────────────┘
┌──────────────────────── ECS의 크리덴셜 유통 ────────────────────────┐
│ │
│ [ECS 컨트롤 플레인] │
│ │ │
│ │ ACS WebSocket (비문서화 프로토콜) │
│ │ 모든 task의 크리덴셜을 한꺼번에 전달 │
│ ▼ │
│ [ECS Agent] ← EC2 인스턴스에서 실행 │
│ │ │
│ │ Task Metadata Service (169.254.170.2) │
│ │ 각 task에 해당 크리덴셜만 제공 │
│ ▼ │
│ [Task A] ← Task A의 크리덴셜만 보임 │
│ [Task B] ← Task B의 크리덴셜만 보임 │
│ │
│ 크리덴셜 획득: 컨트롤 플레인 → Agent → Task (3단계 배포) │
│ 격리 단위: Agent의 메타데이터 스코핑 (소프트 격리) │
└────────────────────────────────────────────────────────────────────┘
핵심 차이를 정리하면 이렇다.
EKS: 각 Pod가 STS를 직접 호출하거나, Pod Identity Agent를 개별적으로 호출. Pod A의 크리덴셜 흐름에 Pod B가 끼어들 수 없는 구조.
ECS: 컨트롤 플레인이 모든 task의 크리덴셜을 하나의 WebSocket 채널로 ECS Agent에 전달. Agent가 내부적으로 각 task에 분배. 이 채널을 가로채면 모든 task의 크리덴셜에 접근 가능.
EKS에서 다른 Pod의 크리덴셜을 훔치려면 노드 크리덴셜 탈취 → SA 토큰 생성이라는 2단계가 필요했다. ECS에서는 ACS WebSocket 하나만 위조하면 끝이다.
ECScape는 Sweet Security의 연구원 Naor Haziz가 2025년 8월 Black Hat USA에서 발표한 ECS 권한 상승 기법이다. 원래 eBPF 기반 모니터링 도구를 개발하다가 ECS 내부 프로토콜을 분석하던 중 우연히 발견했다고 한다.
공격의 본질을 한 문장으로: 저권한 컨테이너가 ECS Agent를 사칭하여, 컨트롤 플레인으로부터 같은 호스트의 모든 task에 대한 IAM 크리덴셜을 수신한다.
# 컨테이너 내부에서 실행
# IMDS는 기본적으로 모든 컨테이너에서 접근 가능
# IMDSv2 토큰 획득
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# Instance Role 크리덴셜 획득
ROLE=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/)
CREDS=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE)
여기서 획득하는 것은 ECS Agent가 사용하는 것과 동일한 Instance Role 크리덴셜이다. 이 크리덴셜로 ECS API를 호출할 수 있다.
왜 가능한가: ECS on EC2의 기본 설정에서 IMDS는 모든 컨테이너에 접근 가능. 이것은 EKS의 IMDS 이슈와 동일한 근본 원인이다.
# Instance Role 크리덴셜로 ECS API 호출
# DiscoverPollEndpoint: Agent가 연결할 컨트롤 플레인 엔드포인트 반환
aws ecs discover-poll-endpoint --cluster CLUSTER_NAME
# 응답:
# {
# "endpoint": "https://ecs-a-1.REGION.amazonaws.com",
# "telemetryEndpoint": "https://ecs-t-1.REGION.amazonaws.com",
# "serviceConnectEndpoint": "https://ecs-sc-1.REGION.amazonaws.com"
# }
이 엔드포인트가 ACS WebSocket이 연결되는 서버 주소다. 일반 ECS 사용자는 이 엔드포인트의 존재 자체를 모르는 경우가 대부분이다.
ACS WebSocket 연결을 위해서는 정상 에이전트가 보내야 할 식별 정보가 필요하다.
# ECS Agent의 Introspection API (localhost:51678)
# 컨테이너 내부에서 접근 가능한 경우가 있음
# 또는 IMDS + ECS API 조합으로 수집:
# - Cluster ARN
# - Container Instance ARN
# - Agent 버전
# - Docker 버전
# - ACS 프로토콜 버전
# - Sequence Number
# Task Metadata Service에서도 일부 정보 획득 가능
curl http://169.254.170.2/v4/metadata
이것이 ECScape의 핵심이다.
[정상 흐름]
ECS Control Plane ←──ACS WebSocket──→ ECS Agent (정상)
│
│ Task Metadata (169.254.170.2)
▼
[Task A] [Task B] [Task C]
[ECScape 공격]
ECS Control Plane ←──ACS WebSocket──→ ECS Agent (정상)
←──ACS WebSocket──→ 공격자 (위조) ← 🚨
│
sendCredentials=true
│
▼
모든 Task의 IAM 크리덴셜 수신
위조된 WebSocket 요청의 핵심 요소는 다음과 같다.
AWS SigV4 서명: Step 1에서 획득한 Instance Role 크리덴셜로 요청에 서명. 컨트롤 플레인은 이 서명이 유효한 에이전트의 것인지 구분하지 못한다.
sendCredentials=true 파라미터: 이 파라미터를 포함하면 컨트롤 플레인이 연결 즉시 모든 running task의 크리덴셜을 전송한다.
정상 에이전트 행위 모방: 위조된 세션은 메시지 수신 확인(ACK), sequence number 증분, heartbeat 전송 등 정상 에이전트의 행위를 완벽히 모방. 컨트롤 플레인 입장에서 정상 에이전트와 구분 불가.
[ACS WebSocket 수신 내용]
Task A (frontend-app):
AccessKeyId: ASIA...
SecretAccessKey: ...
SessionToken: ...
→ IAM Role: arn:aws:iam::123456789012:role/frontend-readonly
Task B (payment-service): ← 💀
AccessKeyId: ASIA...
SecretAccessKey: ...
SessionToken: ...
→ IAM Role: arn:aws:iam::123456789012:role/payment-admin
Task C (data-pipeline): ← 💀💀
AccessKeyId: ASIA...
SecretAccessKey: ...
SessionToken: ...
→ IAM Role: arn:aws:iam::123456789012:role/data-full-access
Task Execution Role: ← 💀💀💀
AccessKeyId: ASIA...
SecretAccessKey: ...
SessionToken: ...
→ Secrets Manager, ECR, CloudWatch Logs 접근 가능
위조된 WebSocket이 연결된 상태로 유지되는 한, 이후에 새로 시작되는 task의 크리덴셜도 자동으로 수신된다. 패시브 수집이 가능하다는 뜻.
특히 Task Execution Role이 함께 유출된다는 점이 중요하다. Execution Role은 ECR 이미지 Pull, Secrets Manager 시크릿 주입, CloudWatch Logs 쓰기 등에 사용되는 역할로, 일반적으로 Task Role보다 넓은 범위의 시크릿에 접근 가능하다.
ECScape의 가장 무서운 특성은 탈취한 크리덴셜 사용이 정상 동작과 구분되지 않는다는 점이다.
{
"eventName": "GetObject",
"eventSource": "s3.amazonaws.com",
"userIdentity": {
"type": "AssumedRole",
"arn": "arn:aws:sts::123456789012:assumed-role/payment-admin/..."
},
"sourceIPAddress": "10.0.1.50"
}
이 로그만 보면 payment-service task가 정상적으로 S3에 접근한 것처럼 보인다. 실제로는 공격자의 저권한 컨테이너가 탈취한 크리덴셜로 호출한 것이지만, AWS 입장에서는 구분이 불가능하다. 크리덴셜 자체가 정상 발급된 것이기 때문이다.
그래도 완전히 투명하지는 않다. 다음 이상 징후를 모니터링할 수 있다.
ECS API 호출 출처 이상: ecs:DiscoverPollEndpoint 같은 ECS 에이전트 전용 API가 컨테이너 프로세스에서 호출되는 것은 비정상. 정상 환경에서는 ECS Agent만이 호출한다.
WebSocket 연결 이상: 컨테이너 프로세스가 AWS 도메인에 대한 WebSocket 연결을 시도하는 것은 적신호. eBPF 기반 런타임 모니터링으로 탐지 가능.
행동 패턴 이상: 저권한 task role이 갑자기 Secrets Manager나 RDS에 접근하기 시작하면 의심. 단, 이는 크리덴셜 탈취 후 사용 단계에서만 탐지 가능하므로 이미 늦은 상황.
EKS 시리즈에서 다룬 공격들과 ECScape를 비교하면 ECS만의 위험성이 선명해진다.
| 항목 | EKS: IMDS 체인 (3편) | EKS: Pod Identity 스푸핑 (1편) | ECS: ECScape |
|---|---|---|---|
| 전제 조건 | SSRF 또는 RCE | hostNetwork + CAP_NET_ADMIN | IMDS 접근만 (기본 설정) |
| 컨테이너 탈출 필요 | 불필요 (SSRF) 또는 필요 (RCE) | 불필요 | 불필요 |
| 탈취 범위 | 노드의 모든 Pod SA 사칭 가능 | 단일 Pod 크리덴셜 | 호스트의 모든 Task + Execution Role |
| 탐지 난이도 | 중간 (IMDS 호출 탐지 가능) | 중간 (Agent 이상 탐지 가능) | 극도로 어려움 (CloudTrail에 피해자 이름) |
| misconfiguration 필요 | hop limit 2 (기본값이 취약) | hostNetwork 설정 필요 | 불필요 (모든 기본값이 충분) |
| AWS 답변 | “고객 책임” | “예상된 동작” | “보안 이슈가 아니다” |
| 패치/CVE | 없음 | 없음 | 없음 (CVE 요청됨, 미할당) |
ECScape가 특히 위험한 조합은 “기본 설정에서 동작” + “탐지 극난” + “패치 없음”이다. EKS의 Pod Identity 스푸핑은 최소한 hostNetwork: true와 CAP_NET_ADMIN이 필요했다. ECScape는 아무런 특별 설정 없이, 기본 ECS on EC2 환경에서 그대로 동작한다.
AWS는 Sweet Security의 보고에 대해 다음과 같이 답변했다.
“이것은 보안 이슈가 아닙니다(does not present a security concern for AWS). EC2 인스턴스가 보안 경계이며, 컨테이너가 아닙니다. 고객은 EC2 기반 ECS 배포와 그 위에서 실행되는 모든 task/컨테이너에 대한 완전한 제어권을 갖고 있습니다.”
연구를 수행한 Haziz의 반응은 이랬다.
“패치하기 매우 어려울 것입니다. 모든 ECS 인스턴스가 새 에이전트를 실행하도록 보장해야 하는데, 기존 에이전트는 그대로 남아있으니까요.”
AWS는 이 연구 이후 공식 문서를 업데이트하여 “같은 EC2 인스턴스에서 실행되는 task는 잠재적으로 다른 task의 크리덴셜에 접근할 수 있다”는 경고를 추가했고, Fargate 사용을 강력히 권장하는 내용을 보강했다. 하지만 코드 수준의 수정이나 패치는 없었다.
이 시리즈의 메타 내러티브에서 반복되는 패턴이다. “설계대로 작동한다”는 것이 “안전하다”는 의미가 아니다. IMDSv1도 설계대로 작동했고, Capital One 사고 이후 IMDSv2가 나왔다. ECS의 ACS 프로토콜도 언젠가 보안이 강화될 수 있지만, 그 전까지는 고객이 직접 방어해야 한다.
Fargate = 각 task가 독립된 마이크로VM에서 실행
→ IMDS 격리, ECS Agent 격리
→ ECScape 완전 불가
"ECScape does not apply to Fargate because
there is no co-tenancy of the instance."
— Naor Haziz
Fargate로 전환하면 ECScape의 전제 조건(같은 호스트에 여러 task 공존)이 원천적으로 제거된다. 하지만 비용, 성능, 기존 인프라 호환성 등의 이유로 EC2 Launch Type을 유지해야 하는 경우가 많다. 그때는 아래의 완화 조치가 필요하다.
ECScape의 첫 번째 단계인 Instance Role 크리덴셜 획득을 차단한다.
# EC2 인스턴스의 hop limit을 1로 설정
# 컨테이너 네트워크 브릿지를 통과하지 못해 IMDS 접근 차단
aws ec2 modify-instance-metadata-options \
--instance-id i-XXXXXXXXX \
--http-put-response-hop-limit 1 \
--http-tokens required
# iptables로 컨테이너의 IMDS 접근 차단 (AWS 공식 권장)
yum install -y iptables-services
iptables --insert FORWARD 1 \
--in-interface docker+ \
--destination 169.254.169.254/32 \
--jump DROP
iptables-save | tee /etc/sysconfig/iptables
systemctl enable --now iptables
주의: ECS Agent 자체가 IMDS를 사용하므로, Agent 프로세스는 접근 가능하도록 유지해야 한다. iptables 규칙을 Docker 인터페이스(docker+)에만 적용하는 이유가 이것이다.
{
"placementConstraints": [
{
"type": "memberOf",
"expression": "attribute:security-tier == high"
}
]
}
고권한 task(결제, 시크릿 관리)와 저권한 task(프론트엔드, 배치 작업)를 물리적으로 다른 EC2 인스턴스에 배치한다. Capacity Provider 전략이나 task placement constraints를 활용.
같은 권한 수준의 task만 같은 호스트에 공존한다면, ECScape로 탈취해도 이미 가진 것과 같은 수준의 크리덴셜만 얻게 되므로 실질적 위험이 크게 감소한다.
ECScape가 Task Execution Role까지 탈취한다는 점을 고려하면, Execution Role에 과도한 Secrets Manager 접근 권한이 있으면 모든 시크릿이 유출될 수 있다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": [
"arn:aws:secretsmanager:REGION:ACCOUNT:secret:myapp/prod/*"
]
}
]
}
secretsmanager:GetSecretValue를 Resource: *가 아닌, 해당 task가 필요한 시크릿만 명시적으로 지정.
-- CloudWatch Logs Insights: 비정상 ECS API 호출 탐지
-- DiscoverPollEndpoint는 ECS Agent만 호출해야 함
fields @timestamp, sourceIPAddress, eventName, userIdentity.arn
| filter eventName in [
"DiscoverPollEndpoint",
"Poll",
"RegisterContainerInstance"
]
| filter userIdentity.arn not like /ecsInstanceRole/
| sort @timestamp desc
# Falco: 컨테이너에서 ECS/IMDS 관련 비정상 네트워크 연결 탐지
- rule: Container Connecting to ECS Control Plane
desc: Detect container process connecting to ECS ACS endpoint
condition: >
outbound and container and
(fd.sip_name contains "ecs-a-" or
fd.sip_name contains "ecs-t-")
output: >
Container connecting to ECS control plane endpoint
(command=%proc.cmdline pod=%k8s.pod.name dest=%fd.name)
priority: CRITICAL
ECS on EC2를 운영 중이라면 아래 항목을 점검하자.
ECScape는 2025년에 공개된 클라우드 보안 연구 중 가장 임팩트가 큰 기법 중 하나다. 그 이유는 단순한 권한 상승을 넘어, ECS의 크리덴셜 배포 아키텍처 자체의 신뢰 모델을 붕괴시키기 때문이다.
Haziz의 말을 빌리면:
“핵심 교훈은 모든 컨테이너를 잠재적으로 침해 가능한 것으로 취급하고, blast radius를 엄격하게 제한해야 한다는 것입니다. AWS의 편리한 추상화는 개발자의 삶을 편하게 해주지만, 권한 수준이 다른 여러 task가 하나의 호스트를 공유할 때, 보안은 그것들을 격리하는 메커니즘의 강도에 의해 결정됩니다.”
다음 글(ECS 2편)에서는 ECScape 외에 ECS에만 존재하는 숨겨진 공격표면 5가지 — Self-Registration, Task Overrides, SSM 로깅 우회, disableApiTermination persistence, Task Execution Role 남용 — 를 다룬다.
참고 자료