Published on

EKS IRSA로 S3 403 해결 - OIDC·STS·정책 점검

Authors

서버리스나 노드 IAM Role 대신 IRSA(IAM Roles for Service Accounts)를 붙였는데도 애플리케이션에서 S3 호출이 403 AccessDenied로 떨어지면, 대부분은 **"권한이 없다"**가 아니라 **"어떤 주체(principal)로 호출되고 있는지"**가 기대와 달라서 발생합니다. 특히 EKS IRSA는 OIDC(웹 아이덴티티)와 STS(AssumeRoleWithWebIdentity), 그리고 IAM 정책/버킷 정책이 삼각형으로 맞물려야 정상 동작합니다.

이 글은 아래 순서로 원인을 좁힙니다.

  1. 파드가 실제로 어떤 IAM Role로 호출하는지(= STS 호출 성공 여부)
  2. OIDC Provider/Trust Policy가 맞는지
  3. IAM 정책(Identity policy)과 S3 버킷 정책(Resource policy)이 함께 허용하는지
  4. 흔한 함정(잘못된 SA, 누락된 sub, 조건 키 오타, KMS, VPC Endpoint 정책 등)

관련해서 OIDC 기반 인증/인가를 점검하는 접근은 ALB Ingress의 OIDC 문제를 다룬 글과도 유사합니다: EKS ALB Ingress 401 반복 - OIDC·JWT·헤더 점검

1) 먼저: 403의 "주체"부터 확인하기

S3 403을 해결하려면, 애플리케이션 로그만 보지 말고 파드 내부에서 현재 호출 주체가 누구인지부터 확인해야 합니다.

파드에서 STS로 현재 Role 확인

아래는 AWS SDK가 참조하는 환경 변수와 WebIdentity 토큰 파일이 제대로 잡혔는지, 그리고 실제로 STS가 어떤 ARN을 반환하는지 확인하는 방법입니다.

# 파드 접속
kubectl -n <namespace> exec -it <pod> -- sh

# IRSA가 주입되면 보통 아래 두 값이 존재
env | grep -E 'AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION|AWS_DEFAULT_REGION'

# STS로 현재 호출 주체 확인 (awscli가 없다면 임시로 설치하거나 디버그 이미지 사용)
aws sts get-caller-identity

정상이라면 ArnIRSA로 만든 Role ARN(예: arn:aws:sts::123456789012:assumed-role/my-irsa-role/...)로 나와야 합니다.

  • 만약 node role(워커 노드 IAM Role)로 보이면: IRSA가 적용되지 않았거나 SDK가 WebIdentity를 쓰지 않고 있습니다.
  • 만약 STS 호출 자체가 실패하면: Trust Policy 또는 OIDC Provider/토큰 조건이 깨졌을 가능성이 큽니다.

awscli가 없을 때: 토큰/Role ARN만이라도 확인

cat $AWS_WEB_IDENTITY_TOKEN_FILE | head -c 30; echo
printf '%s\n' "$AWS_ROLE_ARN"

AWS_WEB_IDENTITY_TOKEN_FILE이 비어 있거나 파일이 없으면, IRSA 주입이 안 된 상태입니다.

2) IRSA 기본 구성 체크: SA 어노테이션과 파드 바인딩

IRSA는 ServiceAccount 단위로 Role을 매핑합니다. 따라서 다음이 모두 일치해야 합니다.

  1. 파드가 사용하는 serviceAccountName
  2. 해당 ServiceAccount에 eks.amazonaws.com/role-arn 어노테이션
  3. Role Trust Policy의 sub 조건이 system:serviceaccount:<namespace>:<sa-name>와 일치

ServiceAccount 매니페스트 예시

apiVersion: v1
kind: ServiceAccount
metadata:
  name: s3-reader
  namespace: app
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/app-s3-reader-irsa

Deployment에서 ServiceAccount 사용 예시

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  namespace: app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      serviceAccountName: s3-reader
      containers:
        - name: demo
          image: public.ecr.aws/docker/library/alpine:3.19
          command: ["sh", "-c", "sleep 3600"]

실제 파드가 어떤 SA를 쓰는지 확인

kubectl -n app get pod <pod> -o jsonpath='{.spec.serviceAccountName}'; echo
kubectl -n app get sa s3-reader -o yaml | sed -n '1,120p'

