- Published on
EKS Pod에서 Kinesis 403 AccessDenied 10분 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스·컨테이너 환경에서 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가지로 나뉩니다.
- 정책(Action/Resource) 자체가 없음
- 예:
kinesis:PutRecord권한 누락
- 예:
- 리소스 ARN/리전/계정이 불일치
- 예:
ap-northeast-2스트림인데us-east-1로 호출
- 예:
- 조건(Condition) 또는 추가 서비스(KMS/VPC Endpoint)에서 차단
- 예: SSE-KMS 사용 중인데
kms:Decrypt/kms:GenerateDataKey누락
- 예: SSE-KMS 사용 중인데
에러 메시지에 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_ARN과AWS_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개가 맞물려야 합니다.
- ServiceAccount에 role arn annotation
kubectl -n <ns> get sa <sa> -o yaml | yq '.metadata.annotations'
eks.amazonaws.com/role-arn: arn:aws:iam::...:role/...
- Pod가 그 ServiceAccount를 사용
kubectl -n <ns> get pod <pod> -o jsonpath='{.spec.serviceAccountName}'; echo
- 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/Resource3종 추출 - 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이면 앱 로직 문제가 아니라 인증/인가/리전/리소스 문제입니다.
함께 보면 좋은 내부 글
- IRSA 자체가 꼬였을 때: EKS IRSA 설정했는데 STS AccessDenied 뜰 때
- IRSA는 되는데 특정 AWS 서비스만 403일 때의 접근법(사례 확장): EKS Pod에서 IRSA는 되는데 DynamoDB 403 해결
- 디버깅 중 exec/logs가 막혀 진단이 안 될 때: EKS에서 kubectl exec·logs가 안 될 때 진단법
결론
EKS Pod에서 Kinesis 403 AccessDenied는 대부분 (1) 실제 호출 Role 착각, (2) Action/Resource/리전 불일치, (3) Deny/SCP/Boundary/KMS 같은 조건 차단 중 하나입니다.
핵심은 “정책을 더 주기”가 아니라, STS로 호출자(Principal)를 확정하고, CloudTrail로 어떤 액션이 어떤 리소스에서 막혔는지 증거를 확보한 뒤, 필요한 최소 권한으로 수정하는 것입니다. 이 순서만 지키면 10분 내에 재현·확정·수정까지 도달할 수 있습니다.