Skip to content

EKS Auth

이번 글에서는 EKS 환경에서의 인증/인가 흐름을 처음부터 끝까지 따라가본다. 단순히 "이렇게 설정하면 된다"가 아니라, kubectl get node를 쳤을 때 내부에서 어떤 일이 벌어지는지, 그리고 파드가 AWS 리소스를 쓸 때 자격증명이 어떻게 주입되는지를 실습과 함께 정리했다.

크게 세 가지 주제를 다룬다.

  • 관리자가 EKS API를 호출할 때의 인증/인가 과정
  • AWS IAM 주체를 K8S Subject에 매핑하는 두 가지 방법 (ConfigMap vs Access Entry)
  • 파드가 AWS 리소스를 사용하기 위한 두 가지 방법 (IRSA vs Pod Identity)

kubectl get node를 치면 무슨 일이 벌어지는가

EKS에서 kubectl get node를 실행하면 총 6단계의 과정을 거친다. 하나씩 따라가보자.

토큰 생성 (클라이언트 측)

kubectl은 ~/.kube/config에 정의된 exec 플러그인을 통해 토큰을 생성한다. EKS의 경우 aws eks get-token 명령이 실행된다.

# ~/.kube/config 중 user 부분
users:
  - name: arn:aws:eks:eu-west-2:<TRUNCATED>:cluster/myeks
    user:
      exec:
        apiVersion: client.authentication.k8s.io/v1beta1
        args:
          - --region
          - eu-west-2
          - eks
          - get-token
          - --cluster-name
          - myeks
        command: aws

이 명령이 하는 일은 STS GetCallerIdentity의 Pre-signed URL을 만들어서 Base64 인코딩한 뒤, 앞에 k8s-aws-v1.을 붙이는 것이다.

실제로 토큰을 디코딩해보면 그냥 URL이 나온다.

# 토큰 발급
TOKEN_DATA=$(aws eks get-token --cluster-name myeks | jq -r '.status.token')
echo $TOKEN_DATA
# k8s-aws-v1.aHR0cHM6Ly9zdHMuZXUtd2VzdC0yLu...

# 페이로드를 Base64 디코딩하면 Pre-signed URL이 나온다
IFS='.' read header payload signature <<< "$TOKEN_DATA"
echo "$payload" | fold -w 4 | sed '$ d' | tr -d '\n' | base64 --decode

디코딩 결과를 보기 좋게 정리하면 이런 모양이다.

https://sts.eu-west-2.amazonaws.com/
  ?Action=GetCallerIdentity
  &Version=2011-06-15
  &X-Amz-Algorithm=AWS4-HMAC-SHA256
  &X-Amz-Credential=<TRUNCATED>/20260407/eu-west-2/sts/aws4_request
  &X-Amz-Date=20260407T135015Z
  &X-Amz-Expires=60
  &X-Amz-SignedHeaders=host;x-k8s-aws-id
  &X-Amz-Signature=<TRUNCATED>

핵심은 이 토큰 자체가 "나는 이 AWS Access Key의 소유자다"라는 서명된 증거라는 점이다. 실제 Secret Key는 네트워크에 한 번도 노출되지 않는다.

SigV4 서명 과정 (심화)

aws eks get-token이 내부적으로 수행하는 SigV4 서명은 4단계로 진행된다.

비유하자면, 시크릿 키는 "마스터 도장"이고, 이걸 직접 찍는 게 아니라 날짜/지역/서비스별로 파생 도장을 만들어서 사용하는 방식이다.

DateKey       = HMAC-SHA256("AWS4" + SecretKey, "20260407")
RegionKey     = HMAC-SHA256(DateKey, "eu-west-2")
ServiceKey    = HMAC-SHA256(RegionKey, "sts")
SigningKey    = HMAC-SHA256(ServiceKey, "aws4_request")

이렇게 만들어진 SigningKey로 요청을 서명한다. 만약 이 서명이 탈취되더라도, 특정 날짜/리전/서비스에서만 유효하기 때문에 피해를 최소화할 수 있다.