여기서 자주 나는 실수:

  • default ServiceAccount로 떠 있는데, Role 어노테이션은 다른 SA에만 붙여둔 경우
  • namespace가 다른 SA를 보고 있는 경우
  • Helm/ArgoCD가 SA를 재생성하면서 어노테이션이 사라진 경우

3) OIDC Provider가 클러스터에 연결되어 있는지 확인

IRSA는 EKS 클러스터의 OIDC Issuer URL에 대해 IAM OIDC Provider가 등록되어 있어야 합니다.

클러스터 OIDC Issuer 확인

aws eks describe-cluster \
  --name <cluster-name> \
  --query 'cluster.identity.oidc.issuer' \
  --output text

출력 예시는 보통 https://oidc.eks.<region>.amazonaws.com/id/<hash> 형태입니다. 이 값에서 https://를 뺀 문자열이 Trust Policy의 Condition 키에 들어갑니다.

IAM에 OIDC Provider가 등록되어 있는지 확인

aws iam list-open-id-connect-providers

# ARN을 찾았다면 상세 확인
aws iam get-open-id-connect-provider --open-id-connect-provider-arn <provider-arn>

OIDC Provider가 없다면 eksctl utils associate-iam-oidc-provider로 연결하는 방식이 일반적입니다.

eksctl utils associate-iam-oidc-provider \
  --cluster <cluster-name> \
  --approve

4) Trust Policy(AssumeRoleWithWebIdentity) 점검

S3 403이지만 근본적으로는 Role Assume이 실패해서 다른 주체로 호출되는 경우가 많습니다. IRSA Role의 Trust Policy를 확인하세요.

올바른 Trust Policy 예시

아래는 대표적인 형태입니다. 핵심은 Principal.Federated가 OIDC Provider ARN을 가리키고, Actionsts:AssumeRoleWithWebIdentity이며, Condition에서 subaud를 정확히 제한하는 것입니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEFG"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEFG:sub": "system:serviceaccount:app:s3-reader",
          "oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEFG:aud": "sts.amazonaws.com"
        }
      }
    }
  ]
}

자주 틀리는 포인트

  • sub의 namespace/SA 이름 오타
  • aud 누락 또는 값 불일치(대부분 sts.amazonaws.com)
  • Condition 키에서 OIDC issuer host/path가 정확히 일치하지 않음
    • 예: https://를 포함하거나, id/<hash> 일부가 다른 값

Trust Policy가 잘못되면 파드 내부에서 aws sts get-caller-identity가 실패하거나, SDK가 fallback으로 다른 자격 증명을 잡아서 의도치 않은 Role로 S3를 호출할 수 있습니다.

5) IAM 정책(Identity policy)에서 S3 권한을 정확히 부여했는지

IRSA Role에 붙은 IAM 정책이 S3 작업을 허용해야 합니다. 여기서 흔한 실수는 ListBucketGetObject의 리소스 ARN을 섞는 것입니다.

