Published on

EKS Pod에서 Kinesis 403 AccessDenied 10분 진단

Authors

서버리스·컨테이너 환경에서 Kinesis(Streams/Firehose) 권한 문제는 "IRSA는 붙어 있는데도 403", 혹은 "로컬에선 되는데 Pod에서만 AccessDenied" 형태로 자주 나타납니다. 특히 EKS에서는 자격증명 체인이 여러 갈래(노드 인스턴스 프로파일, IRSA, 환경변수, 웹아이덴티티)로 얽혀 있어, 진단 순서를 잘못 잡으면 1~2시간이 순식간에 증발합니다.

이 글은 EKS Pod → Kinesis 호출이 403 AccessDenied일 때, 10분 안에 원인을 좁히는 실전 플로우를 제공합니다. 핵심은 “지금 Pod가 실제로 어떤 IAM Principal로 호출하고 있는지”와 “그 Principal이 어떤 Action/Resource/Condition에서 막히는지”를 빠르게 확인하는 것입니다.

> IRSA 자체가 의심된다면 먼저 EKS IRSA 설정했는데 STS AccessDenied 뜰 때도 같이 보면 진단 속도가 더 빨라집니다.


0) 403 AccessDenied 유형부터 분류하기 (1분)

Kinesis 403은 크게 3가지로 나뉩니다.

  1. 정책(Action/Resource) 자체가 없음
    • 예: kinesis:PutRecord 권한 누락
  2. 리소스 ARN/리전/계정이 불일치
    • 예: ap-northeast-2 스트림인데 us-east-1로 호출
  3. 조건(Condition) 또는 추가 서비스(KMS/VPC Endpoint)에서 차단
    • 예: SSE-KMS 사용 중인데 kms:Decrypt/kms:GenerateDataKey 누락

에러 메시지에 User: arn:aws:sts::...:assumed-role/... is not authorized to perform: ... on resource: ...가 나오면, 이 한 줄이 진단의 80%입니다.


1) Pod가 어떤 AWS 자격증명으로 호출 중인지 확인 (2분)

EKS에서 가장 흔한 함정은 IRSA를 의도했는데 실제로는 노드 Role로 호출하거나, 반대로 IRSA가 깨져서 "다른 체인"으로 떨어지는 경우입니다.

1-1. Pod 환경변수로 IRSA(WebIdentity) 확인

kubectl -n <ns> exec -it <pod> -- sh -lc 'env | egrep "AWS_(ROLE_ARN|WEB_IDENTITY_TOKEN_FILE|REGION|DEFAULT_REGION)"'
  • AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILE 이 보이면 IRSA 경로가 열려 있을 가능성이 큽니다.
  • 아무것도 없다면 SDK가 노드 인스턴스 프로파일(EC2 metadata) 로 떨어질 수 있습니다.

1-2. STS로 “내가 누구인지” 즉시 확인

컨테이너에 aws cli가 없다면 디버그용 임시 Pod를 띄우는 방식이 가장 빠릅니다.

kubectl -n <ns> run awscli-debug --rm -it \
  --image=public.ecr.aws/aws-cli/aws-cli:2 \
  --overrides='{
    "spec": {
      "serviceAccountName": "<service-account>",
      "containers": [{
        "name": "awscli",
        "image": "public.ecr.aws/aws-cli/aws-cli:2",
        "command": ["sh", "-lc", "aws sts get-caller-identity && env | egrep \"AWS_(ROLE_ARN|WEB_IDENTITY_TOKEN_FILE|REGION|DEFAULT_REGION)\"" ]
      }]
    }
  }'

출력된 ARN이 다음 중 무엇인지 확인합니다.

  • 기대: arn:aws:sts::<acct>:assumed-role/<IRSA-Role>/<session>
  • 위험: arn:aws:sts::<acct>:assumed-role/<NodeInstanceRole>/<session>

> 자격증명 자체를 못 찾는 케이스(403이 아니라 credential error)는 EKS Pod에서 AWS SDK 자격증명 못찾음 해결 가이드 흐름대로 먼저 정리하는 게 좋습니다.


2) “어떤 Action”이 막혔는지 로그에서 고정 (1분)

