- Published on
AWS S3 AccessDenied? 버킷정책·KMS 키 7단계
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
S3에서 AccessDenied를 만나면 대부분 “권한이 없나 보다”로 끝내기 쉽지만, 실제 원인은 IAM 정책, 버킷 정책, 퍼블릭 액세스 차단, 조직 SCP, VPC 엔드포인트 정책, 그리고 SSE-KMS 키 정책까지 여러 레이어에서 발생합니다. 특히 SSE-KMS를 쓰는 순간 S3 권한만 맞춰서는 해결되지 않고, KMS 권한과 키 정책이 반드시 함께 맞아야 합니다.
이 글은 실무에서 가장 빠르게 원인을 좁히는 순서대로 7단계로 점검합니다. 각 단계는 “증상 → 확인 방법 → 해결” 흐름으로 구성했습니다.
문제 해결 과정에서 네트워크/타임아웃 이슈가 섞여 보일 때도 많습니다. EKS 워크로드에서 S3 호출이 실패한다면 DNS/네트워크 문제도 함께 확인하세요: EKS CoreDNS 장애? DNS 타임아웃 8단계
0. 먼저 에러 형태를 분류하기
AccessDenied도 상황에 따라 의미가 다릅니다.
AccessDenied(403): 권한/정책 문제 가능성 높음InvalidAccessKeyId/SignatureDoesNotMatch: 자격 증명 또는 서명 문제PermanentRedirect: 리전 엔드포인트/버킷 리전 불일치KMS.AccessDeniedException: KMS 권한 또는 키 정책 문제
가장 먼저 어떤 API에서 터지는지 확인해야 합니다.
ListBucket에서 터지면: 버킷 레벨 권한(s3:ListBucket) 문제GetObject에서 터지면: 오브젝트 레벨 권한(s3:GetObject) 문제PutObject에서 터지면:s3:PutObject+ (SSE-KMS면) KMS 권한 문제
빠른 재현 커맨드
아래 예시는 AWS CLI 기준입니다.
aws s3api head-bucket --bucket my-bucket
aws s3api list-objects-v2 --bucket my-bucket --max-items 1
aws s3api head-object --bucket my-bucket --key path/to/file.txt
에러 메시지에 x-amz-request-id가 찍히면 CloudTrail에서 추적할 때 도움이 됩니다.
1단계: 호출 주체(Principal)와 자격 증명부터 확정
가장 흔한 실수는 “내가 A 역할로 실행 중이라고 믿지만 실제로는 B 자격 증명으로 호출”하는 경우입니다.
확인
aws sts get-caller-identity
aws configure list
AssumedRole인지,User인지 확인- CI/CD라면 OIDC로 AssumeRole 된 역할 ARN 확인
- EKS라면 IRSA(ServiceAccount)로 AssumeRole 되는 역할 확인
해결 포인트
- 로컬:
AWS_PROFILE이 맞는지 확인 - GitHub Actions:
aws-actions/configure-aws-credentials에서 role ARN과 audience 확인 - EKS IRSA: 서비스어카운트 어노테이션의 role ARN 및 토큰 audience 확인
2단계: IAM 정책에서 필요한 S3 권한이 “정확히” 있는지
IAM 정책은 “대충 S3FullAccess”가 아니라면, 액션/리소스가 정확히 매칭되어야 합니다. 특히 ListBucket은 버킷 ARN, GetObject는 오브젝트 ARN을 써야 합니다.
최소 권한 예시
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::my-bucket"
},
{
"Sid": "ObjectRW",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
자주 틀리는 지점
s3:ListBucket에arn:aws:s3:::my-bucket/*를 넣음s3:GetObject에arn:aws:s3:::my-bucket만 넣음- prefix 제한을 Condition으로 걸었는데 실제 키가 해당 prefix가 아님
3단계: 버킷 정책(Bucket Policy)의 Deny가 이기고 있지 않은지
S3는 명시적 Deny가 최우선입니다. IAM에서 Allow가 있어도 버킷 정책에 Deny가 있으면 무조건 막힙니다.
확인
- 버킷 정책에서
Effect가Deny인 Statement 존재 여부 - Condition에
aws:PrincipalArn,aws:SourceVpce,aws:SecureTransport,s3:x-amz-server-side-encryption등이 걸려 있는지
대표적인 Deny 패턴
- HTTPS 강제
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": { "aws:SecureTransport": "false" }
}
}
- 특정 VPC 엔드포인트에서만 허용
{
"Sid": "DenyNotFromVPCE",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"StringNotEquals": { "aws:SourceVpce": "vpce-1234567890abcdef0" }
}
}
이 경우, 인터넷 경로로 접근하면 무조건 AccessDenied입니다.
4단계: S3 퍼블릭 액세스 차단(Public Access Block)과 ACL 오해
S3 퍼블릭 액세스 차단은 “버킷 정책/ACL로 퍼블릭을 열어도 막아버리는” 기능입니다. 조직 보안 기준으로 계정 단위로 켜져 있으면, 버킷에서 뭘 해도 공개가 안 됩니다.
확인
- 계정 단위 Public Access Block
- 버킷 단위 Public Access Block
- 오브젝트 ACL을 퍼블릭으로 열려고 했는지
해결
- 퍼블릭 공개가 목적이라면 CloudFront + OAC 같은 방식으로 우회하는 게 일반적
- 단순히 내부 서비스 접근이라면 퍼블릭을 열 생각을 버리고, IAM/버킷 정책으로만 제어
5단계: SSE-KMS 사용 시 KMS 권한이 빠졌는지 (가장 흔한 함정)
버킷이 SSE-KMS를 강제하거나, 업로드 시 --ssekms-key-id를 쓰면 KMS 권한이 없어서 AccessDenied가 납니다.
필요한 권한은 크게 2종류입니다.
- S3 권한:
s3:GetObject,s3:PutObject등 - KMS 권한:
kms:Decrypt,kms:Encrypt,kms:GenerateDataKey등
IAM에 KMS 권한 추가 예시
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowUseKMSForS3",
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:ap-northeast-2:111122223333:key/12345678-1234-1234-1234-1234567890ab"
}
]
}
업로드 재현
aws s3api put-object \
--bucket my-bucket \
--key test.txt \
--body ./test.txt \
--server-side-encryption aws:kms \
--ssekms-key-id arn:aws:kms:ap-northeast-2:111122223333:key/12345678-1234-1234-1234-1234567890ab
여기서 AccessDenied가 나면, 다음 단계(키 정책)를 반드시 봐야 합니다.
6단계: KMS 키 정책(Key Policy)에서 Principal이 허용되어 있는지
KMS는 “IAM에서 허용해도 키 정책에서 막으면 끝”인 대표 서비스입니다. 즉, 키 정책이 최종 관문인 경우가 많습니다.
확인
- KMS 키 정책에 해당 Role/User가 포함되는지
- 교차 계정이라면 외부 계정 Principal 허용이 있는지
- 키 정책에
kms:ViaService조건으로 S3 경유만 허용했는데 리전/서비스명이 안 맞는지
S3 경유 사용을 허용하는 키 정책 힌트
키 정책에서 S3를 통해서만 사용되도록 제한하는 패턴이 있습니다. 이때 조건이 잘못되면 전부 막힙니다.
kms:ViaService값은 보통s3.ap-northeast-2.amazonaws.com같은 형태- 리전이 다르면 실패
또 한 가지: S3가 KMS를 호출할 수 있도록 키 정책에 AWS 서비스/역할 사용을 적절히 열어야 합니다.
7단계: 조직 SCP, Permission Boundary, VPC Endpoint Policy, S3 Object Ownership
여기까지 왔는데도 AccessDenied라면 “조직/네트워크/소유권” 레이어를 봐야 합니다.
7-1. AWS Organizations SCP
SCP는 계정의 최대 권한을 깎습니다. IAM에서 Allow여도 SCP가 Deny하면 실패합니다.
- 조직에서
s3:*또는kms:*제한이 있는지 - 특정 리전만 허용하는 SCP인지
7-2. Permission Boundary
Role에 Permission Boundary가 붙으면, 그 경계 밖 권한은 무시됩니다.
aws iam get-role로 boundary 확인
7-3. VPC Endpoint Policy (Gateway/Interface)
S3 Gateway Endpoint를 쓰는 환경에서는 Endpoint Policy가 또 하나의 필터가 됩니다.
- Endpoint Policy가 특정 버킷만 허용하는지
- 특정 액션만 허용하는지
7-4. S3 Object Ownership / ACL 비활성화
Bucket owner enforced인 버킷은 ACL을 사실상 쓰지 못합니다. 다른 계정이 업로드한 오브젝트의 소유권/권한 모델이 기대와 다르면 접근이 막힐 수 있습니다.
- 교차 계정 업로드라면
bucket-owner-full-controlACL을 기대했는데 정책/설정이 달라졌는지 - 가장 안전한 방식은 버킷 정책으로 교차 계정 PutObject를 통제하고, Object Ownership 정책을 명확히 정하는 것입니다.
CloudTrail로 “정확히 무엇이 Deny했는지” 역추적
S3/KMS 권한 문제는 추측하면 시간이 오래 걸립니다. CloudTrail에서 이벤트를 보면 errorCode, userIdentity, requestParameters, 그리고 경우에 따라 additionalEventData로 힌트를 줍니다.
CloudTrail Lake 또는 Event history에서 찾기
- Event source:
s3.amazonaws.com또는kms.amazonaws.com - Error code:
AccessDenied,AccessDeniedException
KMS 쪽 이벤트가 찍히면 “S3는 통과했는데 KMS에서 막힌 것”일 가능성이 큽니다.
실무용 체크리스트 요약 (7단계)
sts get-caller-identity로 Principal 확정- IAM 정책에서
ListBucket과GetObject리소스 ARN 정확성 확인 - 버킷 정책의 명시적 Deny/Condition 확인
- Public Access Block 및 ACL 전제 오해 제거
- SSE-KMS면 IAM에
kms:Decrypt/kms:GenerateDataKey등 추가 - KMS 키 정책에서 Principal 및
kms:ViaService조건 확인 - SCP/Permission Boundary/VPC Endpoint Policy/Object Ownership 확인
부록: 디버깅할 때 유용한 AWS CLI 옵션
요청 서명/리다이렉트/권한 관련 단서를 보기 위해 --debug가 꽤 유용합니다.
aws s3api get-object --bucket my-bucket --key path/to/file.txt /tmp/file.txt --debug
다만 로그에 민감정보가 섞일 수 있으니, CI 로그나 공유 채널에 그대로 붙여넣지는 마세요.
마무리: “S3 권한”이 아니라 “경로 전체”를 맞추기
S3 AccessDenied는 단일 설정이 아니라 정책 체인의 합성 결과입니다. 특히 SSE-KMS를 쓰는 환경에서는 “S3 정책은 맞는데 KMS가 막는” 경우가 압도적으로 많습니다. 위 7단계를 순서대로 밟으면, 보통 10~20분 내에 원인을 특정할 수 있습니다.
운영 환경에서 이런 권한 이슈는 재시도 폭주나 타임아웃으로 2차 장애를 만들기도 합니다. 호출 실패가 누적될 때는 재시도/데드라인 전략도 함께 점검하세요: gRPC MSA에서 데드라인·재시도 폭주 막는 법