최소 권한 예시: 특정 prefix 읽기

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListBucket",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::my-bucket"],
      "Condition": {
        "StringLike": {
          "s3:prefix": ["reports/*"]
        }
      }
    },
    {
      "Sid": "GetObjects",
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::my-bucket/reports/*"]
    }
  ]
}
  • s3:ListBucket은 버킷 ARN(arn:aws:s3:::my-bucket)에 걸어야 합니다.
  • s3:GetObject는 오브젝트 ARN(arn:aws:s3:::my-bucket/path/*)에 걸어야 합니다.

권한은 있는데도 403? 실제 호출 API를 확인

애플리케이션이 내부적으로 HeadObject, GetObjectAttributes, ListObjectsV2 등을 호출할 수 있습니다. SDK/라이브러리별로 필요한 권한이 달라 403이 날 수 있습니다.

권한 설계가 애매하면 CloudTrail에서 eventName을 확인해, 거절된 액션을 그대로 정책에 반영하는 방식이 빠릅니다.

6) 버킷 정책(Resource policy) 또는 조직 정책(SCP)로 막히는 경우

IAM Role 정책이 허용해도, S3는 버킷 정책에서 명시적으로 막으면 403이 납니다.

버킷 정책에서 Principal 제한 확인

예를 들어 버킷 정책이 특정 Role ARN만 허용하도록 되어 있는데, 실제 호출 주체가 다른 Role이면 무조건 403입니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowOnlySpecificRole",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/app-s3-reader-irsa"
      },
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::my-bucket/reports/*"
    }
  ]
}

여기서도 핵심은 1번에서 확인한 get-caller-identity 결과 ARN과 버킷 정책의 Principal이 일치하는지입니다.

또한 AWS Organizations를 쓰면 SCPs3:* 또는 특정 리전을 제한해 403을 만들 수 있습니다. 이 경우 CloudTrail의 errorCode/errorMessage에 힌트가 남는 편입니다.

7) KMS로 암호화된 S3 객체라면: KMS 권한도 필요

SSE-KMS로 암호화된 객체를 읽을 때는 S3 권한만으로는 부족하고, KMS 키에 대한 kms:Decrypt 권한이 추가로 필요합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["kms:Decrypt", "kms:DescribeKey"],
      "Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/11111111-2222-3333-4444-555555555555"
    }
  ]
}

그리고 KMS Key policy(Resource policy)에서도 해당 Role을 신뢰해야 합니다. KMS는 S3보다 정책 조합이 더 엄격하게 느껴질 수 있습니다.

8) VPC Endpoint(S3 Gateway Endpoint) 정책으로 막히는 경우

프라이빗 서브넷에서 S3로 나갈 때 S3 Gateway Endpoint를 쓰는 환경이라면, Endpoint policy가 허용하지 않으면 403이 날 수 있습니다.

  • 증상: 같은 Role/정책인데도 어떤 VPC/서브넷에서는 되고 어떤 곳에서는 403
  • 해결: VPC Endpoint policy에서 대상 버킷/액션을 허용

Endpoint 정책은 네트워크 레벨처럼 보이지만, 결과는 S3 403으로 나타날 수 있어 헷갈립니다.

9) 재현 가능한 "원인 좁히기" 디버그 루틴

현장에서 시간을 가장 절약해주는 루틴은 다음입니다.

(1) 파드 내부에서 주체 확인

aws sts get-caller-identity

(2) 같은 파드에서 최소 S3 API 호출

aws s3api head-bucket --bucket my-bucket
aws s3api list-objects-v2 --bucket my-bucket --prefix reports/ --max-keys 1
aws s3api head-object --bucket my-bucket --key reports/sample.csv

여기서 어떤 API가 403인지에 따라 필요한 액션이 갈립니다.

(3) CloudTrail에서 Deny 이벤트 확인

CloudTrail에서 AccessDenied 이벤트를 보면 다음을 체크합니다.

  • userIdentity.arn이 기대한 IRSA Role인지
  • eventName이 무엇인지
  • resources가 어떤 ARN인지
  • errorMessageexplicit deny가 있는지

(4) Trust Policy와 sub를 다시 대조

system:serviceaccount:app:s3-reader가 정확한지, namespace/SA 이름이 배포와 동일한지 확인합니다.

10) 자주 만나는 실수 체크리스트(요약)

  • 파드의 serviceAccountName이 기대한 SA인가
  • SA에 eks.amazonaws.com/role-arn 어노테이션이 존재하는가
  • 클러스터 OIDC Issuer와 IAM OIDC Provider가 연결되어 있는가
  • Role Trust Policy에 sts:AssumeRoleWithWebIdentity + 올바른 sub/aud 조건이 있는가
  • 파드에서 aws sts get-caller-identity가 IRSA Role로 나오는가
  • IAM 정책에서 ListBucket(버킷 ARN)과 GetObject(오브젝트 ARN)가 올바른가
  • 버킷 정책에서 Principal 제한/명시적 Deny가 없는가
  • SSE-KMS라면 KMS 권한 및 Key policy가 맞는가
  • S3 VPC Endpoint policy가 허용하는가

마무리

EKS IRSA에서 S3 403은 단순히 정책 한 줄이 빠진 문제가 아니라, **OIDC(신원) → STS(역할 가정) → S3 정책(권한)**이 연쇄적으로 맞는지 확인하는 문제입니다. 가장 먼저 파드 내부에서 sts get-caller-identity로 "내가 누구인가"를 확정하고, 그 다음 Trust Policy의 sub/aud, 마지막으로 IAM 정책과 버킷 정책을 맞추면 대부분의 403은 짧은 시간 안에 해결됩니다.

OIDC 기반 트러블슈팅을 더 넓은 관점에서 보고 싶다면, 인증 헤더/토큰/프로바이더 불일치를 다룬 글도 참고할 만합니다: EKS ALB Ingress 401 반복 - OIDC·JWT·헤더 점검