Published on

EKS IRSA로 Pod IAM 권한 최소화 실전 가이드

Authors

EKS에서 애플리케이션 Pod가 AWS API를 호출해야 할 때, 가장 흔한 안티패턴은 node instance role 에 과도한 권한을 몰아주는 방식입니다. 이 방식은 한 번 열어둔 권한이 클러스터 전체로 확산되기 쉽고, 특정 네임스페이스나 특정 서비스만 써야 하는 권한을 통제하기도 어렵습니다.

IRSA(IAM Roles for Service Accounts)는 Kubernetes ServiceAccount 와 IAM Role을 1:1로 연결해 Pod 단위로 권한을 부여하는 메커니즘입니다. 핵심은 다음 한 줄입니다.

  • Pod는 노드의 자격 증명을 쓰지 않고, OIDC 기반으로 STS AssumeRoleWithWebIdentity 를 통해 “자기 ServiceAccount에 매핑된 Role”만 임시 자격 증명으로 획득한다

이 글에서는 IRSA를 “동작하게 만드는 수준”이 아니라, 운영에서 사고를 줄이는 “권한 최소화 실전” 관점으로 설계와 구현 체크리스트를 제공합니다. AccessDenied로 막힐 때는 함께 보면 좋은 글로 EKS IRSA AccessDenied 권한 오류 빠른 해결도 참고하세요.

IRSA가 권한 최소화에 유리한 이유

1) 권한 경계가 노드에서 Pod로 내려온다

노드 Role은 기본적으로 해당 노드에서 실행되는 모든 Pod에 영향을 줄 수 있습니다. 반면 IRSA는 ServiceAccount 단위로 Role을 분리하므로, 예를 들어 payments 네임스페이스의 s3-uploader Pod만 S3 PutObject 권한을 갖게 할 수 있습니다.

2) 임시 자격 증명으로 회전과 유출 리스크를 줄인다

IRSA는 STS 기반 임시 자격 증명을 사용합니다. 정적 Access Key를 Secret에 넣는 방식 대비 유출 시 피해 범위와 지속 시간이 줄어듭니다.

3) 감사와 추적이 쉬워진다

CloudTrail에서 AssumeRoleWithWebIdentity 와 Role 단위 이벤트로 애플리케이션별 행위를 분리해 추적할 수 있습니다.

사전 준비: OIDC Provider 연결 확인

IRSA는 EKS 클러스터에 OIDC Provider가 연결되어 있어야 합니다. 보통 eksctl utils associate-iam-oidc-provider 로 구성합니다.

aws eks describe-cluster \
  --name my-eks \
  --query "cluster.identity.oidc.issuer" \
  --output text

출력 예시는 https://oidc.eks.ap-northeast-2.amazonaws.com/id/XXXXXXXX 형태입니다. 이 URL이 IAM OIDC Provider로 등록되어 있어야 합니다.

eksctl 사용 예시는 다음과 같습니다.

eksctl utils associate-iam-oidc-provider \
  --cluster my-eks \
  --approve

실전 설계: 최소 권한을 위한 4가지 원칙

원칙 1) Role은 “업무 단위”로 쪼개고 재사용을 경계한다

예를 들어 “백엔드 공통 Role” 같이 크게 묶으면 다시 권한이 비대해집니다.

  • s3-uploader-role
  • dynamodb-reader-role
  • sqs-consumer-role

처럼 기능 단위로 쪼개고, 정말 필요할 때만 합칩니다.

원칙 2) Trust Policy에서 sub 를 네임스페이스와 SA로 고정한다

IRSA의 최소 권한은 권한 정책만이 아니라 “누가 이 Role을 Assume할 수 있는가”가 결정합니다.

Trust Policy에서 Conditionsubsystem:serviceaccount:{namespace}:{serviceaccount} 로 고정해야 합니다.

원칙 3) 권한 정책은 Resource를 좁히고, 가능한 경우 조건을 건다

S3라면 특정 버킷과 prefix로 제한하고, DynamoDB라면 특정 테이블 ARN으로 제한합니다.

원칙 4) Pod에서 ServiceAccount 토큰 자동 마운트를 통제한다

IRSA는 웹 아이덴티티 토큰을 사용합니다. 필요하지 않은 Pod까지 토큰이 마운트되면 공격 표면이 커집니다.

  • 기본: 네임스페이스 또는 SA에서 automountServiceAccountToken 을 명시
  • 필요 없는 워크로드는 false