AWS 서버 측에서는 동일한 과정을 거쳐 서명을 재현하고, 클라이언트가 보낸 서명과 비교한다. 일치하면 "이 사람은 Secret Key를 가지고 있는 사람이 맞다"고 판단한다.

Bearer Token으로 EKS API 호출

kubectl의 Client-Go 라이브러리는 위에서 만든 토큰을 Bearer Token으로 EKS API Endpoint에 보낸다.

# kubectl get node -v=10 으로 확인해보면
curl -XGET \
  -H "Authorization: Bearer k8s-aws-v1.aHR0cHM6Ly..." \
  'https://<TRUNCATED>.gr7.eu-west-2.eks.amazonaws.com/api/v1/nodes?limit=500'

직접 curl로 호출해볼 수도 있다. 토큰의 유효시간은 15분이므로 그 안에 사용해야 한다.

TOKEN_DATA=$(aws eks get-token --cluster-name myeks | jq -r '.status.token')

curl -k -s -XGET \
  -H "Authorization: Bearer $TOKEN_DATA" \
  -H "Accept: application/json" \
  'https://<TRUNCATED>.gr7.eu-west-2.eks.amazonaws.com/api/v1/nodes?limit=500' | jq

Token Review

EKS API 서버는 받은 토큰을 검증하기 위해 Webhook Token Authentication을 사용한다. 내부의 aws-iam-authenticator 서버가 토큰에 포함된 Pre-signed URL을 AWS STS에 제출하여 서명을 검증한다.

직접 TokenReview를 요청해볼 수 있다.

TOKEN_DATA=$(aws eks get-token --cluster-name myeks | jq -r '.status.token')

cat > token-review.yaml << EOF
apiVersion: authentication.k8s.io/v1
kind: TokenReview
metadata:
  name: mytoken
spec:
  token: ${TOKEN_DATA}
EOF

kubectl create -f token-review.yaml -v=9

응답에서 핵심 부분을 보면 다음과 같다.

{
  "status": {
    "authenticated": true,
    "user": {
      "username": "arn:aws:iam::<TRUNCATED>:user/admin",
      "uid": "aws-iam-authenticator:<TRUNCATED>",
      "groups": ["system:authenticated"]
    }
  }
}

여기서 groups에 system:masters가 안 보이는 게 정상이다. EKS를 생성한 IAM 주체는 자동으로 system:masters 권한을 갖지만, TokenReview 응답에는 표시되지 않는다.

GetCallerIdentity 호출 (AWS STS)

aws-iam-authenticator 서버는 토큰에 포함된 Pre-signed URL을 STS에 제출한다. STS는 서명을 검증하고 "이 사람은 IAM user/admin이 맞다"라고 응답한다.

이 과정은 CloudTrail에서 확인할 수 있다. - 이벤트 소스: sts.amazonaws.com - 이벤트 이름: GetCallerIdentity - userAgent: Go-http-client/1.1 (aws-iam-authenticator가 Go로 작성됨)

인가(Authorization) 처리

인증이 끝나면 인가 단계로 넘어간다. 여기서 두 가지 방식이 있다.

AWS IAM -> K8S Subject 매핑: ConfigMap vs Access Entry

인증된 IAM 주체를 K8S 내부의 사용자/그룹에 매핑하는 방식은 두 가지가 있다.

aws-auth ConfigMap (Deprecated)

기존에는 kube-system 네임스페이스의 aws-auth ConfigMap에 매핑 정보를 직접 기록했다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-auth
  namespace: kube-system
data:
  mapRoles: |
    - rolearn: arn:aws:iam::<TRUNCATED>:role/myeks-ng-1
      groups:
      - system:bootstrappers
      - system:nodes
      username: system:node:{{EC2PrivateDNSName}}
  mapUsers: |
    - groups:
      - system:masters
      userarn: arn:aws:iam::<TRUNCATED>:user/admin
      username: kubernetes-admin

