Published on

EKS Pod에서 IRSA는 되는데 DynamoDB 403 해결

Authors

서론

EKS에서 IRSA(IAM Roles for Service Accounts)를 붙였고 Pod 내부에서 aws sts get-caller-identity도 정상인데, 정작 DynamoDB만 403 AccessDeniedException 또는 User is not authorized to perform: dynamodb:... 형태로 터지는 경우가 꽤 흔합니다. 이 상황이 헷갈리는 이유는 “IRSA 자체는 동작한다(=웹 아이덴티티로 롤 AssumeRole 성공)”는 신호가 이미 확인되었기 때문입니다. 하지만 DynamoDB 403은 대부분 정책(Policy)·리소스 ARN·조건(Condition)·KMS/엔드포인트 정책 같은 ‘권한의 마지막 1mm’에서 발생합니다.

이 글은 다음 순서로 문제를 좁혀 나갑니다.

  1. IRSA가 정말로 해당 Pod에서 기대한 Role로 동작하는지 확정
  2. DynamoDB 403에서 가장 빈번한 원인(리소스 ARN/리전/계정/테이블명, Index ARN, PartiQL 등) 체크
  3. IAM Policy/Permission boundary/SCP/세션 정책/태그 조건 점검
  4. VPC Endpoint(게이트웨이) 정책 및 KMS(암호화) 권한 확인
  5. CloudTrail로 “누가/어떤 조건에서” 거부됐는지 역추적

중간에 네트워크/STS 관련 이슈가 섞여 있다면, STS 호출 자체가 지연/타임아웃 되는 케이스도 있으니 별도로 아래 글도 참고하면 좋습니다.


1. 증상 정리: “IRSA는 되는데 DynamoDB만 403”의 의미

  • aws sts get-caller-identity 성공
    • 즉, Pod는 AWS_WEB_IDENTITY_TOKEN_FILE + AWS_ROLE_ARN로 STS AssumeRoleWithWebIdentity를 성공했고, 임시 자격증명을 발급받고 있음
  • DynamoDB API 호출에서만 403/AccessDeniedException
    • 즉, 자격증명은 유효하지만 해당 API/리소스에 대한 권한이 없거나, 다른 정책 계층에서 명시적으로 거부되고 있음

여기서 핵심은 “IRSA 문제”가 아니라 “DynamoDB 권한 해석 문제”로 접근해야 한다는 점입니다.


2. 가장 먼저 할 일: Pod가 어떤 Role로 동작하는지 확정

IRSA가 붙었다고 생각했는데, 실제로는 다른 Role(예: 노드 인스턴스 프로파일)로 호출되는 경우도 있습니다. 아래를 Pod 안에서 확인하세요.

2.1 환경 변수 확인

kubectl exec -it deploy/myapp -- sh -c 'env | egrep "AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION|AWS_DEFAULT_REGION"'
  • AWS_ROLE_ARN이 기대한 IAM Role ARN인지
  • AWS_WEB_IDENTITY_TOKEN_FILE이 존재하는지
  • Region이 의도한 값인지(특히 AWS_REGION/AWS_DEFAULT_REGION 누락 시 SDK가 다른 기본값을 잡는 경우)

2.2 실제 호출 주체 확인(STS)

kubectl exec -it deploy/myapp -- sh -c 'aws sts get-caller-identity'

출력의 Arn이 보통 아래처럼 나옵니다.

  • arn:aws:sts::<account-id>:assumed-role/<role-name>/<session-name>

여기서 <role-name>이 IRSA Role인지 확인합니다.


3. DynamoDB 403의 1순위 원인: 리소스 ARN 불일치

정책은 붙어 있는데도 403이 나는 가장 흔한 이유는 정책의 Resource ARN이 실제 호출 리소스와 다르기 때문입니다.

3.1 테이블 ARN/리전/계정/이름 오타

DynamoDB 테이블 ARN 형식:

  • arn:aws:dynamodb:<region>:<account-id>:table/<table-name>

예를 들어, ap-northeast-2에서 my-table이면:

  • arn:aws:dynamodb:ap-northeast-2:123456789012:table/my-table

정책이 us-east-1로 되어 있거나, 계정이 다르거나, 테이블명이 환경별로 다른데 하드코딩되어 있으면 403이 납니다.

3.2 GSI/LSI(Index) 접근 권한 누락

Query/Scan을 인덱스 대상으로 수행할 때는 Resource가 테이블뿐 아니라 index ARN으로 평가될 수 있습니다.

  • arn:aws:dynamodb:<region>:<account-id>:table/<table-name>/index/<index-name>