애플리케이션 로그 또는 AWS SDK 예외에서 다음 3가지를 뽑아냅니다.

  • Principal(호출자): assumed-role/...
  • Action: kinesis:PutRecord, kinesis:PutRecords, kinesis:DescribeStream, firehose:PutRecordBatch
  • Resource: arn:aws:kinesis:<region>:<acct>:stream/<name> 혹은 Firehose delivery stream ARN

이 3가지가 확보되면 IAM 정책 문제를 거의 기계적으로 해결할 수 있습니다.


3) 리전/엔드포인트 불일치 체크 (1분)

의외로 흔합니다. Pod에서 AWS_REGION 또는 SDK 기본 리전이 잘못 잡히면, 존재하지 않는 리전의 리소스 ARN로 평가되어 AccessDenied처럼 보이는 상황이 생깁니다(특히 정책에 Resource를 특정 리전 ARN으로 고정했을 때).

kubectl -n <ns> exec -it <pod> -- sh -lc 'echo "AWS_REGION=$AWS_REGION"; echo "AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION"'

그리고 실제 스트림 리전을 명시해서 호출해봅니다.

aws kinesis describe-stream-summary \
  --stream-name <stream> \
  --region ap-northeast-2
  • 여기서 성공하면 앱/SDK 리전 설정 문제입니다.
  • 여기서도 AccessDenied면 IAM/리소스/조건 문제로 넘어갑니다.

4) IAM 정책에서 가장 많이 틀리는 5가지 (3분)

4-1. Resource ARN을 잘못 썼다 (stream vs *)

Kinesis Streams는 리소스 ARN이 다음 형태입니다.

  • arn:aws:kinesis:<region>:<acct>:stream/<stream-name>