이 방식의 문제점은 명확하다.

  • 사람이 직접 YAML을 편집하므로 휴먼 에러가 발생하기 쉽다
  • 실수로 system:nodes를 삭제하면 모든 노드가 NotReady 상태에 빠진다
  • 별도의 ClusterRole/ClusterRoleBinding을 만들어야 하므로 관리 리소스가 늘어난다
  • 현재 Deprecated 상태다

EKS API Access Entry (권장)

Access Entry는 AWS EKS API를 통해 매핑을 관리한다. YAML을 직접 수정할 필요 없이 AWS API/콘솔에서 설정한다.

# 현재 Access Entry 확인
aws eks list-access-entries --cluster-name myeks | jq

# testuser에 대한 Access Entry 생성
aws eks create-access-entry \
  --cluster-name myeks \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser

# 관리형 정책 연결
aws eks associate-access-policy \
  --cluster-name myeks \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy \
  --access-scope type=cluster

Access Entry 방식의 핵심 차이점은 인가를 K8S RBAC이 아닌 AWS 측 Webhook으로도 처리할 수 있다는 것이다.

EKS API 서버의 인가 모드가 Node, RBAC, Webhook 세 가지 전부를 사용한다. Webhook이 먼저 검토하고, 모르는 유저면 "No Opinion"을 반환하여 RBAC으로 넘긴다.

직접 SubjectAccessReview로 확인해볼 수 있다.

ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)

cat > sar-request.yaml << EOF
apiVersion: authorization.k8s.io/v1
kind: SubjectAccessReview
spec:
  user: "arn:aws:iam::${ACCOUNT_ID}:user/admin"
  groups:
    - system:masters
  resourceAttributes:
    namespace: "kube-system"
    verb: "get"
    resource: "pods"
EOF

kubectl create -f sar-request.yaml -v=8
# 응답: "allowed": true

두 방식 비교

구분 aws-auth ConfigMap Access Entry (EKS API)
데이터 저장소 K8s ConfigMap (etcd) AWS EKS 내부 DB
관리 방식 YAML 직접 편집 AWS API/콘솔
인가 방식 K8S RBAC만 사용 Access Policy (Webhook) 사용 가능
추가 리소스 ClusterRole/Binding 직접 생성 필요 EKS 관리형 정책 선택만 하면 됨
장애 위험 YAML 실수 시 노드 NotReady 가능 API 기반이라 상대적으로 안전
상태 Deprecated 권장

정책이 중복되면 EKS API가 우선되고 ConfigMap은 무시된다.

커스텀 RBAC과 Access Entry 조합 실습

EKS 관리형 정책 대신 직접 만든 ClusterRole을 사용할 수도 있다. Access Entry에서 kubernetesGroups만 지정하고, 해당 그룹에 대한 ClusterRoleBinding을 직접 생성하는 방식이다.

