- Published on
AWS S3 403 AccessDenied - 버킷정책·SCP 10분 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
S3에서 403 AccessDenied가 발생하면 대부분 “권한이 없다”로 뭉뚱그려 끝나지만, 실제 원인은 여러 레이어에 걸쳐 있습니다. 특히 조직 단위로 SCP를 쓰거나, Block Public Access가 켜져 있거나, KMS 암호화를 쓰는 버킷이라면 IAM만 보고는 절대 해결되지 않습니다.
이 글은 10분 안에 원인을 좁히는 진단 루틴을 목표로 합니다. 콘솔 클릭 위주가 아니라, 재현 가능하고 로그가 남는 AWS CLI 중심으로 정리합니다.
0분: 에러 형태부터 분류하기
먼저 “어떤 S3 API에서 403이 나는지”가 중요합니다. 같은 AccessDenied라도 원인이 달라집니다.
ListBucket계열: 버킷 목록/프리픽스 조회에서 403GetObject계열: 객체 다운로드에서 403PutObject계열: 업로드에서 403HeadObject계열: 존재 확인에서 403
가장 빠른 방법은 동일 요청을 CLI로 재현해 정확한 API와 리소스 ARN을 확정하는 것입니다.
aws s3api head-object \
--bucket my-bucket \
--key path/to/file.txt
aws s3api get-object \
--bucket my-bucket \
--key path/to/file.txt \
/tmp/file.txt
aws s3api list-objects-v2 \
--bucket my-bucket \
--prefix path/
여기서 list-objects-v2만 막히면 s3:ListBucket(버킷 ARN) 권한이 빠진 케이스가 매우 흔합니다.
1분: “내가 누구로 호출 중인지”부터 고정
S3 권한 이슈의 절반은 “권한이 없어서”가 아니라 “내가 생각한 자격증명이 아니라서”입니다. 특히 로컬, CI, ECS, EKS, Lambda가 섞이면 더 자주 발생합니다.
aws sts get-caller-identity
aws configure list
get-caller-identity의Account와Arn을 메모합니다.- AssumeRole을 쓴다면
Arn에assumed-role이 보입니다. - CI라면 OIDC로 발급된 Role인지 확인해야 합니다.
관련해서 GitHub Actions OIDC를 쓰는 경우에는 아래 글의 “정책은 맞는데 계속 AccessDenied” 패턴이 S3에서도 그대로 재발합니다.
2분: IAM 정책에서 “Allow만” 보지 말고 Deny를 찾아라
S3는 최종 평가에서 명시적 Deny가 하나라도 있으면 무조건 Deny입니다.
2-1. 붙어 있는 정책 빠르게 열람
aws iam list-attached-user-policies --user-name my-user
aws iam list-attached-role-policies --role-name my-role
aws iam list-role-policies --role-name my-role
2-2. 정책 시뮬레이터로 액션 단위 확인
iam simulate-principal-policy는 “이 주체가 이 액션을 이 리소스에 할 수 있나”를 빠르게 확인할 수 있습니다.
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/MyRole \
--action-names s3:GetObject s3:PutObject s3:ListBucket \
--resource-arns \
arn:aws:s3:::my-bucket \
arn:aws:s3:::my-bucket/*
주의할 점:
s3:ListBucket은 리소스가arn:aws:s3:::버킷명(버킷 ARN)입니다.s3:GetObject/s3:PutObject는arn:aws:s3:::버킷명/*(오브젝트 ARN)입니다.
이 둘을 뒤섞으면 “Allow 했는데도 안 됨”처럼 보입니다.
4분: 버킷 정책에서 교차 계정, 프리픽스 제한, 조건식을 확인
IAM이 Allow라도 버킷 정책이 Deny면 끝입니다. 또한 교차 계정 액세스는 버킷 정책이 사실상 필수인 경우가 많습니다.
4-1. 버킷 정책 확인
aws s3api get-bucket-policy --bucket my-bucket --query Policy --output text
4-2. 흔한 Deny 패턴
(1) TLS 강제 Deny
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {"aws:SecureTransport": "false"}
}
}
이 경우 HTTPS가 아닌 요청(또는 프록시/SDK 설정 문제)이면 403이 납니다.
(2) 특정 VPC 엔드포인트만 허용
{
"Sid": "DenyUnlessFromVpce",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"StringNotEquals": {"aws:sourceVpce": "vpce-abc123"}
}
}
로컬이나 다른 네트워크에서 접근하면 무조건 403입니다.
(3) 프리픽스 단위로만 허용
arn:aws:s3:::my-bucket/project-a/*만 허용했는데 project-b/에 접근하면 403이 납니다.
6분: S3 Block Public Access가 정책을 “무력화”하는지 확인
Block Public Access는 퍼블릭 ACL뿐 아니라 특정 형태의 퍼블릭 버킷 정책도 차단합니다. “나는 퍼블릭으로 연 게 아닌데?”라고 생각해도, 버킷 정책의 Principal이 *인 순간 퍼블릭으로 간주될 수 있습니다.
aws s3api get-public-access-block --bucket my-bucket
체크 포인트:
BlockPublicPolicy가true인데 버킷 정책이 퍼블릭으로 해석되면 적용이 거부되거나 접근이 막힙니다.- CloudFront OAC/OAI, 특정 AWS 서비스 접근을 위해 Principal을 넓게 열어둔 정책이 의도치 않게 걸리는 경우가 있습니다.
7분: 조직(Organizations) SCP로 막히는지 확인
SCP는 계정 내 IAM Allow보다 “위”에서 동작합니다. 즉 IAM에서 Allow여도 SCP에서 Deny면 403입니다. 특히 보안 조직에서 아래 같은 가드레일을 자주 둡니다.
- 특정 리전 외 S3 사용 금지
- 암호화 없는 PutObject 금지
- 특정 버킷 패턴 접근 금지
7-1. SCP 의심 시 빠른 징후
- 같은 Role로 다른 버킷은 되는데 특정 버킷만 안 됨
- 같은 버킷인데 특정 액션(예:
PutObject)만 안 됨 - 계정 전체에서 일관되게 막힘
SCP는 CLI로 “내가 어떤 SCP를 상속받는지”를 한 번에 보기 어렵고, 보통 AWS Organizations 콘솔에서 추적합니다. 하지만 실무적으로는 **CloudTrail 이벤트의 errorCode와 eventName**을 먼저 보고, 그 다음 보안팀/플랫폼팀에 “이 액션이 SCP로 Deny된 것 같은데 확인 부탁”이라고 증거를 주는 방식이 가장 빠릅니다.
CloudTrail Lake나 Athena가 없다면, 최소한 이벤트 히스토리에서 S3 데이터 이벤트가 기록되는지(설정 필요)도 확인하세요.
8분: KMS 암호화(SSE-KMS)면 S3 권한만으로는 부족
버킷이 SSE-KMS를 사용하면, s3:GetObject가 있어도 KMS 키에 대한 권한이 없으면 403이 납니다.
8-1. 객체/버킷의 기본 암호화 확인
aws s3api get-bucket-encryption --bucket my-bucket
8-2. 필요한 KMS 권한 예시
대략 아래 권한이 필요합니다(정확히는 워크로드에 따라 다름).
- 다운로드:
kms:Decrypt - 업로드:
kms:Encrypt,kms:GenerateDataKey
IAM 정책에 추가하는 예시:
{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/11111111-2222-3333-4444-555555555555"
}
그리고 KMS Key Policy에서도 해당 Role을 허용해야 합니다. “IAM에 넣었는데도 안 됨”이면 Key Policy가 막고 있는 경우가 많습니다.
9분: 요청이 “다른 리전 엔드포인트”로 가고 있지 않은가
S3는 글로벌 서비스처럼 보이지만 버킷은 리전에 귀속됩니다. SDK나 CLI가 잘못된 리전으로 호출하면 리다이렉트/서명 불일치가 나기도 하고, 상황에 따라 403으로 보일 수 있습니다.
aws s3api get-bucket-location --bucket my-bucket
CLI에서 리전을 고정해 재시도합니다.
aws s3api head-object \
--region ap-northeast-2 \
--bucket my-bucket \
--key path/to/file.txt
10분: “최소 재현 정책”으로 원인을 절단한다
여기까지 봤는데도 애매하면, 권한을 크게 열기 전에 최소 재현용 정책으로 레이어를 분리하는 게 빠릅니다.
10-1. 테스트 전용 프리픽스 만들기
s3://my-bucket/_debug-access/같은 프리픽스를 하나 잡습니다.- 기존 정책의 조건(프리픽스 제한, VPCe 제한 등)을 건드리지 않고, 테스트 프리픽스에만 임시 허용을 추가합니다.
10-2. 버킷 정책에 특정 Role만 임시 허용 예시
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowDebugPrefixForSpecificRole",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/MyRole"
},
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-bucket/_debug-access/*"
}
]
}
이 상태에서 아래를 실행해 성공/실패를 분리합니다.
echo "test" > /tmp/s3-debug.txt
aws s3 cp /tmp/s3-debug.txt s3://my-bucket/_debug-access/s3-debug.txt
aws s3 cp s3://my-bucket/_debug-access/s3-debug.txt /tmp/s3-debug-downloaded.txt
- 여기서도 실패하면 IAM 밖(SCP, KMS, VPCe, Block Public Access 등) 가능성이 급상승합니다.
- 여기서 성공하면 기존 프리픽스 정책/조건식 문제일 확률이 큽니다.
자주 보는 원인별 증상-처방 요약
ListBucket만 403
- 증상:
aws s3 ls s3://my-bucket/실패, 하지만 객체 경로를 정확히 찍으면 되는 듯 보임 - 처방:
s3:ListBucket를 버킷 ARN에 Allow, 필요 시s3:prefix조건 확인
업로드만 403
- 처방 1: SSE-KMS면 KMS 권한과 Key Policy 확인
- 처방 2: SCP에서 “암호화 없는 PutObject Deny” 가드레일 확인
- 처방 3: 버킷 정책에서
s3:x-amz-server-side-encryption조건식 확인
특정 네트워크에서만 403
- 처방: 버킷 정책의
aws:sourceVpce,aws:SourceIp,aws:PrincipalArn조건 확인
운영 팁: AccessDenied를 “증거 기반”으로 줄이는 방법
- CloudTrail에 S3 데이터 이벤트를 필요한 버킷에만 켜고, 403을 빠르게 추적할 수 있게 합니다.
- 버킷 정책은 Deny를 쓸 때
Sid를 구체적으로 작성해, 나중에 로그에서 사람이 바로 이해할 수 있게 합니다. - 조직에서 SCP를 강하게 쓰는 환경이라면, 앱 팀이 셀프 디버깅할 수 있도록 “허용된 S3 패턴”과 “필수 암호화 규칙”을 문서화합니다.
비슷한 톤으로 “짧은 시간 안에 원인 좁히기” 스타일의 진단 글이 필요하다면, 아래 글의 체크리스트 구성도 참고가 됩니다.
결론: 403은 한 군데만 보지 말고 레이어로 잘라라
S3의 403 AccessDenied는 IAM 정책만으로는 설명이 안 되는 경우가 많습니다. 호출 주체(STS) 고정 → IAM Deny 탐색 → 버킷 정책 조건식 → Block Public Access → SCP → KMS → 리전 순으로 레이어를 잘라가면, 대부분 10분 안에 “어디서 막혔는지”까지는 도달합니다.
마지막으로, 해결 과정에서 “정책을 크게 열어놓고 나중에 줄이기”는 보안 사고로 이어지기 쉽습니다. 위의 _debug-access처럼 테스트 프리픽스 기반 최소 재현으로 원인을 분리한 뒤, 필요한 액션과 리소스만 정확히 허용하는 방식이 가장 안전하고 빠릅니다.