- Published on
AWS S3 AccessDenied 403 - IAM·버킷정책·KMS 점검
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버나 배치에서 S3를 호출하다가 AccessDenied 와 403 을 만나면, 대부분은 “권한 부족”으로 뭉뚱그려 생각합니다. 하지만 S3의 권한 결정은 IAM 정책, 버킷 정책, ACL/오브젝트 소유권, 퍼블릭 액세스 차단(PAB), SSE-KMS 키 정책까지 겹치며, 여기에 VPC Endpoint 정책이나 Organizations SCP 같은 상위 정책이 더해지면 원인 추적이 급격히 어려워집니다.
이 글은 “무엇을 어디서부터 확인해야 하는지”를 재현 가능하고 로그 기반으로 정리한 체크리스트입니다. 특히 403 AccessDenied 가 GetObject 에서 나는지, PutObject 에서 나는지, 혹은 ListBucket 에서 나는지에 따라 필요한 권한이 다르므로, API 단위로 분해해서 접근합니다.
1) 먼저: 어떤 API가 막혔는지부터 고정하기
S3 403은 동일해 보여도, 실제로는 서로 다른 권한 세트가 필요합니다.
s3:ListBucket는 버킷 ARN 에 적용됩니다:arn:aws:s3:::my-buckets3:GetObject,s3:PutObject는 오브젝트 ARN 에 적용됩니다:arn:aws:s3:::my-bucket/path/to/key
CLI로 문제를 최소 재현해 보세요.
aws s3api head-object --bucket my-bucket --key path/to/key
aws s3api get-object --bucket my-bucket --key path/to/key /tmp/out
aws s3api list-objects-v2 --bucket my-bucket --max-items 1
head-object 가 막히면 대개 s3:GetObject 또는 KMS 권한 이슈가 많고, list-objects-v2 만 막히면 s3:ListBucket 가 빠진 경우가 흔합니다.
CloudTrail 이벤트로 “정확히” 확인
가장 빠른 방법은 CloudTrail에서 해당 시간대 이벤트를 보는 것입니다.
- Event source:
s3.amazonaws.com - Error code:
AccessDenied - Event name:
GetObject,PutObject,ListBucket,HeadObject등
이때 requestParameters.bucketName 과 requestParameters.key 를 보면 “버킷 레벨”인지 “오브젝트 레벨”인지 확정할 수 있습니다.
2) IAM 정책: Allow가 있어도 Deny가 있으면 끝
S3 권한 평가는 “명시적 Deny가 최우선”입니다. 즉, 아래 중 하나라도 Deny면 403이 납니다.
- IAM 사용자/역할 정책의
Deny - Permission Boundary의
Deny - Organizations SCP의
Deny - 버킷 정책의
Deny - VPC Endpoint 정책의
Deny
IAM에서 자주 빠지는 권한 조합
예: 특정 prefix만 리스트하고 객체를 읽고 싶을 때
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucketWithPrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-bucket",
"Condition": {
"StringLike": {
"s3:prefix": ["images/*"]
}
}
},
{
"Sid": "ReadObjects",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/images/*"
}
]
}
포인트는 ListBucket 은 버킷 ARN, GetObject 는 오브젝트 ARN 이라는 점입니다. 이 매핑이 틀리면 계속 403이 납니다.
STS AssumeRole 환경이면 “누가 호출했는지”를 확인
ECS/EKS/EC2에서 역할을 쓰는 경우, 코드에 박힌 자격증명이 아니라 실제 실행 환경의 Role 이 호출합니다.
aws sts get-caller-identity
여기서 나온 Arn 이 기대한 역할인지부터 확인하세요.
3) 버킷 정책: 조건(Condition) 때문에 막히는 경우가 매우 흔함
버킷 정책은 IAM Allow가 있어도 추가로 막을 수 있습니다. 특히 아래 조건이 자주 원인입니다.
aws:PrincipalArn또는aws:userid조건이 불일치aws:SourceVpce(VPC Endpoint 강제)aws:SourceIp(고정 IP만 허용)s3:x-amz-server-side-encryption(SSE 강제)s3:signatureversion또는 TLS 강제
예: 특정 VPC Endpoint에서만 접근 허용
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyWithoutVPCEndpoint",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-abc123"
}
}
}
]
}
이 경우 로컬 개발 환경이나 다른 네트워크에서 호출하면 무조건 403입니다.
퍼블릭 액세스 차단(PAB)과의 상호작용
버킷 정책에서 퍼블릭을 열어두었더라도, S3의 Block Public Access 설정이 켜져 있으면 의도와 다르게 차단될 수 있습니다.
BlockPublicAclsIgnorePublicAclsBlockPublicPolicyRestrictPublicBuckets
특히 BlockPublicPolicy 가 켜져 있으면 “퍼블릭 허용 버킷 정책”이 무시되어 403처럼 보일 수 있습니다.
4) Object Ownership / ACL: 크로스 어카운트에서 403의 핵심
S3는 과거에 ACL이 중요한 축이었고, 지금도 크로스 어카운트 업로드에서는 ACL/소유권이 403의 주요 원인입니다.
대표 시나리오:
- A 계정이 버킷 소유자
- B 계정이
PutObject로 업로드 - 업로드된 객체의 소유자가 B가 되어버림
- A 계정(버킷 소유자)이
GetObject하려다 403
해결의 정석은 Bucket owner enforced 를 사용하는 것입니다.
- S3 콘솔: Object Ownership
Bucket owner enforced - 결과: ACL 비활성화, 버킷 소유자가 객체 소유
이미 운영 중이라면 영향 범위를 먼저 확인해야 합니다.
5) SSE-KMS를 쓰면: S3 권한만으로는 절대 안 됨
x-amz-server-side-encryption: aws:kms 로 암호화된 객체는, 읽기/쓰기 시점에 KMS 권한이 추가로 필요합니다.
읽기(GetObject)에서 필요한 것
- S3:
s3:GetObject - KMS:
kms:Decrypt(그리고 보통kms:DescribeKey)
쓰기(PutObject)에서 필요한 것
- S3:
s3:PutObject - KMS:
kms:Encrypt,kms:GenerateDataKey(그리고 보통kms:DescribeKey)
즉, IAM에 S3 Allow만 넣어도 403이 납니다.
KMS 키 정책(Key policy)이 “진짜 최종 관문”
KMS는 IAM 정책만으로 끝나지 않고, 키 정책이 해당 Principal을 신뢰해야 합니다.
예: 특정 역할에 복호화 허용
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowDecryptForAppRole",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/my-app-role"
},
"Action": [
"kms:Decrypt",
"kms:DescribeKey"
],
"Resource": "*"
}
]
}
또한, 버킷 정책에서 SSE-KMS를 강제해놓고 클라이언트가 SSE-S3로 업로드하면, PutObject 가 403으로 막힐 수 있습니다.
KMS 관련 403을 빠르게 판별하는 법
CloudTrail에서 S3 이벤트뿐 아니라 KMS 이벤트도 같이 보세요.
- Event source:
kms.amazonaws.com - Event name:
Decrypt,Encrypt,GenerateDataKey - Error code:
AccessDenied
S3 403이지만 실은 KMS Deny인 경우가 많습니다.
6) VPC Endpoint(S3 Gateway/Interface) 정책도 별도의 “필터”
사설망에서 S3를 쓰는 구성(EKS 프라이빗, EC2 프라이빗)에서는 VPC Endpoint 정책이 IAM/버킷 정책 위에 한 겹 더 생깁니다.
예: 특정 버킷만 허용하는 Endpoint 정책
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
여기서 PutObject 가 누락되어 있으면 업로드만 403이 납니다.
7) 실전 디버깅 순서(가장 빠른 루트)
운영에서 시간을 아끼려면 아래 순서가 효율적입니다.
- 호출 주체 확정:
aws sts get-caller-identity로 실제 Role/User 확인 - CloudTrail로 실패한 API 확정:
GetObject인지ListBucket인지 확정 - S3 권한 매핑 확인: 버킷 ARN vs 오브젝트 ARN 리소스 범위가 맞는지
- 명시적 Deny 탐색: 버킷 정책, IAM, SCP, Permission Boundary, Endpoint 정책
- 퍼블릭 액세스 차단(PAB) 확인: 정책이 의도대로 먹는지
- SSE-KMS 여부 확인: 객체/버킷의 기본 암호화가 KMS인지, KMS 키 정책/권한 확인
- 크로스 어카운트 소유권/ACL 확인: Object Ownership 설정과 기존 객체 소유자
이 흐름대로 보면 “감”이 아니라 “증거”로 403을 좁힐 수 있습니다.
8) 재현용 최소 예제: Node.js에서 흔한 실수 2가지
(1) ListBucket 없이 prefix 조회 시도
SDK가 내부적으로 ListObjectsV2 를 호출하면 s3:ListBucket 이 필요합니다.
import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "ap-northeast-2" });
const res = await s3.send(
new ListObjectsV2Command({
Bucket: "my-bucket",
Prefix: "images/",
MaxKeys: 10,
})
);
console.log(res.Contents?.map((o) => o.Key));
권한은 반드시 s3:ListBucket 를 버킷 ARN에 줘야 합니다.
(2) SSE-KMS 버킷에 PutObject 하는데 KMS 권한 누락
버킷 기본 암호화가 SSE-KMS면, 애플리케이션 Role에 KMS 권한이 필요합니다.
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "ap-northeast-2" });
await s3.send(
new PutObjectCommand({
Bucket: "my-bucket",
Key: "uploads/a.txt",
Body: "hello",
})
);
이 코드 자체는 문제 없어도, KMS의 kms:GenerateDataKey 가 없으면 403이 날 수 있습니다.
9) 체크리스트 요약
403 AccessDenied는 “S3만”의 문제가 아닐 수 있음- API별로 필요한 권한이 다름:
ListBucket은 버킷 ARN,GetObject는 오브젝트 ARN - Deny가 하나라도 있으면 끝: IAM, 버킷 정책, SCP, Endpoint 정책 모두 확인
- SSE-KMS면 KMS 권한과 키 정책이 필수
- 크로스 어카운트 업로드는 Object Ownership 설정이 핵심
10) 함께 읽으면 좋은 트러블슈팅 글
권한/정책 이슈처럼 원인이 여러 층에 걸친 문제는 “증상 고정 → 관측(로그) → 범위 축소” 패턴이 중요합니다. 같은 방식으로 접근하는 트러블슈팅 사례로 아래 글도 참고할 만합니다.
- EKS에서 Pod는 뜨는데 Service Endpoints가 0일 때
- systemd 서비스 자동 재시작 무한루프 진단 가이드
- Claude Tool Use 400 에러 - JSON 스키마 해결법
운영에서 가장 자주 시간을 잡아먹는 케이스는 “IAM에는 Allow가 있는데 왜 403이지?”입니다. 그때는 거의 항상 버킷 정책의 조건, VPC Endpoint 정책, SSE-KMS 키 정책, SCP/Boundary의 Deny 중 하나가 숨어 있습니다. CloudTrail로 실패 이벤트를 고정하고, 위 순서대로 한 겹씩 벗기면 재현과 해결이 가능합니다.