정책에 테이블만 넣고 인덱스를 빼면 dynamodb:Query가 인덱스 리소스에서 거부될 수 있습니다.

권장 패턴은 아래처럼 테이블과 인덱스를 함께 허용하는 것입니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Query",
        "dynamodb:Scan",
        "dynamodb:DescribeTable"
      ],
      "Resource": [
        "arn:aws:dynamodb:ap-northeast-2:123456789012:table/my-table",
        "arn:aws:dynamodb:ap-northeast-2:123456789012:table/my-table/index/*"
      ]
    }
  ]
}

3.3 PartiQL(ExecuteStatement/ExecuteTransaction) 사용 시 액션 누락

애플리케이션이 DynamoDB를 “SQL 비슷하게” 쓰는 경우(PartiQL)에는 ExecuteStatement 같은 액션이 필요합니다.

  • dynamodb:ExecuteStatement
  • dynamodb:ExecuteTransaction

일반 CRUD만 허용해두고 PartiQL을 호출하면 403이 납니다.


4. “정책은 맞는데도” 403: 조건(Condition)과 상위 거부를 의심

IRSA Role에 정책을 붙였는데도 계속 403이면, 아래 계층에서 거부되고 있을 확률이 큽니다.

  • Permission Boundary
  • Organizations SCP(Service Control Policy)
  • Session policy(AssumeRole 시 세션 정책)
  • IAM Policy의 Condition(특히 태그 기반, VPC 엔드포인트 기반)

4.1 Permission Boundary / SCP 확인 포인트

  • Role에 Permission boundary가 걸려 있으면, 붙여둔 Allow 정책이 있어도 boundary 밖 액션은 거부됩니다.
  • 조직 계정이라면 SCP가 dynamodb:*를 제한할 수 있습니다.

이건 콘솔에서 Role 상세를 확인하거나, 계정 관리 정책을 확인해야 합니다.

4.2 자주 터지는 Condition 예시: aws:PrincipalTag, aws:RequestTag

예를 들어 “PrincipalTag가 있어야 DynamoDB 접근 허용” 같은 정책이 있는데 IRSA Role 세션에 태그가 안 붙으면 403입니다.

또는 dynamodb:LeadingKeys 조건으로 특정 파티션 키만 허용하는 정책이 있어도 예상치 못한 키에서 거부됩니다.


5. VPC Endpoint(게이트웨이) 정책 때문에 403이 나는 케이스

프라이빗 서브넷에서 DynamoDB에 붙기 위해 Gateway VPC Endpoint를 쓰는 경우가 많습니다. 이때 DynamoDB는 네트워크는 연결되지만, 엔드포인트 정책이 리소스/액션을 제한하면 403이 발생합니다.

특징:

  • STS는 NAT로 나가거나(혹은 STS 엔드포인트) 별도로 동작해서 “IRSA는 됨”
  • DynamoDB 트래픽은 Gateway Endpoint로 빠지면서 “Endpoint policy에 의해 거부”

5.1 엔드포인트 정책 점검

VPC 콘솔 → Endpoints → com.amazonaws.<region>.dynamodb → Policy 확인

테스트로는 일시적으로(보안 검토 후) 아래처럼 넓게 열어 원인을 분리할 수 있습니다.

{
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "dynamodb:*",
      "Resource": "*"
    }
  ]
}

이 상태에서 403이 사라지면, 원인은 IAM이 아니라 엔드포인트 정책입니다. 이후 최소 권한으로 다시 줄이세요.


6. 테이블이 KMS 암호화라면: kms:Decrypt 권한 누락

DynamoDB 테이블이 AWS managed key가 아니라 Customer managed KMS key(CMK) 로 암호화되어 있으면, DynamoDB 액션이 허용되어도 KMS에서 막혀 403/AccessDenied가 날 수 있습니다(에러 메시지에 KMS가 드러나기도 하지만, 앱에서는 DynamoDB 403처럼만 보일 때도 있습니다).

필요한 권한(일반적으로):

  • kms:Decrypt
  • kms:Encrypt
  • kms:GenerateDataKey

그리고 KMS Key policy에서도 해당 Role을 허용해야 합니다(IAM 정책만으로는 부족한 경우가 많음).

예시(IAM 정책 측):

{
  "Effect": "Allow",
  "Action": [
    "kms:Decrypt",
    "kms:Encrypt",
    "kms:GenerateDataKey"
  ],
  "Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/<key-id>"
}