예제 시나리오: 특정 버킷에만 업로드하는 Pod

요구사항:

  • 네임스페이스 apps
  • ServiceAccount s3-uploader
  • S3 버킷 my-company-uploads
  • prefix incoming/ 아래에만 PutObject 허용
  • 버킷 리스트 권한은 최소화

1) IAM Policy 작성

PutObject 는 오브젝트 ARN에 적용되므로 arn:aws:s3:::버킷/프리픽스* 형태로 제한합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPutOnlyToIncomingPrefix",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:AbortMultipartUpload",
        "s3:ListMultipartUploadParts"
      ],
      "Resource": "arn:aws:s3:::my-company-uploads/incoming/*"
    },
    {
      "Sid": "AllowBucketLocationIfNeeded",
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketLocation"
      ],
      "Resource": "arn:aws:s3:::my-company-uploads"
    }
  ]
}

애플리케이션이 버킷을 탐색할 필요가 없다면 s3:ListBucket 은 넣지 않는 편이 안전합니다. SDK가 내부적으로 필요로 하는 액션만 최소로 추가하세요.

2) IAM Role Trust Policy 작성

아래에서 issuer 는 OIDC issuer에서 https:// 를 제거한 값이 들어갑니다. 또한 sub 를 네임스페이스와 ServiceAccount로 고정합니다.

주의: 본문에 부등호가 노출되면 MDX 빌드가 깨질 수 있으니, 비교 연산자나 제네릭 표기가 필요하면 항상 인라인 코드로 감싸세요.

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

여기서 최소 권한의 핵심은 sub 고정입니다. system:serviceaccount:apps:* 같이 와일드카드를 열어두면 같은 네임스페이스의 다른 Pod가 Role을 가로채기 쉬워집니다.

3) ServiceAccount에 Role ARN 어노테이션

EKS는 eks.amazonaws.com/role-arn 어노테이션을 통해 IRSA를 연결합니다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: s3-uploader
  namespace: apps
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/s3-uploader-role
automountServiceAccountToken: true

automountServiceAccountToken 은 IRSA를 쓰는 Pod에 필요하므로 true 로 둡니다. 반대로 IRSA가 필요 없는 ServiceAccount는 false 를 고려하세요.

4) Deployment에서 ServiceAccount 지정

apiVersion: apps/v1
kind: Deployment
metadata:
  name: uploader
  namespace: apps
spec:
  replicas: 2
  selector:
    matchLabels:
      app: uploader
  template:
    metadata:
      labels:
        app: uploader
    spec:
      serviceAccountName: s3-uploader
      containers:
        - name: app
          image: public.ecr.aws/docker/library/python:3.12-slim
          command: ["python", "-c"]
          args:
            - |
              import boto3
              s3 = boto3.client("s3")
              s3.put_object(Bucket="my-company-uploads", Key="incoming/hello.txt", Body=b"hi")
              print("uploaded")

이제 이 Pod는 노드 Role이 아니라 s3-uploader-role 로만 S3에 접근합니다.

검증: Pod가 어떤 IAM을 쓰는지 확인하는 방법

1) 환경 변수 확인

IRSA가 정상이라면 컨테이너 내부에 다음 환경 변수가 존재합니다.

  • AWS_ROLE_ARN
  • AWS_WEB_IDENTITY_TOKEN_FILE
kubectl -n apps exec deploy/uploader -- env | grep AWS_

2) STS Caller Identity로 Role 확인

kubectl -n apps exec deploy/uploader -- bash -lc \
  'python - <<"PY"
import boto3
print(boto3.client("sts").get_caller_identity())
PY'

출력의 Arnassumed-role/s3-uploader-role 형태로 나오면 성공입니다.

운영에서 자주 터지는 함정과 대응

함정 1) AccessDenied인데 정책은 맞는 것 같다

대부분은 Trust Policy의 sub 또는 OIDC issuer 불일치입니다. 또는 Pod가 기대한 ServiceAccount를 쓰지 않고 default 를 쓰는 경우가 많습니다.

  • Deployment의 serviceAccountName 확인
  • ServiceAccount 어노테이션 확인
  • Trust Policy의 issuer 문자열, aud 확인

