Published on

S3 AccessDenied 403 - 버킷 정책과 SSE-KMS 권한

Authors

서버에서 S3 객체를 읽거나 쓰는 코드가 갑자기 AccessDenied 403을 뱉을 때, 많은 팀이 IAM 권한만 먼저 의심합니다. 하지만 실제로는 S3 버킷 정책SSE-KMS(KMS로 암호화된 S3 객체) 권한이 동시에 관여하면서 “겉으로는 S3 403인데, 속 원인은 KMS 거절”인 케이스가 매우 흔합니다.

이 글은 다음을 목표로 합니다.

  • 403을 정확히 어떤 계층에서 거절했는지(IAM, Bucket Policy, KMS Key Policy, VPC Endpoint Policy 등) 분리
  • SSE-KMS에서 특히 자주 나오는 함정(키 정책, kms:ViaService, kms:EncryptionContext, 멀티리전/크로스계정)을 체크리스트로 정리
  • 최소 재현 커맨드와 정책 예시 제공

운영 환경이 EKS라면, 네트워크/엔드포인트까지 얽혀 “DNS는 되는데 HTTPS만 실패” 같은 증상이 같이 보일 수 있습니다. 이 경우는 별도 네트워크 점검도 필요하니 EKS Pod DNS는 되는데 HTTPS만 실패할 때 점검도 함께 참고하세요.

403 AccessDenied를 보는 올바른 순서

S3 요청이 거절되는 경로는 대략 아래 순서로 생각하면 빠릅니다.

  1. 요청 주체(Principal): 실제로 어떤 IAM Role/User/AssumedRole로 호출했는가
  2. IAM 정책: 해당 Principal의 Allow가 있는가, Explicit Deny가 있는가
  3. S3 버킷 정책: 버킷 정책에서 Explicit Deny가 있는가, 조건이 맞지 않아 Allow가 안 되는가
  4. S3 Block Public Access / Object Ownership: 퍼블릭 차단/ACL 비활성 등으로 차단되는가
  5. SSE-KMS라면 KMS 권한: kms:Decrypt, kms:Encrypt, kms:GenerateDataKey 등 + 키 정책
  6. (옵션) VPC Endpoint Policy / SCP / Permission Boundary: 조직 정책이나 엔드포인트 정책이 막는가

핵심은 S3 Put/Get 자체 권한이 있어도, 객체가 SSE-KMS로 암호화되어 있으면 KMS에서 한 번 더 승인이 필요하다는 점입니다.

가장 흔한 패턴 4가지

1) s3:GetObject는 있는데 kms:Decrypt가 없어 읽기 실패

증상

  • aws s3 cp s3://...가 403
  • CloudTrail을 보면 S3 이벤트는 거절만 보이거나, KMS DecryptAccessDenied로 찍힘

원인

  • 객체가 SSE-KMS로 암호화되어 있는데 호출 주체에 kms:Decrypt 권한이 없음
  • 또는 KMS 키 정책에서 해당 Principal을 신뢰하지 않음

해결

  • IAM에 kms:Decrypt를 추가하는 것만으로는 부족할 수 있고, KMS Key Policy에도 허용이 필요합니다.

2) 업로드는 되는데 다운로드만 403 (혹은 그 반대)

증상

  • Put은 성공, Get은 403
  • 또는 Get은 되는데 Put에서 403

원인

  • Put에는 kms:Encrypt/kms:GenerateDataKey가 필요
  • Get에는 kms:Decrypt가 필요
  • 버킷 정책에서 s3:PutObject만 허용하고 s3:GetObject는 조건 누락

해결

  • Put/Get 각각에 필요한 KMS 액션이 다름을 분리해서 권한을 구성

3) 버킷 정책의 조건이 너무 빡세서 특정 경로/엔드포인트만 실패

증상

  • 사내 네트워크나 특정 VPC에서만 성공
  • CI/CD, 로컬, 다른 VPC에서만 403

원인

  • 버킷 정책에 aws:SourceVpce, aws:SourceIp, aws:PrincipalArn 등의 조건이 걸려 있음
  • KMS 키 정책에도 kms:ViaService 조건이 걸려 있는데 리전/서비스명이 불일치

해결

  • “누가/어디서/어떤 경로로” 접근하는지 조건을 정확히 맞추거나 예외를 설계

4) 크로스계정에서 SSE-KMS 객체 접근 시 키 정책 미스매치

증상

  • 버킷은 계정 A, 접근 주체는 계정 B
  • S3 권한은 다 열어줬는데도 403