7. CloudTrail로 ‘정확히 무엇이 거부됐는지’ 확인하기

DynamoDB 403을 가장 빠르게 끝내는 방법은 CloudTrail 이벤트에서 errorCodeerrorMessage, 그리고 userIdentity.arn을 보는 것입니다.

7.1 CloudTrail Event lookup

CloudTrail 콘솔에서 Event history → Event source를 dynamodb.amazonaws.com로 필터링하고, 실패 이벤트를 확인하세요.

확인 포인트:

  • userIdentity.arn: 기대한 IRSA Role인지
  • eventName: GetItem, Query, ExecuteStatement 등 실제 호출 API
  • resources: 어떤 ARN으로 평가되었는지(테이블/인덱스)
  • errorMessage: “explicit deny”, “not authorized”, “not allowed by endpoint policy” 힌트가 포함되는 경우가 많음

8. 재현 가능한 점검 스크립트: Pod 내부에서 DynamoDB 호출 확인

애플리케이션 코드가 복잡하면, Pod 내부에서 AWS CLI로 최소 호출을 재현해 원인을 분리하는 게 좋습니다.

8.1 현재 자격증명으로 테이블 Describe

kubectl exec -it deploy/myapp -- sh -c '
aws dynamodb describe-table \
  --region ap-northeast-2 \
  --table-name my-table
'
  • 여기서 403이면: 리소스 ARN/엔드포인트 정책/상위 거부/SCP/Boundary/KMS 가능성
  • 여기서 성공인데 앱만 실패면: 앱이 다른 리전/다른 테이블/인덱스/PartiQL/추가 액션을 호출 중

8.2 인덱스 Query 재현

kubectl exec -it deploy/myapp -- sh -c '
aws dynamodb query \
  --region ap-northeast-2 \
  --table-name my-table \
  --index-name my-gsi \
  --key-condition-expression "pk = :v" \
  --expression-attribute-values "{\":v\": {\"S\": \"test\"}}"
'

이 호출이 403이면 정책에 .../index/*가 빠졌을 확률이 큽니다.


9. (예시) Python boto3에서 IRSA + DynamoDB 호출 코드

애플리케이션 레벨에서 리전/엔드포인트를 명시해 “다른 리전으로 나가서 다른 테이블 ARN과 불일치” 같은 실수를 줄일 수 있습니다.

import os
import boto3
from botocore.exceptions import ClientError

REGION = os.getenv("AWS_REGION", "ap-northeast-2")
TABLE = os.getenv("DDB_TABLE", "my-table")

ddb = boto3.resource("dynamodb", region_name=REGION)

table = ddb.Table(TABLE)

try:
    resp = table.get_item(Key={"pk": "user#1", "sk": "profile"})
    print(resp.get("Item"))
except ClientError as e:
    # 여기서 e.response['Error']['Code'] / Message를 로깅해두면 CloudTrail과 대조가 쉬움
    print("DynamoDB error:", e.response["Error"]["Code"], e.response["Error"]["Message"])
    raise

운영에서는 Error.Message를 그대로 노출하면 보안상 민감할 수 있으니, 내부 로그로만 남기고 CloudTrail과 함께 보세요.


10. 체크리스트: 403을 빠르게 끝내는 10분 진단 순서

  1. Pod에서 aws sts get-caller-identity로 Role 확정
  2. Pod에서 aws dynamodb describe-table로 CLI 재현
  3. 정책 Resource ARN이 정확한지(리전/계정/테이블명)
  4. .../index/* 포함 여부(인덱스 Query/Scan)
  5. PartiQL 사용 시 ExecuteStatement 등 액션 포함 여부
  6. Permission boundary / SCP / explicit deny 존재 여부
  7. DynamoDB Gateway Endpoint 정책이 제한하고 있지 않은지
  8. 테이블이 CMK면 KMS 권한 + Key policy 허용 여부
  9. CloudTrail에서 실패 이벤트의 eventName, resources, errorMessage 확인
  10. 앱의 Region/테이블명 환경변수(혹은 SDK 기본값) 재확인

EKS에서 “Pod는 정상, IRSA도 정상”인데 특정 AWS 서비스만 403이 나는 문제는, 대부분 위 10개 중 2~3개에서 결론이 납니다. 특히 인덱스 ARN 누락, Gateway Endpoint 정책, KMS 권한이 상위권입니다.

EKS에서 다른 형태의 ‘정상처럼 보이지만 실제로는 특정 단계에서 막히는’ 장애 패턴을 더 보고 싶다면, 아래 글도 함께 참고해보세요.