# ClusterRole 생성
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-viewer-role
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["list", "get", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-admin-role
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["*"]
EOF

# ClusterRoleBinding 생성 - 그룹에 바인딩
kubectl create clusterrolebinding viewer-role-binding \
  --clusterrole=pod-viewer-role --group=pod-viewer
kubectl create clusterrolebinding admin-role-binding \
  --clusterrole=pod-admin-role --group=pod-admin

# Access Entry에서 testuser를 pod-viewer 그룹에 매핑
aws eks create-access-entry \
  --cluster-name myeks \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser \
  --kubernetes-group pod-viewer

# testuser로 테스트 - pod 조회는 되지만 삭제는 안 됨
kubectl auth can-i get pods --all-namespaces    # yes
kubectl auth can-i delete pods --all-namespaces # no

# pod-admin으로 업데이트하면 삭제도 가능해진다
aws eks update-access-entry \
  --cluster-name myeks \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser \
  --kubernetes-group pod-admin

이 방식은 AWS Webhook이 먼저 확인하고 "No Opinion"을 반환한 뒤, K8S RBAC에서 커스텀 ClusterRoleBinding을 통해 인가하는 하이브리드 동작이다.

워커 노드(EC2)가 EKS API를 호출하는 과정

관리자뿐만 아니라 워커 노드도 EKS API를 호출한다. 과정은 관리자와 동일하지만 사용하는 자격증명이 다르다.

# 워커 노드에 SSM으로 접속
aws ssm start-session --target $NODE1

# EC2의 IAM Role 확인 - Instance Profile을 통한 AssumedRole
aws sts get-caller-identity --query Arn
# "arn:aws:sts::<TRUNCATED>:assumed-role/myeks-ng-1/i-<TRUNCATED>"

관리자는 IAM User의 영구 Access Key(AKIA로 시작)를 사용하고, 워커 노드는 EC2 Instance Profile의 임시 Access Key(ASIA로 시작)를 사용한다.

Access Entry에서 워커 노드의 IAM Role은 system:nodes 그룹에 매핑되어 있다.

aws eks describe-access-entry \
  --cluster-name myeks \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:role/myeks-ng-1 | jq
# "kubernetesGroups": ["system:nodes"]
# "type": "EC2_LINUX"

참고로 AWS IAM 자격증명의 접두사로 유형을 구분할 수 있다.

접두사 대상 설명
AKIA IAM Access Key 영구 키. IAM 사용자의 고정 액세스 키
ASIA Temporary Access Key 임시 키. AssumeRole이나 STS로 발급
AIDA IAM User ID IAM 사용자의 고유 식별 ID
AROA IAM Role ID IAM 역할의 고유 식별 ID

K8S RBAC 기초 실습

EKS 인증/인가를 이해하기 위한 기초로, K8S의 서비스 어카운트와 RBAC을 실습해본다.

서비스 어카운트와 네임스페이스 생성

# 네임스페이스 생성
kubectl create namespace dev-team
kubectl create namespace infra-team

# 각 네임스페이스에 서비스 어카운트 생성
kubectl create sa dev-k8s -n dev-team
kubectl create sa infra-k8s -n infra-team

SA를 지정한 파드 생성 후 권한 테스트

# dev-team 네임스페이스에 dev-k8s SA를 사용하는 파드 생성
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: dev-kubectl
  namespace: dev-team
spec:
  serviceAccountName: dev-k8s
  containers:
  - name: kubectl-pod
    image: bitnami/kubectl
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

# 단축 alias 설정
alias k1='kubectl exec -it dev-kubectl -n dev-team -- kubectl'

# 권한 테스트 - 전부 실패한다
k1 get pods         # Error: forbidden
k1 auth can-i get pods  # no

아직 Role과 RoleBinding이 없으므로 아무 것도 할 수 없다.

Role과 RoleBinding 생성

# dev-team 네임스페이스 내 모든 권한을 가진 Role 생성
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: role-dev-team
  namespace: dev-team
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
EOF

# RoleBinding으로 SA와 Role을 연결
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: roleB-dev-team
  namespace: dev-team
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: role-dev-team
subjects:
- kind: ServiceAccount
  name: dev-k8s
  namespace: dev-team
EOF

이제 다시 테스트하면 dev-team 네임스페이스 내에서는 동작하지만, 다른 네임스페이스나 클러스터 범위 리소스에는 접근할 수 없다.

k1 get pods                    # 성공
k1 run nginx --image nginx     # 성공
k1 get pods -n kube-system     # 실패 - 다른 네임스페이스
k1 get nodes                   # 실패 - 클러스터 범위 리소스

이것이 Role(네임스페이스 범위)과 ClusterRole(클러스터 범위)의 차이다.

RBAC에서 사용할 수 있는 verbs는 다음과 같다.

약어 Verb 설명
G get 단일 리소스 조회
L list 리소스 목록 조회
W watch 리소스 변경 감시
C create 리소스 생성
U update 리소스 전체 수정
P patch 리소스 일부 수정
D delete 단일 리소스 삭제
DC deletecollection 리소스 컬렉션 삭제

RBAC 확인용 krew 플러그인

# 설치
kubectl krew install access-matrix rbac-tool rolesum whoami

# 현재 인증된 주체 확인
kubectl whoami
# arn:aws:iam::<TRUNCATED>:user/admin

# 특정 그룹의 RBAC 정보 확인
kubectl rbac-tool lookup system:masters
#   SUBJECT        | SUBJECT TYPE | SCOPE       | NAMESPACE | ROLE
# +----------------+--------------+-------------+-----------+---------------+
#   system:masters | Group        | ClusterRole |           | cluster-admin

# SA의 권한 요약
kubectl rolesum -k Group pod-viewer
# Policies:
# [CRB] */viewer-role-binding -> [CR] */pod-viewer-role
#   Resource  Name  Exclude  Verbs  G L W C U P D DC
#   pods      [*]     [-]     [-]   O O O X X X X X

파드가 AWS 리소스를 사용하는 방법

파드 안에서 실행되는 애플리케이션이 S3, DynamoDB 같은 AWS 리소스를 사용하려면 AWS 자격증명이 필요하다. 여기서 크게 세 가지 선택지가 있다.

Node IAM Role (비권장)

EC2 Instance Profile에 필요한 권한을 모두 부여하는 방식이다. 가장 간단하지만 최소 권한 원칙에 위배된다. 해당 노드에서 실행되는 모든 파드가 동일한 권한을 갖게 된다.

테라폼으로 노드 그룹 설정 시 이런 식으로 IAM 정책을 추가한다.

eks_managed_node_groups = {
  primary = {
    name             = "${var.ClusterBaseName}-ng-1"
    instance_types   = ["${var.WorkerNodeInstanceType}"]
    # ...
    iam_role_additional_policies = {
      ExternalDNSPolicy            = aws_iam_policy.external_dns_policy.arn
      AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
    }
  }
}

문제는, 파드 A는 S3만 필요하고 파드 B는 DynamoDB만 필요한데, 둘 다 같은 노드에서 실행되면 양쪽 권한을 모두 가지게 된다는 것이다.

IRSA (IAM Roles for Service Accounts)

IRSA는 파드 단위로 IAM Role을 부여하는 방식이다. OIDC를 기반으로 동작한다.

비유하자면 이렇다. - K8S가 "이 파드는 my-sa 서비스 어카운트를 사용하고 있어요"라는 신분증(JWT 토큰)을 발급한다 - AWS STS가 "이 신분증이 진짜인지 K8S OIDC Provider에게 확인할게요"라고 검증한다 - 검증이 통과하면 "그러면 이 IAM Role의 임시 자격증명을 줄게요"라고 응답한다

동작 흐름을 순서대로 보면 다음과 같다.

파드 생성 시 MutatingWebhook이 Env/Volume 주입
-> 파드의 AWS SDK가 주입된 토큰과 IAM Role ARN으로 STS에 임시 키 요청
-> STS는 IAM을 통해 OIDC Provider의 공개키로 토큰 서명 검증
-> 검증 통과 시 임시 자격증명 반환
-> 파드가 임시 자격증명으로 AWS 리소스 접근

SA 없이 파드를 만들면

먼저 SA 없이 파드를 만들어보면 AWS 접근이 안 되는 걸 확인할 수 있다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-iam-test1
spec:
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      args: ['s3', 'ls']
  restartPolicy: Never
  automountServiceAccountToken: false
  terminationGracePeriodSeconds: 0
EOF

kubectl logs eks-iam-test1
# An error occurred (AccessDenied)

IRSA 설정 실습

이제 IRSA를 설정해보자.

# IAM Policy 생성 (AWS LBC용 예시)
curl -o aws_lb_controller_policy.json \
  https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/refs/heads/main/docs/install/iam_policy.json

aws iam create-policy \
  --policy-name AWSLoadBalancerControllerIAMPolicy \
  --policy-document file://aws_lb_controller_policy.json

# IRSA 생성 - IAM Role + K8S SA + Trust Policy가 한 번에 만들어진다
eksctl create iamserviceaccount \
  --cluster=myeks \
  --namespace=kube-system \
  --name=aws-load-balancer-controller \
  --attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy \
  --override-existing-serviceaccounts \
  --approve

# 확인
kubectl get sa -n kube-system aws-load-balancer-controller -o yaml
# annotations:
#   eks.amazonaws.com/role-arn: arn:aws:iam::<TRUNCATED>:role/eksctl-myeks-addon-...

생성된 IAM Role의 Trust Policy를 보면 이런 구조다.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::<TRUNCATED>:oidc-provider/oidc.eks.eu-west-2.amazonaws.com/id/<TRUNCATED>"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "oidc.eks.eu-west-2.amazonaws.com/id/<TRUNCATED>:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller",
        "oidc.eks.eu-west-2.amazonaws.com/id/<TRUNCATED>:aud": "sts.amazonaws.com"
      }
    }
  }]
}