정책에 stream/*가 아니라 deliveryStream/*(Firehose)로 써놓거나, 스트림 이름 오타가 있으면 바로 막힙니다.

최소 예시(Streams PutRecords)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "kinesis:PutRecord",
        "kinesis:PutRecords",
        "kinesis:DescribeStreamSummary",
        "kinesis:ListShards"
      ],
      "Resource": "arn:aws:kinesis:ap-northeast-2:123456789012:stream/my-stream"
    }
  ]
}
  • Producer라면 DescribeStreamSummary, ListShards가 SDK 내부에서 필요해지는 경우가 있습니다(라이브러리/언어별 차이).

4-2. Firehose와 Streams를 혼동했다

Firehose는 API와 ARN이 다릅니다.

  • Action: firehose:PutRecord, firehose:PutRecordBatch
  • Resource: arn:aws:firehose:<region>:<acct>:deliverystream/<name>

Streams에 쏘는데 Firehose 권한만 주거나(혹은 반대)면 403이 납니다.

4-3. 권한은 있는데 “Deny”가 이긴다 (SCP/Permission Boundary/세션 정책)

다음이 있으면 Allow가 있어도 최종적으로 Deny가 우선합니다.

  • AWS Organizations SCP
  • IAM Permission Boundary
  • Role session policy(AssumeRole 시 부여)

이 경우 CloudTrail/Policy Simulator로 봐야 빠릅니다(아래 6번 참고).

4-4. IRSA Role 신뢰정책(Trust Policy) 조건이 미스매치

IRSA Role의 trust policy에서 sub가 서비스어카운트와 정확히 일치해야 합니다.

예:

  • system:serviceaccount:<ns>:<sa-name>

오타/네임스페이스 변경/SA 교체 후 롤백 등으로 mismatch가 나면 STS AssumeRoleWithWebIdentity 단계부터 꼬이지만, 앱에 따라선 다른 자격증명 체인으로 떨어져 403만 보일 수도 있습니다.

4-5. Kinesis가 SSE-KMS를 쓰는데 KMS 권한이 없다

Kinesis Streams에서 SSE-KMS(고객 관리형 키)를 쓰면 호출 Role에 KMS 권한이 필요할 수 있습니다(특히 consumer/producer 패턴과 SDK 동작에 따라).

대표적으로 다음을 키 정책 또는 IAM 정책으로 허용해야 합니다.

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

또한 KMS Key Policy에서 해당 Role을 신뢰하지 않으면 IAM에 Allow가 있어도 실패합니다.


5) Kubernetes 쪽에서 IRSA 연결고리 3종 세트 점검 (2분)

IRSA는 결국 아래 3개가 맞물려야 합니다.

  1. ServiceAccount에 role arn annotation
kubectl -n <ns> get sa <sa> -o yaml | yq '.metadata.annotations'
  • eks.amazonaws.com/role-arn: arn:aws:iam::...:role/...
  1. Pod가 그 ServiceAccount를 사용
kubectl -n <ns> get pod <pod> -o jsonpath='{.spec.serviceAccountName}'; echo
  1. OIDC provider + trust policy가 일치
  • EKS 클러스터 OIDC issuer URL과 IAM OIDC provider가 동일해야 함
  • trust policy의 aud=sts.amazonaws.com, sub=system:serviceaccount:ns:sa 확인

IRSA는 “설정은 했는데 실제 Pod가 그 SA를 안 쓰는” 실수가 매우 많습니다(Deployment의 serviceAccountName 누락, Helm values 미반영 등).


6) CloudTrail로 403을 ‘증거’로 확정하기 (3분)

가장 빠른 종결 방법은 CloudTrail에서 해당 이벤트를 찾는 것입니다.

  • 이벤트 소스: kinesis.amazonaws.com 또는 firehose.amazonaws.com
  • 에러 코드: AccessDenied
  • userIdentity.arn으로 실제 Principal 확인

CLI로 최근 이벤트를 필터링하는 예:

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventSource,AttributeValue=kinesis.amazonaws.com \
  --max-results 20 \
  --region ap-northeast-2

찾은 이벤트의 Resources, EventName, ErrorMessage를 보면 다음이 명확해집니다.

  • 누가(어떤 role)
  • 무엇을(action)
  • 어디에(resource)
  • 왜(조건/명시적 deny)

조직 SCP나 permission boundary가 걸려 있으면 CloudTrail의 에러 메시지에 힌트가 남는 경우가 많습니다.


7) 10분 타임라인으로 따라 하는 “최단 진단 루트”

  • 1분: 앱 로그에서 Principal/Action/Resource 3종 추출
  • 2분: aws sts get-caller-identity로 실제 호출 Role 확정
  • 1분: 리전 환경변수/SDK 리전 확인
  • 3분: IAM 정책에서 Action/Resource 정확성 + Firehose/Streams 구분 + Deny 요소 점검
  • 2분: ServiceAccount annotation + Pod serviceAccountName + trust policy 일치 확인
  • 1분(여유): CloudTrail로 최종 증거 확보

이 루트대로 하면 “감으로 정책을 늘리는” 방식(보안 사고의 지름길)을 피하면서도 빠르게 원인을 고정할 수 있습니다.


부록) 재현 가능한 최소 코드로 권한/리전 문제 분리하기

애플리케이션이 복잡하면, 동일한 Pod/SA에서 가장 단순한 호출로 분리 테스트하는 게 좋습니다.

Python(boto3) - PutRecord 테스트

import os
import boto3

region = os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION") or "ap-northeast-2"
stream_name = os.environ["STREAM_NAME"]

client = boto3.client("kinesis", region_name=region)

resp = client.put_record(
    StreamName=stream_name,
    Data=b"hello",
    PartitionKey="pk1",
)
print(resp)

실행:

python test_kinesis.py
  • 여기서도 403이면 앱 로직 문제가 아니라 인증/인가/리전/리소스 문제입니다.

함께 보면 좋은 내부 글


결론

EKS Pod에서 Kinesis 403 AccessDenied는 대부분 (1) 실제 호출 Role 착각, (2) Action/Resource/리전 불일치, (3) Deny/SCP/Boundary/KMS 같은 조건 차단 중 하나입니다.

핵심은 “정책을 더 주기”가 아니라, STS로 호출자(Principal)를 확정하고, CloudTrail로 어떤 액션이 어떤 리소스에서 막혔는지 증거를 확보한 뒤, 필요한 최소 권한으로 수정하는 것입니다. 이 순서만 지키면 10분 내에 재현·확정·수정까지 도달할 수 있습니다.