Lambda에서 S3 객체를 읽으려는데
AccessDenied가 반환됩니다. IAM Role에s3:GetObject를 붙였는데 왜 안 될까요? S3 AccessDenied는 권한 평가 계층이 여러 겹이라 원인을 특정하기 어렵습니다. 이 글에서는 원인별 진단 흐름과 해결 방법을 정리합니다.
핵심 요약
| 원인 | 증상 | 확인 방법 |
|---|---|---|
| IAM Policy에 Action/Resource 누락 | 모든 S3 요청에서 403 | IAM Policy Simulator 또는 aws iam simulate-principal-policy |
| Bucket Policy에 명시적 Deny | 특정 조건에서만 403 | Bucket Policy의 Deny Statement 확인 |
| Block Public Access 활성화 | Public ACL/Policy 설정 시 403 | S3 콘솔 > Permissions > Block Public Access |
| Cross-Account 접근 시 양쪽 허용 누락 | 다른 계정에서 접근 시 403 | 요청자 IAM + 버킷 소유자 Bucket Policy 양쪽 확인 |
| VPC Endpoint Policy 제한 | VPC 내부에서만 403 | VPC Endpoint Policy의 Action/Resource 확인 |
| KMS 암호화 객체 접근 | 특정 객체에서만 403 | 객체 암호화 설정 + KMS Key Policy 확인 |
| SCP(Organizations) 제한 | 계정 전체에서 S3 차단 | Organizations > SCP 정책 확인 |
1. S3 권한 평가 구조
S3 AccessDenied를 진단하려면 먼저 AWS가 S3 요청을 어떤 순서로 평가하는지 이해해야 합니다.
S3 요청이 들어오면 AWS는 여러 계층의 정책을 동시에 평가합니다. 하나라도 명시적 Deny가 있으면 즉시 거부되고, 모든 계층에서 Allow가 있어야 접근이 허용됩니다.
평가 대상 정책:
- SCP (Service Control Policy): Organizations 레벨에서 계정 전체에 적용되는 상한선
- Bucket Policy: 버킷 단위로 적용되는 리소스 기반 정책
- IAM Policy: 요청자(User/Role)에 부여된 자격 증명 기반 정책
- ACL (Access Control List): 레거시 방식의 접근 제어 (신규 버킷에서는 비활성화 권장)
- VPC Endpoint Policy: VPC Endpoint를 경유할 때 적용되는 추가 필터
- KMS Key Policy: SSE-KMS로 암호화된 객체 접근 시 별도 평가
실무에서 가장 흔한 실수는 IAM Policy만 확인하고 나머지 계층을 놓치는 것입니다.
2. 원인별 진단 흐름
아래 플로우차트를 따라가면 대부분의 AccessDenied 원인을 좁힐 수 있습니다.
3. 원인 1: IAM Policy에 Action 또는 Resource 누락
가장 흔한 원인입니다. IAM Policy에 필요한 Action이 없거나, Resource ARN이 정확하지 않은 경우입니다.
실무 시나리오
Lambda 함수에서 S3 객체를 읽으려고 합니다. IAM Role에 아래 정책을 붙였는데 AccessDenied가 발생합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket"
}
]
}
문제: Resource가 버킷 ARN(arn:aws:s3:::my-bucket)만 지정되어 있습니다. s3:GetObject는 객체 수준 Action이므로 arn:aws:s3:::my-bucket/*처럼 객체 경로를 포함해야 합니다.
해결 방법
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
버킷 수준 vs 객체 수준 Action 구분
| Action 유형 | Resource 형식 | 예시 |
|---|---|---|
| 버킷 수준 | arn:aws:s3:::bucket-name |
s3:ListBucket, s3:GetBucketPolicy |
| 객체 수준 | arn:aws:s3:::bucket-name/* |
s3:GetObject, s3:PutObject, s3:DeleteObject |
s3:ListBucket과 s3:GetObject를 함께 사용하려면 Resource를 두 줄로 분리해야 합니다:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-bucket"
},
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
진단 명령어
# 현재 호출자 확인
aws sts get-caller-identity
# IAM Policy Simulator로 권한 테스트
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/my-lambda-role \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::my-bucket/test.txt
4. 원인 2: Bucket Policy의 명시적 Deny
Bucket Policy에 "Effect": "Deny" 조건이 있으면, IAM Policy에서 Allow를 부여해도 Deny가 우선합니다.
실무 시나리오
보안팀이 특정 IP 대역 외부에서의 접근을 차단하는 Bucket Policy를 설정했습니다. 개발자가 사무실 외부(VPN 미연결)에서 접근하면 AccessDenied가 발생합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyOutsideIP",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"NotIpAddress": {
"aws:SourceIp": "203.0.113.0/24"
}
}
}
]
}
주의할 점
명시적 Deny는 어떤 Allow보다 우선합니다. 관리자 권한(AdministratorAccess)을 가진 사용자도 이 Deny에 걸리면 접근할 수 없습니다.
흔한 Deny 조건 패턴
| 조건 | 의미 | 확인 방법 |
|---|---|---|
NotIpAddress: aws:SourceIp |
특정 IP 외 차단 | 요청자 IP 확인 |
StringNotEquals: aws:SourceVpc |
특정 VPC 외 차단 | VPC ID 확인 |
StringNotEquals: aws:PrincipalOrgID |
Organization 외 차단 | Organization ID 확인 |
BoolIfExists: aws:MultiFactorAuthPresent |
MFA 미인증 차단 | MFA 세션 여부 확인 |
StringNotEquals: s3:x-amz-server-side-encryption |
암호화 없는 업로드 차단 | 업로드 시 암호화 헤더 포함 여부 |
진단 명령어
# Bucket Policy 확인
aws s3api get-bucket-policy --bucket my-bucket --output text | python -m json.tool
# CloudTrail에서 Deny 이벤트 확인
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=GetObject \
--max-results 5
5. 원인 3: Block Public Access 설정
2023년 4월 이후 생성된 S3 버킷은 Block Public Access(BPA)가 기본 활성화됩니다. BPA가 활성화된 상태에서 Public ACL이나 Public Bucket Policy를 설정하면 AccessDenied가 발생합니다.
BPA 4가지 설정
| 설정 | 차단 대상 |
|---|---|
BlockPublicAcls |
새로운 Public ACL 추가 차단 |
IgnorePublicAcls |
기존 Public ACL 무시 |
BlockPublicPolicy |
Public Bucket Policy 추가 차단 |
RestrictPublicBuckets |
Public Policy가 있는 버킷의 Cross-Account 접근 제한 |
실무 시나리오
정적 웹사이트 호스팅을 위해 Bucket Policy에 "Principal": "*"를 설정했는데, 정책 저장 자체가 거부됩니다.
An error occurred (AccessDenied) when calling the PutBucketPolicy operation: Access Denied
이 경우 BlockPublicPolicy가 활성화되어 있어 Public Policy 자체를 저장할 수 없습니다.
해결 방법
정적 웹사이트 호스팅이 목적이라면: 1. CloudFront + OAC(Origin Access Control)를 사용하여 BPA를 유지하면서 배포하는 방식을 권장합니다. 2. BPA를 비활성화하는 것은 보안 위험이 있으므로, 반드시 필요한 경우에만 개별 설정을 해제합니다.
진단 명령어
# 버킷 레벨 BPA 확인
aws s3api get-public-access-block --bucket my-bucket
# 계정 레벨 BPA 확인 (계정 전체에 적용)
aws s3control get-public-access-block --account-id 123456789012
주의: 계정 레벨 BPA가 활성화되어 있으면 버킷 레벨에서 해제해도 여전히 차단됩니다. 계정 레벨이 상위 우선순위입니다.
6. 원인 4: Cross-Account 접근 시 양쪽 허용 누락
다른 AWS 계정의 S3 버킷에 접근할 때는 요청자 측 IAM Policy와 버킷 소유자 측 Bucket Policy 양쪽 모두에서 허용해야 합니다.
실무 시나리오
Account A(111111111111)의 Lambda가 Account B(222222222222)의 S3 버킷에서 데이터를 읽어야 합니다.
Account A (요청자) — IAM Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::account-b-bucket/*"
}
]
}
Account B (버킷 소유자) — Bucket Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCrossAccountRead",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/lambda-role"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::account-b-bucket/*"
}
]
}
둘 중 하나라도 빠지면 AccessDenied가 발생합니다.
Cross-Account 접근의 대안: S3 Access Point
버킷에 접근하는 계정이 여러 개라면, 계정마다 Bucket Policy에 Principal을 추가하는 대신 S3 Access Point를 사용하면 정책 관리가 단순해집니다.
객체 소유권 주의사항
Cross-Account로 객체를 업로드할 때, 기본적으로 업로더 계정이 객체 소유자가 됩니다. 버킷 소유자가 해당 객체를 읽지 못하는 상황이 발생할 수 있습니다.
해결: 버킷의 Object Ownership 설정을 BucketOwnerEnforced로 변경하면 버킷 소유자가 모든 객체의 소유권을 갖습니다.
aws s3api put-bucket-ownership-controls --bucket my-bucket \
--ownership-controls Rules=[{ObjectOwnership=BucketOwnerEnforced}]
7. 원인 5: VPC Endpoint Policy 제한
Private Subnet의 리소스가 VPC Endpoint(Gateway 타입)를 통해 S3에 접근할 때, VPC Endpoint Policy가 추가 필터로 작동합니다.
실무 시나리오
Private Subnet의 EC2에서 S3에 접근합니다. IAM Role과 Bucket Policy는 정상인데 AccessDenied가 발생합니다. 같은 IAM Role로 Public Subnet(NAT Gateway 경유)에서는 정상 동작합니다.
이 경우 VPC Endpoint Policy가 특정 버킷만 허용하도록 제한되어 있을 수 있습니다.
VPC Endpoint Policy 예시 (제한적)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificBucket",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::allowed-bucket",
"arn:aws:s3:::allowed-bucket/*"
]
}
]
}
이 정책이 적용된 VPC Endpoint를 경유하면, allowed-bucket 외의 버킷에는 접근할 수 없습니다.
진단 명령어
# VPC Endpoint 목록 확인
aws ec2 describe-vpc-endpoints --filters Name=service-name,Values=com.amazonaws.ap-northeast-2.s3
# VPC Endpoint Policy 확인
aws ec2 describe-vpc-endpoints --vpc-endpoint-ids vpce-0123456789abcdef0 \
--query 'VpcEndpoints[].PolicyDocument'
# Route Table에서 VPC Endpoint 경유 여부 확인
aws ec2 describe-route-tables --route-table-ids rtb-0123456789abcdef0
해결 방법
VPC Endpoint Policy를 Full Access로 변경하거나, 필요한 버킷을 Resource에 추가합니다:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": "*"
}
]
}
보안 관점에서는 Full Access보다 필요한 버킷만 허용하는 것이 권장됩니다. 다만 새 버킷을 추가할 때마다 Endpoint Policy도 업데이트해야 하는 운영 부담이 있습니다.
8. 원인 6: KMS 암호화 객체 접근
SSE-KMS로 암호화된 객체를 읽으려면 S3 권한 외에 KMS Key에 대한 kms:Decrypt 권한이 필요합니다.
실무 시나리오
같은 버킷 내에서 일부 객체만 AccessDenied가 발생합니다. 확인해보니 해당 객체들은 Customer Managed Key(CMK)로 암호화되어 있습니다.
필요한 권한
S3 객체를 읽으려면: - s3:GetObject (IAM Policy 또는 Bucket Policy) - kms:Decrypt (IAM Policy) - KMS Key Policy에서 해당 Principal의 사용 허용
IAM Policy 예시
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-bucket/*"
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/key-id"
}
]
}
KMS Key Policy 확인
# 객체의 암호화 방식 확인
aws s3api head-object --bucket my-bucket --key test.txt \
--query '{Encryption: ServerSideEncryption, KMSKeyId: SSEKMSKeyId}'
# KMS Key Policy 확인
aws kms get-key-policy --key-id arn:aws:kms:ap-northeast-2:123456789012:key/key-id \
--policy-name default --output text
Cross-Account + KMS 조합
Cross-Account 접근에서 KMS 암호화 객체를 읽으려면 조건이 더 복잡해집니다:
- 요청자 IAM Policy:
s3:GetObject+kms:Decrypt - 버킷 소유자 Bucket Policy: 요청자 Principal Allow
- KMS Key Policy: 요청자 계정/Principal에
kms:Decrypt허용
세 가지 중 하나라도 빠지면 AccessDenied가 발생합니다.
9. 원인 7: SCP(Service Control Policy) 제한
AWS Organizations를 사용하는 환경에서는 SCP가 계정 전체의 권한 상한선을 설정합니다. SCP에서 S3 접근을 제한하면 해당 계정의 모든 IAM 엔티티가 영향을 받습니다.
실무 시나리오
새로 생성한 AWS 계정에서 S3에 접근할 수 없습니다. IAM Policy에 AdministratorAccess를 부여했는데도 AccessDenied가 발생합니다.
이 경우 Organizations의 OU(Organizational Unit)에 적용된 SCP가 S3 접근을 제한하고 있을 수 있습니다.
진단 방법
# 계정에 적용된 SCP 목록 확인
aws organizations list-policies-for-target \
--target-id 123456789012 \
--filter SERVICE_CONTROL_POLICY
# SCP 내용 확인
aws organizations describe-policy --policy-id p-1234567890
SCP는 Organizations 관리 계정에서만 확인/수정할 수 있습니다. 일반 멤버 계정에서는 자신에게 적용된 SCP를 직접 확인할 수 없으므로, 관리 계정 담당자에게 확인을 요청해야 합니다.
10. 빠른 진단 체크리스트
AccessDenied가 발생했을 때 아래 순서로 확인하면 대부분의 원인을 빠르게 좁힐 수 있습니다.
Step 1: 호출자 확인
aws sts get-caller-identity
예상한 IAM User/Role이 맞는지 확인합니다. AssumeRole 체인에서 잘못된 Role을 사용하고 있는 경우가 있습니다.
Step 2: IAM Policy 확인
# 연결된 정책 목록
aws iam list-attached-role-policies --role-name my-role
# 인라인 정책 확인
aws iam list-role-policies --role-name my-role
aws iam get-role-policy --role-name my-role --policy-name my-inline-policy
Step 3: Bucket Policy 확인
aws s3api get-bucket-policy --bucket my-bucket --output text | python -m json.tool
Step 4: Block Public Access 확인
# 버킷 레벨
aws s3api get-public-access-block --bucket my-bucket
# 계정 레벨
aws s3control get-public-access-block --account-id $(aws sts get-caller-identity --query Account --output text)
Step 5: CloudTrail 이벤트 확인
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=GetObject \
--start-time "2026-06-01T00:00:00Z" \
--max-results 10
CloudTrail 이벤트의 errorCode와 errorMessage 필드에서 구체적인 거부 이유를 확인할 수 있습니다.
Step 6: IAM Access Analyzer 활용
IAM Access Analyzer의 Policy Validation 기능을 사용하면 정책의 문법 오류나 보안 경고를 사전에 확인할 수 있습니다.
aws accessanalyzer validate-policy \
--policy-document file://policy.json \
--policy-type IDENTITY_POLICY
11. 실무 운영 팁
최소 권한 원칙과 디버깅의 균형
운영 환경에서는 최소 권한 원칙을 따르되, 디버깅이 어려워지는 문제가 있습니다. 아래 방법으로 균형을 맞출 수 있습니다:
- 개발 환경: 넓은 권한으로 시작하고, CloudTrail에서 실제 사용된 Action만 추출하여 운영 환경 정책을 만듭니다.
- IAM Access Analyzer: 실제 사용 패턴을 기반으로 최소 권한 정책을 자동 생성합니다.
- Permission Boundary: 개발자에게 넓은 권한을 주되, 상한선을 설정합니다.
자주 빠뜨리는 Action
| 작업 | 필요한 Action (자주 누락) |
|---|---|
aws s3 ls |
s3:ListBucket (버킷 수준 Resource) |
aws s3 cp (다운로드) |
s3:GetObject |
aws s3 cp (업로드) |
s3:PutObject |
aws s3 sync |
s3:ListBucket + s3:GetObject + s3:PutObject |
aws s3 rm |
s3:DeleteObject |
| Multipart Upload | s3:PutObject + s3:AbortMultipartUpload + s3:ListMultipartUploadParts |
에러 메시지 해석
S3 AccessDenied 에러 메시지는 보안상 의도적으로 모호합니다. 다만 몇 가지 패턴으로 원인을 추정할 수 있습니다:
An error occurred (AccessDenied) when calling the GetObject operation: Access Denied
- 이 메시지만으로는 IAM/Bucket Policy/KMS 중 어디서 거부되었는지 알 수 없습니다.
- CloudTrail 이벤트를 확인하면
errorMessage에 더 구체적인 정보가 포함될 수 있습니다.
An error occurred (AccessDenied) when calling the PutBucketPolicy operation: Access Denied
- Bucket Policy 자체를 저장할 수 없는 경우: Block Public Access가 원인일 가능성이 높습니다.
12. 정리
S3 AccessDenied는 단일 원인이 아니라 여러 권한 계층의 조합으로 발생합니다. 진단할 때는 아래 순서를 권장합니다:
aws sts get-caller-identity로 호출자 확인- IAM Policy에서 Action과 Resource(버킷 수준 vs 객체 수준) 확인
- Bucket Policy에서 명시적 Deny 조건 확인
- Cross-Account라면 양쪽 정책 모두 확인
- VPC Endpoint 경유 여부와 Endpoint Policy 확인
- KMS 암호화 객체라면 Key Policy와
kms:Decrypt확인 - 위 모두 정상이면 SCP 확인 (Organizations 관리 계정에 문의)
운영 환경에서는 CloudTrail 로그를 활성화하고, IAM Access Analyzer를 활용하면 권한 문제를 사전에 예방할 수 있습니다.
관련 글: - AWS VPC란 무엇인가: 구조, 서브넷, 라우팅 기본 개념 - NAT Gateway vs VPC Endpoint 차이: 비용과 보안 관점에서 선택하기
'Troubleshooting' 카테고리의 다른 글
| EKS Pod가 외부 인터넷에 접근하지 못하는 경우 (0) | 2026.06.06 |
|---|---|
| Terraform Error acquiring the state lock 해결 방법 (0) | 2026.06.05 |
| Kubernetes CrashLoopBackOff 원인과 해결 방법 (0) | 2026.06.05 |
| Kubernetes ImagePullBackOff 원인과 해결 방법 (0) | 2026.06.01 |
| ALB 502 Bad Gateway 원인 분석: Target Group, Health Check, 타임아웃까지 (0) | 2026.05.31 |