sub 조건으로 특정 네임스페이스의 특정 SA만 이 Role을 Assume할 수 있도록 제한한다.

IRSA가 주입하는 것들

IRSA를 사용하는 파드의 spec을 보면 pod-identity-webhook(MutatingWebhook)이 다음을 자동 주입한다.

# 환경변수 주입
env:
  - name: AWS_ROLE_ARN
    value: "arn:aws:iam::<TRUNCATED>:role/eksctl-myeks-..."
  - name: AWS_WEB_IDENTITY_TOKEN_FILE
    value: "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"

# 볼륨 주입 - K8S 기본 토큰과는 별도의 두 번째 토큰
volumes:
  - name: aws-iam-token
    projected:
      sources:
        - serviceAccountToken:
            audience: sts.amazonaws.com    # K8S 기본 토큰은 audience가 kubernetes.default.svc
            expirationSeconds: 86400
            path: token

K8S 기본 SA 토큰과 IRSA 토큰의 차이를 비교하면 다음과 같다.

항목 K8S 기본 SA 토큰 IRSA 토큰
audience https://kubernetes.default.svc sts.amazonaws.com
경로 /var/run/secrets/kubernetes.io/serviceaccount/token /var/run/secrets/eks.amazonaws.com/serviceaccount/token
용도 K8S API 서버 인증 AWS STS 인증
만료 3607초 86400초 (24시간)