원인

  • KMS 키가 계정 A에 있고, 키 정책이 계정 B Principal을 허용하지 않음
  • 또는 kms:EncryptionContext:aws:s3:arn 조건이 버킷 ARN과 다르게 설정됨

해결

  • KMS 키 정책이 최종 관문이라는 점을 기억하고, 크로스계정 Principal을 키 정책에 명시

빠른 진단: 어떤 계층이 거절했는지 확인하는 커맨드

1) 현재 호출 주체 확인

aws sts get-caller-identity

여기서 나온 Arn이 실제로 버킷 정책/키 정책에서 허용한 Principal과 일치하는지부터 확인합니다.

2) S3 API를 낮은 수준으로 호출해 에러 메시지 분리

aws s3api head-object --bucket my-bucket --key path/to/object
aws s3api get-object --bucket my-bucket --key path/to/object /tmp/out
  • head-object는 객체 메타데이터 접근이므로 정책 차이를 드러내는 데 도움이 됩니다.
  • SSE-KMS 객체는 Get 시 KMS 권한이 필요하므로, 여기서 403이 나면 KMS를 의심할 근거가 됩니다.

3) IAM 시뮬레이터로 S3 권한 확인(단, KMS 키 정책까지 완벽히 대체하진 못함)

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/MyRole \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::my-bucket/path/to/object

시뮬레이션이 allowed인데도 실제 호출이 403이면, 버킷 정책의 조건/명시적 거부, KMS 키 정책, SCP, VPC Endpoint Policy 같은 “외부 요인”을 의심해야 합니다.

4) CloudTrail에서 KMS Decrypt 거절 여부 확인

CloudTrail EventName이 Decrypt이고 ErrorCode가 AccessDenied면 거의 확정적으로 KMS 레이어 문제입니다.

SSE-KMS에서 반드시 알아야 하는 권한 구성

SSE-KMS는 S3가 내부적으로 KMS를 호출해 데이터 키를 만들거나(업로드), 데이터 키를 복호화(다운로드)합니다. 따라서 S3 권한과 별개로 KMS 권한이 필요합니다.

필요한 KMS 액션 요약

  • 업로드(암호화): kms:Encrypt, kms:GenerateDataKey (상황에 따라 kms:GenerateDataKeyWithoutPlaintext)
  • 다운로드(복호화): kms:Decrypt
  • (선택) 키 조회/검증: kms:DescribeKey

예시: IAM Role에 S3 + KMS 최소 권한 부여