빠른 디버깅 루틴은 EKS IRSA AccessDenied 권한 오류 빠른 해결에 정리해 두었습니다.

함정 2) 권한을 노드 Role과 IRSA에 이중으로 주고 “되니까” 방치

이러면 IRSA로 최소 권한을 설계해도 실제로는 노드 Role로 우회 접근이 가능합니다.

  • 원칙적으로 애플리케이션 AWS 권한은 IRSA로만 부여
  • 노드 Role은 CNI, EBS CSI, 로그/메트릭 등 노드 애드온에 필요한 최소 권한만

함정 3) S3 권한에서 ListBucket 을 과도하게 열어둠

SDK나 라이브러리가 HeadBucket 또는 GetBucketLocation 을 요구하는 경우가 있어 “일단 s3:*” 같은 처방이 나오기 쉽습니다. S3는 액션별 리소스 스코프가 달라서 정책이 미묘합니다.

  • 오브젝트 작업: arn:aws:s3:::bucket/prefix/*
  • 버킷 작업: arn:aws:s3:::bucket

함정 4) 외부에서 공급되는 이미지가 토큰을 읽어갈 수 있는 위험

IRSA는 ServiceAccount 토큰을 사용합니다. 취약한 이미지나 공급망 이슈가 있으면 토큰이 유출될 수 있습니다.

  • IRSA가 필요 없는 워크로드는 automountServiceAccountToken: false
  • 네임스페이스 기본값으로 토큰 마운트를 끄고, 필요한 SA만 켜는 전략 검토
  • NetworkPolicy로 메타데이터/외부 통신 최소화

Terraform로 IRSA 구성 자동화 예시

운영에서는 수동 클릭보다 IaC가 안전합니다. 아래는 IRSA Role과 Policy를 만드는 Terraform 예시의 뼈대입니다.

data "aws_eks_cluster" "this" {
  name = var.cluster_name
}

data "aws_iam_openid_connect_provider" "this" {
  url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer
}

resource "aws_iam_policy" "s3_uploader" {
  name   = "s3-uploader-policy"
  policy = file("policies/s3-uploader.json")
}

resource "aws_iam_role" "s3_uploader" {
  name = "s3-uploader-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = data.aws_iam_openid_connect_provider.this.arn
      }
      Action = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "${replace(data.aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")}:sub" = "system:serviceaccount:apps:s3-uploader"
          "${replace(data.aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")}:aud" = "sts.amazonaws.com"
        }
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "attach" {
  role       = aws_iam_role.s3_uploader.name
  policy_arn = aws_iam_policy.s3_uploader.arn
}

이후 Kubernetes ServiceAccount 는 Helm이나 Kustomize로 배포하면서 eks.amazonaws.com/role-arn 만 주입하면 됩니다.

체크리스트: “최소 권한 IRSA” 합격 기준

  • Trust Policy의 sub 가 정확히 system:serviceaccount:ns:sa 로 고정되어 있는가
  • audsts.amazonaws.com 으로 고정되어 있는가
  • IAM Policy에서 ActionResource 가 최소 범위인가
  • Deployment가 의도한 serviceAccountName 을 사용하고 있는가
  • 노드 Role에 동일 권한이 남아 있지 않은가
  • 필요 없는 워크로드의 automountServiceAccountToken 을 꺼서 공격 표면을 줄였는가

마무리

IRSA는 “EKS에서 AWS 권한을 주는 또 하나의 방법”이 아니라, 멀티테넌시와 제로트러스트에 가까운 운영을 가능하게 하는 기본기입니다. 특히 사고는 보통 정책을 하나 더 열어서 해결할 때가 아니라, 열어둔 권한이 다른 Pod로 전이될 때 발생합니다.

IRSA를 도입했다면 다음 단계는 반드시 “Role을 잘게 쪼개기”와 “Trust Policy의 sub 고정”입니다. 그리고 AccessDenied나 403을 만났을 때는 권한 정책뿐 아니라 Assume 경로(issuer, aud, sub, SA 매핑)를 함께 점검해야 합니다.

추가로 AWS API 권한 이슈를 체계적으로 디버깅하는 관점은 AWS Bedrock InvokeModel 403·Throttling 해결 - IAM·VPC·쿼터도 참고하면, “정책은 맞는데 왜 막히지” 같은 상황에서 원인 분리가 더 빨라집니다.