IRSA 토큰을 JWT 디코딩하면 이렇게 생겼다.

{
  "aud": ["sts.amazonaws.com"],
  "exp": 1775808011,
  "iat": 1775721611,
  "iss": "https://oidc.eks.eu-west-2.amazonaws.com/id/<TRUNCATED>",
  "kubernetes.io": {
    "namespace": "default",
    "pod": { "name": "eks-iam-test3" },
    "serviceaccount": { "name": "my-sa" }
  },
  "sub": "system:serviceaccount:default:my-sa"
}

AWS SDK는 환경변수 AWS_WEB_IDENTITY_TOKEN_FILEAWS_ROLE_ARN을 자동으로 읽어서 AssumeRoleWithWebIdentity를 호출한다. 그래서 코드에서 별도의 자격증명 설정이 필요 없다.

# IRSA 사용 시 Python 코드 - 키를 하드코딩할 필요가 없다
import boto3
s3 = boto3.client('s3')  # SDK가 알아서 토큰을 찾아 사용
response = s3.list_buckets()

CloudTrail에서 AssumeRoleWithWebIdentity 이벤트로 IRSA의 동작을 확인할 수 있다.

S3 읽기 전용 IRSA 예시

LBC 외에 S3 읽기 전용 권한을 가진 IRSA를 직접 만들어보자.

# IRSA 생성
eksctl create iamserviceaccount \
  --name my-sa \
  --namespace default \
  --cluster myeks \
  --approve \
  --role-name eksctl-myeks-pod-irsa-s3-readonly-role \
  --attach-policy-arn $(aws iam list-policies --query 'Policies[?PolicyName==`AmazonS3ReadOnlyAccess`].Arn' --output text)

