Published on

AWS S3 AccessDenied? 버킷정책·KMS 키 7단계

Authors

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:ListBucketarn:aws:s3:::my-bucket/*를 넣음
  • s3:GetObjectarn:aws:s3:::my-bucket만 넣음
  • prefix 제한을 Condition으로 걸었는데 실제 키가 해당 prefix가 아님

3단계: 버킷 정책(Bucket Policy)의 Deny가 이기고 있지 않은지

S3는 명시적 Deny가 최우선입니다. IAM에서 Allow가 있어도 버킷 정책에 Deny가 있으면 무조건 막힙니다.

확인

  • 버킷 정책에서 EffectDeny인 Statement 존재 여부
  • Condition에 aws:PrincipalArn, aws:SourceVpce, aws:SecureTransport, s3:x-amz-server-side-encryption 등이 걸려 있는지

대표적인 Deny 패턴

  1. HTTPS 강제
{
  "Sid": "DenyInsecureTransport",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": [
    "arn:aws:s3:::my-bucket",
    "arn:aws:s3:::my-bucket/*"
  ],
  "Condition": {
    "Bool": { "aws:SecureTransport": "false" }
  }
}
  1. 특정 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-control ACL을 기대했는데 정책/설정이 달라졌는지
  • 가장 안전한 방식은 버킷 정책으로 교차 계정 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단계)

  1. sts get-caller-identity로 Principal 확정
  2. IAM 정책에서 ListBucketGetObject 리소스 ARN 정확성 확인
  3. 버킷 정책의 명시적 Deny/Condition 확인
  4. Public Access Block 및 ACL 전제 오해 제거
  5. SSE-KMS면 IAM에 kms:Decrypt/kms:GenerateDataKey 등 추가
  6. KMS 키 정책에서 Principal 및 kms:ViaService 조건 확인
  7. 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에서 데드라인·재시도 폭주 막는 법