IAM 최소 권한 (Least Privilege) - 정답 및 해설
Step 1 정답: 현재 IAM 정책 확인
정답
웹 콘솔 풀이
- AWS 콘솔에 로그인합니다.
- 상단 검색창에
IAM을 입력하고 IAM 서비스를 선택합니다. - 왼쪽 메뉴에서 Roles를 클릭합니다.
- 검색창에
ShopEasy를 입력하고 ShopEasy-EC2-Role을 클릭합니다. - Permissions 탭에서 연결된 정책을 확인합니다:
# 현재 연결된 정책 (Chapter 04에서 설정한 것)
Policy name Type
----------------------------- ------------------
AmazonS3FullAccess AWS managed policy
AmazonDynamoDBFullAccess AWS managed policy
- 각 정책 이름을 클릭하면 정책의 JSON 내용을 확인할 수 있습니다.
- AmazonS3FullAccess의 JSON을 확인하면:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:*", <-- 모든 S3 작업 허용!
"Resource": "*" <-- 모든 S3 리소스 허용!
}
]
}
- AmazonDynamoDBFullAccess의 JSON을 확인하면:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:*", <-- 모든 DynamoDB 작업 허용!
...
],
"Resource": "*" <-- 모든 DynamoDB 리소스 허용!
}
]
}
AWS CLI 풀이
# ShopEasy-EC2-Role에 연결된 관리형 정책 목록 조회
aws iam list-attached-role-policies --role-name ShopEasy-EC2-Role
# 예상 출력:
# {
# "AttachedPolicies": [
# {
# "PolicyName": "AmazonS3FullAccess",
# "PolicyArn": "arn:aws:iam::aws:policy/AmazonS3FullAccess"
# },
# {
# "PolicyName": "AmazonDynamoDBFullAccess",
# "PolicyArn": "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
# }
# ]
# }
# 각 정책의 상세 내용 확인 (선택)
# S3FullAccess 정책 버전 확인
aws iam get-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess
# S3FullAccess 정책 JSON 내용 확인
aws iam get-policy-version \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess \
--version-id v1
AmazonS3FullAccess의 핵심은 "Action": "s3:*"와 "Resource": "*"입니다. 이는 계정 내 모든 S3 버킷에 대해 모든 작업(읽기, 쓰기, 삭제, 버킷 삭제 포함!)을 허용합니다.
마찬가지로 AmazonDynamoDBFullAccess는 "Action": "dynamodb:*"로 모든 DynamoDB 작업을 허용합니다. 테이블 생성, 삭제까지 가능하므로 매우 위험합니다.
ShopEasy 서비스는 실제로 S3 버킷 2개와 DynamoDB 테이블 1개만 사용합니다. 따라서 이 리소스들에 대한 필요한 작업만 허용하는 커스텀 정책으로 교체하겠습니다.
Step 2 정답: S3 커스텀 정책 생성
정답
웹 콘솔 풀이
- IAM → 왼쪽 메뉴에서 Policies를 클릭합니다.
- Create policy 버튼을 클릭합니다.
- JSON 탭을 클릭하고, 아래 정책을 붙여넣습니다 (
{ACCOUNT_ID}를 본인의 계정 ID로 변경):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3BucketAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::shopeasy-images-{ACCOUNT_ID}/*",
"arn:aws:s3:::shopeasy-frontend-{ACCOUNT_ID}/*"
]
},
{
"Sid": "S3ListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": [
"arn:aws:s3:::shopeasy-images-{ACCOUNT_ID}",
"arn:aws:s3:::shopeasy-frontend-{ACCOUNT_ID}"
]
}
]
}
- Next를 클릭합니다.
- Policy name:
ShopEasy-S3-Custom을 입력합니다. - Description (선택):
ShopEasy S3 least privilege policy - images and frontend buckets only - Create policy를 클릭합니다.
AWS CLI 풀이
# 계정 ID 확인
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
echo "계정 ID: $ACCOUNT_ID"
# S3 커스텀 정책 JSON 파일 생성
cat > /tmp/shopeasy-s3-custom.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3BucketAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::shopeasy-images-${ACCOUNT_ID}/*",
"arn:aws:s3:::shopeasy-frontend-${ACCOUNT_ID}/*"
]
},
{
"Sid": "S3ListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": [
"arn:aws:s3:::shopeasy-images-${ACCOUNT_ID}",
"arn:aws:s3:::shopeasy-frontend-${ACCOUNT_ID}"
]
}
]
}
EOF
# IAM 정책 생성
aws iam create-policy \
--policy-name ShopEasy-S3-Custom \
--policy-document file:///tmp/shopeasy-s3-custom.json \
--description "ShopEasy S3 least privilege policy - images and frontend buckets only"
# 생성 확인
aws iam get-policy \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/ShopEasy-S3-Custom
echo "S3 커스텀 정책 생성 완료!"
이 정책은 2개의 Statement로 구성됩니다. S3에서는 버킷 수준과 객체 수준의 ARN 형식이 다르기 때문입니다:
- Statement 1 (S3BucketAccess): 버킷 내 객체에 대한 작업
- Resource:
arn:aws:s3:::bucket-name/*(끝에/*있음) - Actions:
GetObject(읽기),PutObject(쓰기),DeleteObject(삭제)
- Resource:
- Statement 2 (S3ListBucket): 버킷 자체에 대한 작업
- Resource:
arn:aws:s3:::bucket-name(끝에/*없음) - Action:
ListBucket(파일 목록 조회)
- Resource:
흔한 실수: 모든 Action을 하나의 Statement에 넣고 Resource에 /*를 붙이면, ListBucket은 동작하지 않습니다. 반대로 /*를 빼면 GetObject 등이 동작하지 않습니다.
Step 3 정답: DynamoDB 커스텀 정책 생성
정답
웹 콘솔 풀이
- IAM → Policies → Create policy를 클릭합니다.
- JSON 탭을 클릭하고, 아래 정책을 붙여넣습니다:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DynamoDBTableAccess",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:DeleteItem",
"dynamodb:UpdateItem"
],
"Resource": "arn:aws:dynamodb:ap-northeast-2:*:table/Reviews"
}
]
}
- Next를 클릭합니다.
- Policy name:
ShopEasy-DynamoDB-Custom을 입력합니다. - Description (선택):
ShopEasy DynamoDB least privilege policy - Reviews table only - Create policy를 클릭합니다.
AWS CLI 풀이
# 계정 ID 확인
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
# DynamoDB 커스텀 정책 JSON 파일 생성
cat > /tmp/shopeasy-dynamodb-custom.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DynamoDBTableAccess",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:DeleteItem",
"dynamodb:UpdateItem"
],
"Resource": "arn:aws:dynamodb:ap-northeast-2:*:table/Reviews"
}
]
}
EOF
# IAM 정책 생성
aws iam create-policy \
--policy-name ShopEasy-DynamoDB-Custom \
--policy-document file:///tmp/shopeasy-dynamodb-custom.json \
--description "ShopEasy DynamoDB least privilege policy - Reviews table only"
# 생성 확인
aws iam get-policy \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/ShopEasy-DynamoDB-Custom
echo "DynamoDB 커스텀 정책 생성 완료!"
DynamoDB 정책은 S3보다 간단합니다. 1개의 Statement로 충분합니다.
- Resource:
arn:aws:dynamodb:ap-northeast-2:*:table/Reviewsap-northeast-2: 서울 리전만 허용*: 계정 ID (와일드카드 사용. 실무에서는 정확한 계정 ID를 쓰는 것이 더 안전)table/Reviews: Reviews 테이블만 허용
- Actions: 데이터 CRUD에 필요한 6가지 작업만 허용
PutItem: 리뷰 작성 (Create)GetItem: 단일 리뷰 조회 (Read)Query: 특정 상품의 리뷰 조회 (Read)Scan: 전체 리뷰 조회 (Read)UpdateItem: 리뷰 수정 (Update)DeleteItem: 리뷰 삭제 (Delete)
허용하지 않는 위험한 작업들: dynamodb:CreateTable, dynamodb:DeleteTable, dynamodb:DescribeTable 등 테이블 관리 작업은 포함하지 않습니다. EC2 API 서버는 데이터 CRUD만 하면 되므로 테이블 관리 권한은 불필요합니다.
Step 4 정답: FullAccess 정책 교체
정답
웹 콘솔 풀이
4-1. 커스텀 정책 연결 (먼저!)
- IAM → Roles → ShopEasy-EC2-Role을 클릭합니다.
- Permissions 탭에서 Add permissions → Attach policies를 클릭합니다.
- 검색창에
ShopEasy를 입력합니다. - ShopEasy-S3-Custom과 ShopEasy-DynamoDB-Custom에 체크합니다.
- Add permissions를 클릭합니다.
4-2. FullAccess 정책 분리
- 다시 Permissions 탭에서 AmazonS3FullAccess 옆의 Remove 버튼을 클릭합니다.
- 확인 창에서 Remove policy를 클릭합니다.
- 같은 방법으로 AmazonDynamoDBFullAccess도 분리합니다.
4-3. 결과 확인
Permissions 탭에 다음 2개의 정책만 남아 있어야 합니다:
# 정책 교체 후 연결된 정책
Policy name Type
----------------------------- ----------------------
ShopEasy-S3-Custom Customer managed policy
ShopEasy-DynamoDB-Custom Customer managed policy
AWS CLI 풀이
# 계정 ID 확인
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
# === 4-1. 커스텀 정책 연결 (먼저!) ===
echo "=== 커스텀 정책 연결 ==="
# S3 커스텀 정책 연결
aws iam attach-role-policy \
--role-name ShopEasy-EC2-Role \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/ShopEasy-S3-Custom
# DynamoDB 커스텀 정책 연결
aws iam attach-role-policy \
--role-name ShopEasy-EC2-Role \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/ShopEasy-DynamoDB-Custom
echo "커스텀 정책 연결 완료!"
# === 4-2. FullAccess 정책 분리 ===
echo ""
echo "=== FullAccess 정책 분리 ==="
# S3FullAccess 분리
aws iam detach-role-policy \
--role-name ShopEasy-EC2-Role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess
# DynamoDBFullAccess 분리
aws iam detach-role-policy \
--role-name ShopEasy-EC2-Role \
--policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
echo "FullAccess 정책 분리 완료!"
# === 4-3. 결과 확인 ===
echo ""
echo "=== 현재 연결된 정책 확인 ==="
aws iam list-attached-role-policies --role-name ShopEasy-EC2-Role
# 예상 출력:
# {
# "AttachedPolicies": [
# {
# "PolicyName": "ShopEasy-S3-Custom",
# "PolicyArn": "arn:aws:iam::ACCOUNT_ID:policy/ShopEasy-S3-Custom"
# },
# {
# "PolicyName": "ShopEasy-DynamoDB-Custom",
# "PolicyArn": "arn:aws:iam::ACCOUNT_ID:policy/ShopEasy-DynamoDB-Custom"
# }
# ]
# }
4-4. 서비스 동작 테스트
# EC2 퍼블릭 IP를 확인 (EC2 내부에서 실행하는 경우)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
EC2_IP=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/public-ipv4)
# === API 서버 테스트 ===
# 1. 상품 목록 조회 (RDS - IAM 정책과 무관하게 동작해야 함)
echo "=== 상품 목록 조회 테스트 ==="
curl -s http://${EC2_IP}:5000/api/products | head -c 200
echo ""
# 2. 리뷰 조회 (DynamoDB - ShopEasy-DynamoDB-Custom 정책 필요)
echo ""
echo "=== 리뷰 조회 테스트 ==="
curl -s http://${EC2_IP}:5000/api/reviews/1
echo ""
# 3. S3 파일 접근 테스트 (EC2 내부에서 실행)
echo ""
echo "=== S3 접근 테스트 ==="
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
# 허용된 버킷 접근 (성공해야 함)
aws s3 ls s3://shopeasy-images-${ACCOUNT_ID}/ 2>&1 | head -5
echo "shopeasy-images 버킷 접근: 성공"
# 허용되지 않은 버킷 접근 (실패해야 함 - AccessDenied)
aws s3 ls s3://some-other-bucket/ 2>&1 | head -1
echo "다른 버킷 접근: 거부됨 (정상)"
echo ""
echo "=== 테스트 완료 ==="
echo "모든 ShopEasy 기능이 정상 동작하면 최소 권한 적용 성공!"
정책 교체 순서가 중요합니다!
- 먼저 커스텀 정책을 연결: 권한이 누적되므로, 커스텀 정책 + FullAccess가 동시에 존재하는 것은 문제없습니다.
- 그 후 FullAccess를 분리: 커스텀 정책이 이미 필요한 권한을 제공하므로, FullAccess를 분리해도 서비스에 영향이 없습니다.
반대 순서(FullAccess 먼저 분리)로 하면, 커스텀 정책을 연결하기 전에 모든 S3/DynamoDB 권한이 사라져 서비스가 중단될 수 있습니다.
테스트 포인트:
- 상품 목록 조회: RDS를 사용하므로 IAM 정책과 무관합니다. 이 테스트가 실패하면 API 서버 자체에 문제가 있는 것입니다.
- 리뷰 조회: DynamoDB Reviews 테이블을 사용합니다. 커스텀 정책이 올바르면 정상 동작합니다.
- S3 파일 접근: 허용된 ShopEasy 버킷은 접근 가능하고, 다른 버킷은 AccessDenied가 발생해야 합니다.
학생들에게 최소 권한의 효과를 보여주기 위해, EC2에서 다음 명령을 실행하여 차이를 시연하면 효과적입니다:
# EC2에 SSH 접속 후 실행
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
echo "=== 허용된 작업 (성공해야 함) ==="
# 1. ShopEasy 이미지 버킷 목록 조회
aws s3 ls s3://shopeasy-images-${ACCOUNT_ID}/
echo "[OK] shopeasy-images 버킷 목록 조회"
# 2. ShopEasy 프론트엔드 버킷 목록 조회
aws s3 ls s3://shopeasy-frontend-${ACCOUNT_ID}/
echo "[OK] shopeasy-frontend 버킷 목록 조회"
# 3. DynamoDB Reviews 테이블 스캔
aws dynamodb scan --table-name Reviews --max-items 1
echo "[OK] Reviews 테이블 스캔"
echo ""
echo "=== 거부된 작업 (AccessDenied 발생해야 함) ==="
# 4. 다른 S3 버킷 접근 시도
aws s3 ls 2>&1 | head -1
echo "[DENIED] 전체 S3 버킷 목록 조회 거부"
# 5. DynamoDB 테이블 목록 조회 시도
aws dynamodb list-tables 2>&1 | head -3
echo "[DENIED] DynamoDB 테이블 목록 조회 거부"
# 6. DynamoDB 테이블 삭제 시도
aws dynamodb delete-table --table-name Reviews 2>&1 | head -3
echo "[DENIED] Reviews 테이블 삭제 거부"
트러블슈팅 가이드
자주 발생하는 문제와 해결 방법
원인: 커스텀 정책의 Resource ARN이나 Action이 잘못 지정되었습니다.
해결: 정책 JSON을 다시 확인합니다.
# 현재 연결된 정책 확인
aws iam list-attached-role-policies --role-name ShopEasy-EC2-Role
# S3 커스텀 정책 내용 확인
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
POLICY_VERSION=$(aws iam get-policy \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/ShopEasy-S3-Custom \
--query "Policy.DefaultVersionId" --output text)
aws iam get-policy-version \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/ShopEasy-S3-Custom \
--version-id ${POLICY_VERSION} \
--query "PolicyVersion.Document"
# DynamoDB 커스텀 정책 내용 확인
POLICY_VERSION=$(aws iam get-policy \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/ShopEasy-DynamoDB-Custom \
--query "Policy.DefaultVersionId" --output text)
aws iam get-policy-version \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/ShopEasy-DynamoDB-Custom \
--version-id ${POLICY_VERSION} \
--query "PolicyVersion.Document"
체크포인트:
- S3 정책에서
GetObject,PutObject,DeleteObject의 Resource에/*가 있는가? - S3 정책에서
ListBucket의 Resource에/*가 없는가? - 버킷 이름에 오타가 없는가? (
shopeasy-images-{ACCOUNT_ID}형식) - DynamoDB 정책의 테이블 이름이 정확히
Reviews인가? (대소문자 구분) - DynamoDB 정책의 리전이
ap-northeast-2인가?
원인: 정책 JSON 형식이 올바르지 않습니다.
해결: JSON 문법을 확인합니다.
# JSON 문법 검증
cat /tmp/shopeasy-s3-custom.json | python3 -m json.tool
# 오류가 있으면 다음 사항을 확인:
# 1. 쉼표(,) 위치 - 마지막 항목 뒤에 쉼표가 있으면 안 됨
# 2. 중괄호({}) 및 대괄호([]) 짝이 맞는지
# 3. "Version"이 "2012-10-17"인지
# 4. "Effect"가 "Allow" 또는 "Deny"인지
# 5. ARN 형식이 올바른지
흔한 JSON 실수:
- 마지막 항목 뒤에 쉼표:
"s3:DeleteObject",(X) →"s3:DeleteObject"(O) - 작은따옴표 사용:
's3:GetObject'(X) →"s3:GetObject"(O) - Version 오타:
"2012-10-7"(X) →"2012-10-17"(O)
원인: 커스텀 정책을 연결하기 전에 FullAccess 정책을 분리했습니다.
해결: 임시로 FullAccess 정책을 다시 연결합니다.
# FullAccess 정책 긴급 재연결
aws iam attach-role-policy \
--role-name ShopEasy-EC2-Role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess
aws iam attach-role-policy \
--role-name ShopEasy-EC2-Role \
--policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
echo "FullAccess 정책 재연결 완료!"
echo "이제 커스텀 정책을 먼저 연결한 후, FullAccess를 분리하세요."
올바른 순서:
- 커스텀 정책 연결 (Attach) → 이 시점에서 4개 정책 동시 존재 (정상)
- FullAccess 정책 분리 (Detach) → 커스텀 정책 2개만 남음 (목표 상태)
원인: EC2 인스턴스의 IAM 역할 자격증명(credentials)이 캐시되어 있습니다. EC2 메타데이터 서비스는 자격증명을 최대 6시간까지 캐시합니다.
해결: 1~2분 기다린 후 다시 테스트합니다. 대부분의 경우 몇 초~몇 분 내에 반영됩니다.
# EC2에서 현재 사용 중인 자격증명 확인
aws sts get-caller-identity
# 잠시 후 다시 테스트
sleep 60
# 다시 S3 접근 테스트
aws s3 ls 2>&1 | head -3
# AccessDenied가 발생해야 최소 권한이 올바르게 적용된 것
원인: S3 커스텀 정책에서 리뷰 이미지 버킷 이름이 정확하지 않거나, s3:PutObject 권한이 누락되었습니다.
해결:
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
# 1. 실제 S3 버킷 이름 확인
aws s3 ls | grep shopeasy
# 2. 정책의 Resource와 실제 버킷 이름이 일치하는지 확인
# 정책에 지정된 버킷: shopeasy-images-{ACCOUNT_ID}
# 실제 버킷 이름을 정책과 비교
# 3. PutObject 테스트
echo "test" > /tmp/test-upload.txt
aws s3 cp /tmp/test-upload.txt s3://shopeasy-images-${ACCOUNT_ID}/test-upload.txt
# 성공하면 PutObject 권한 정상
# 4. 테스트 파일 삭제
aws s3 rm s3://shopeasy-images-${ACCOUNT_ID}/test-upload.txt
학생이 문제를 겪을 때 다음 순서로 디버깅하면 효율적입니다:
- 연결된 정책 확인:
aws iam list-attached-role-policies --role-name ShopEasy-EC2-Role - 정책 JSON 확인: Resource ARN과 Action이 올바른지 확인
- 버킷/테이블 이름 확인: 실제 리소스 이름과 정책의 ARN이 일치하는지 확인
- S3 ARN 형식:
ListBucket과GetObject의 ARN 형식 차이 확인 - 캐시 대기: 정책 변경 후 1~2분 대기
학생들에게 변경 전후의 차이를 요약하여 보여주세요:
| 항목 | 변경 전 (FullAccess) | 변경 후 (커스텀) |
|---|---|---|
| S3 접근 범위 | 모든 버킷 | shopeasy 버킷 2개만 |
| S3 허용 작업 | s3:* (모든 작업) | Get/Put/Delete/List 4개만 |
| DynamoDB 접근 범위 | 모든 테이블 | Reviews 테이블만 |
| DynamoDB 허용 작업 | dynamodb:* (모든 작업) | CRUD 6개만 |
| 테이블 삭제 가능? | 가능 (위험!) | 불가능 |
| 다른 서비스 버킷 접근? | 가능 (위험!) | 불가능 |
| 서비스 동작 | 정상 | 정상 (동일) |
| 침해 시 피해 범위 | AWS 계정 전체 | ShopEasy 서비스만 |