# 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-iam-test3
spec:
  serviceAccountName: my-sa
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF

# 되는 것과 안되는 것
kubectl exec -it eks-iam-test3 -- aws s3 ls                                    # 성공 - S3ReadOnly 권한 있음
kubectl exec -it eks-iam-test3 -- aws ec2 describe-instances --region eu-west-2 # 실패 - EC2 권한 없음

EKS Pod Identity

Pod Identity는 IRSA의 후속 방식으로, 설정이 훨씬 간단하다.

IRSA와의 핵심 차이점은 다음과 같다. - OIDC Provider 설정이 필요 없다 - IAM Role의 Trust Policy가 단순하다 (클러스터별 OIDC URL 참조 대신 pods.eks.amazonaws.com 서비스만 지정) - 여러 클러스터에서 동일한 IAM Role을 재사용할 수 있다

Pod Identity 동작 방식

각 워커 노드에 DaemonSet으로 eks-pod-identity-agent가 실행된다. 이 에이전트가 Link-Local 주소(169.254.170.23:80)에서 자격증명을 제공한다.

# 노드에 SSM 접속 후 확인
sudo ss -tnlp | grep eks-pod-identit
# LISTEN 0 4096 169.254.170.23:80 0.0.0.0:*
# 즉, 169.254.170.23:80에서 자격증명 API를 제공한다

Pod Identity 설정 실습

# podidentityassociation 생성 - IAM Role + SA + 연결이 한 번에 된다
eksctl create podidentityassociation \
  --cluster myeks \
  --namespace default \
  --create-service-account \
  --service-account-name s3-sa \
  --role-name s3-eks-pod-identity-role \
  --permission-policy-arns arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
  --region eu-west-2

# 확인
eksctl get podidentityassociation --cluster myeks
aws eks list-pod-identity-associations --cluster-name myeks | jq

생성된 IAM Role의 Trust Policy가 IRSA에 비해 훨씬 깔끔하다.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Service": "pods.eks.amazonaws.com"
    },
    "Action": [
      "sts:AssumeRole",
      "sts:TagSession"
    ]
  }]
}

IRSA와 비교하면 클러스터별 OIDC URL이나 sub/aud 조건이 없다. 클러스터와 무관하게 pods.eks.amazonaws.com만 지정하면 된다. 그래서 새 클러스터를 만들 때마다 Trust Policy를 수정할 필요가 없다.

# 테스트 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-pod-identity
spec:
  serviceAccountName: s3-sa
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF

# 확인
kubectl exec -it eks-pod-identity -- aws sts get-caller-identity --query Arn
kubectl exec -it eks-pod-identity -- aws s3 ls

Pod Identity가 주입하는 것들

IRSA와 주입 방식이 다르다.

kubectl exec -it eks-pod-identity -- env | grep AWS
# AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE=/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token
# AWS_CONTAINER_CREDENTIALS_FULL_URI=http://169.254.170.23/v1/credentials

IRSA는 AWS_WEB_IDENTITY_TOKEN_FILEAWS_ROLE_ARN을 주입하고, Pod Identity는 AWS_CONTAINER_CREDENTIALS_FULL_URI를 주입한다.

SDK가 이 환경변수를 보고 169.254.170.23 (Pod Identity Agent)에서 자격증명을 가져오는 방식이다.

Pod Identity 토큰의 audience는 pods.eks.amazonaws.com이다.

{
  "aud": ["pods.eks.amazonaws.com"],
  "exp": 1775906063,
  "iat": 1775824814,
  "iss": "https://oidc.eks.eu-west-2.amazonaws.com/id/<TRUNCATED>",
  "kubernetes.io": {
    "serviceaccount": { "name": "s3-sa" }
  },
  "sub": "system:serviceaccount:default:s3-sa"
}

CloudTrail에서는 AssumeRoleForPodIdentity 이벤트로 확인할 수 있다 (IRSA는 AssumeRoleWithWebIdentity).