아래는 특정 버킷 prefix에만 접근하고, 특정 KMS 키만 사용하도록 제한한 예시입니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowListBucketPrefix",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::my-bucket",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["app/*"]
        }
      }
    },
    {
      "Sid": "AllowGetPutOnObjects",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::my-bucket/app/*"
    },
    {
      "Sid": "AllowUseKmsKeyForS3",
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt",
        "kms:Encrypt",
        "kms:GenerateDataKey",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/11111111-2222-3333-4444-555555555555"
    }
  ]
}

여기까지 했는데도 403이 계속 나면, 다음 단계는 KMS 키 정책입니다.

예시: KMS Key Policy에서 Role 허용(가장 자주 놓침)

KMS는 “IAM 정책에서 허용”만으로 충분하지 않고, 키 정책이 해당 Principal을 신뢰해야 합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnableRootPermissions",
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::123456789012:root"},
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "AllowRoleUseOfKey",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/MyRole"
      },
      "Action": [
        "kms:Decrypt",
        "kms:Encrypt",
        "kms:GenerateDataKey",
        "kms:DescribeKey"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "s3.ap-northeast-2.amazonaws.com"
        }
      }
    }
  ]
}

kms:ViaService 조건은 “이 키를 S3를 통해서만 쓰게” 제한하는 데 유용하지만, 리전이 다르면 바로 403이 됩니다. 예를 들어 S3 요청이 ap-northeast-1로 나가거나, 멀티리전 액세스 포인트/복제 구성에서 리전이 바뀌면 조건 불일치가 발생할 수 있습니다.

S3 버킷 정책에서 SSE-KMS 강제할 때의 흔한 실수

보안 강화를 위해 “반드시 SSE-KMS로 업로드하라”를 버킷 정책으로 강제하는 경우가 많습니다. 이때 조건을 잘못 걸면 모든 Put이 403이 됩니다.

예시: SSE-KMS 미사용 업로드 거부

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUnEncryptedObjectUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-bucket/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    }
  ]
}

이 정책이 있으면, SDK/CLI가 Put 요청에 x-amz-server-side-encryption 헤더를 넣지 않는 순간 403입니다.

특정 KMS 키만 허용까지 강제

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyWrongKmsKey",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-bucket/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption-aws-kms-key-id": "arn:aws:kms:ap-northeast-2:123456789012:key/11111111-2222-3333-4444-555555555555"
        }
      }
    }
  ]
}

여기서도 자주 생기는 문제는 “애플리케이션은 별도의 키를 쓰도록 설정돼 있었음”, “리전이 달라 ARN이 달라짐”, “alias를 넣었는데 정책은 key ARN만 허용” 같은 불일치입니다.

운영에서 자주 겪는 함정 체크리스트

1) AssumedRole ARN 매칭 실수

버킷 정책에서 aws:PrincipalArn을 조건으로 쓰는 경우, 실제 호출 ARN은 assumed-role 형태입니다. 예를 들어 arn:aws:sts::...:assumed-role/RoleName/SessionName 입니다. 정책에서 iam::...:role/RoleName만 비교하면 조건이 안 맞을 수 있습니다.

대응

  • StringLikeassumed-role/RoleName/*까지 포함하거나, 아예 Principal을 Role로 두고 조건을 최소화합니다.

2) VPC Endpoint를 쓰는데 버킷 정책이 aws:SourceVpce로 잠겨 있음

엔드포인트 ID가 바뀌거나(재생성), 다른 계정/다른 VPC에서 접근하면 즉시 403입니다.

대응

  • 엔드포인트 변경 가능성을 고려해 IaC로 고정 관리
  • 멀티 VPC/멀티 계정이면 허용 목록을 명시적으로 관리

3) KMS 키가 “다른 리전”에 있음

S3 SSE-KMS는 같은 리전의 KMS 키를 쓰는 것이 기본입니다. 구성 자체가 꼬이면 kms:ViaService 조건 불일치나 키 접근 실패가 나기 쉽습니다.

4) 조직의 SCP 또는 Permission Boundary

IAM 정책을 아무리 고쳐도 SCP에서 kms:Decrypt를 막으면 끝입니다. “특정 계정에서만 계속 실패”한다면 조직 정책을 확인하세요.

애플리케이션 코드에서의 실전 설정 예시

AWS CLI로 SSE-KMS 업로드 재현

aws s3api put-object \
  --bucket my-bucket \
  --key app/test.txt \
  --body ./test.txt \
  --server-side-encryption aws:kms \
  --ssekms-key-id arn:aws:kms:ap-northeast-2:123456789012:key/11111111-2222-3333-4444-555555555555

Java SDK v2 예시(업로드 시 SSE-KMS 지정)

import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.core.sync.RequestBody;

var s3 = S3Client.create();

var req = PutObjectRequest.builder()
    .bucket("my-bucket")
    .key("app/test.txt")
    .serverSideEncryption("aws:kms")
    .ssekmsKeyId("arn:aws:kms:ap-northeast-2:123456789012:key/11111111-2222-3333-4444-555555555555")
    .build();

s3.putObject(req, RequestBody.fromString("hello"));

주의: 버킷 정책이 SSE-KMS를 강제하는데 코드에서 옵션을 빼먹으면, IAM이 아무리 열려도 403입니다.

결론: 403을 “S3 문제”로만 보지 말 것

S3 AccessDenied 403은 표면적으로 단순해 보이지만, 실제 원인은 다음 조합으로 결정됩니다.

  • S3 IAM 권한(Principal 정책)
  • 버킷 정책의 조건과 명시적 거부
  • SSE-KMS 사용 여부와 KMS 키 정책/권한
  • (환경에 따라) VPC Endpoint Policy, SCP, Permission Boundary

실전에서는 CloudTrail에서 KMS Decrypt/GenerateDataKey 거절이 있는지를 먼저 확인하고, 그 다음에 버킷 정책 조건(SourceVpce, PrincipalArn, SSE 강제 조건)을 점검하는 순서가 가장 빠릅니다.

EKS 환경에서 서비스 계정 기반 IAM(Role for Service Account)을 쓰는 경우, “내가 생각한 Role로 호출되는지”가 특히 중요합니다. IMDS 차단/자격 증명 경로가 꼬이면 전혀 다른 자격 증명으로 S3를 호출할 수도 있으니, 필요하다면 EKS에서 AWS SDK의 IMDS 접근을 확실히 차단하는 법도 함께 점검해 보세요.