파드 내부에서 직접 자격증명 API를 호출해볼 수도 있다.

kubectl exec -it eks-pod-identity -- bash
# 파드 내부에서
EKS_POD_IDENTITY_TOKEN=$(cat $AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE)
curl -s 169.254.170.23/v1/credentials -H "Authorization: $EKS_POD_IDENTITY_TOKEN" | jq
# {
#   "AccessKeyId": "<TRUNCATED>",
#   "SecretAccessKey": "<TRUNCATED>",
#   "Token": "<TRUNCATED>",
#   "Expiration": "2026-04-07T16:14:05Z"
# }

IRSA vs Pod Identity 비교

항목 IRSA Pod Identity
인증 메커니즘 OIDC 기반 EKS Auth 서비스 (Pod Identity Agent)
OIDC Provider 필요 (클러스터별 1개) 불필요
Trust Policy 클러스터별 OIDC URL 포함 (복잡) pods.eks.amazonaws.com만 지정 (단순)
새 클러스터 추가 시 Trust Policy 수정 필요 수정 불필요, 동일 Role 재사용
토큰 audience sts.amazonaws.com pods.eks.amazonaws.com
CloudTrail 이벤트 AssumeRoleWithWebIdentity AssumeRoleForPodIdentity
자격증명 획득 방식 STS에 직접 요청 169.254.170.23 (Pod Identity Agent)
SDK 요구사항 OIDC Web Identity 지원 버전 최신 버전 필요
지원 환경 EKS, EKS Anywhere, 자체 관리 K8S EKS만
ABAC 지원 미지원 sts:TagSession으로 지원

선택 기준을 정리하면 다음과 같다.

  • 새 프로젝트이고 EKS만 사용한다면 -> Pod Identity
  • 여러 클러스터에서 동일한 Role을 재사용해야 한다면 -> Pod Identity
  • EKS Anywhere나 자체 관리 K8S도 사용한다면 -> IRSA
  • 기존에 IRSA로 잘 운영 중이라면 -> 급하게 바꿀 필요는 없다

system:masters 그룹에 대한 참고사항

K8S에서 system:masters 그룹은 인증만 통과하면 인가 검사를 완전히 우회하는 슈퍼유저 그룹이다. RBAC도, Webhook도 거치지 않는다.

EKS를 생성한 IAM 주체는 자동으로 이 그룹에 포함되지만, 어디에도 표시되지 않는다. 그래서 TokenReview 응답에서 groups에 system:authenticated만 보이는 것이 정상이다.

이 그룹의 관리 지침은 AWS Root 계정과 유사하다. - 일상 업무에서는 사용하지 말 것 - 부트스트랩 이후에는 사용을 최소화할 것 - 관리자 인증서가 탈취당하면 복구가 매우 어려움

정리

전체 흐름을 한 장으로 요약하면 다음과 같다.

[관리자/노드가 kubectl 실행]
-> aws eks get-token (SigV4 서명된 Pre-signed URL 생성)
-> Bearer Token으로 EKS API 호출
-> TokenReview (aws-iam-authenticator가 STS로 서명 검증)
-> 인가 (Access Entry + Webhook 또는 ConfigMap + RBAC)
-> K8S Action 실행

[파드가 AWS 리소스 접근]
IRSA: 파드 -> SA 토큰(aud:sts.amazonaws.com) -> STS AssumeRoleWithWebIdentity -> 임시 자격증명
Pod Identity: 파드 -> SA 토큰(aud:pods.eks.amazonaws.com) -> Pod Identity Agent -> EKS Auth -> 임시 자격증명

인증/인가 방식은 ConfigMap에서 Access Entry로, 파드의 AWS 접근은 Node IAM Role에서 IRSA를 거쳐 Pod Identity로 발전해왔다.

핵심 방향은 일관적이다: 더 세밀한 권한 제어, 더 적은 관리 부담, 더 안전한 자격증명 관리.