[CloudNeta] EKS 워크샵 스터디 (4) - EKS Security Part 1 - K8S 인증/인가 기초

이번 게시글에서는 EKS 워크샵 스터디 제 4주차 내용을 작성합니다.

이번 주에는 EKS의 Identity and Access Management에 대한 내용을 중점으로 다룹니다. 쿠버네티스의 인증/인가 기초부터 EKS에서 IAM Role을 Pod에 연결하는 방법까지 전체 흐름을 실습합니다.

이 글은 2부로 나누어집니다.

  1. 4주차 - EKS Security Part 1 - K8S 인증/인가 기초 (현재 보고계신 글)
  2. 4주차 - EKS Security Part 2 - EKS IAM 연동 (IRSA, Pod Identity)

환경구성 - 작업환경 구성하기

이번 주 코드도 마찬가지로 분석해봅시다.

코드 살펴보기

구동 확인하기

EKS Pod Identity Agent

# Pod Identity Agent가 DaemonSet으로 각 노드에 배포되었는지 확인
$ kubectl get ds,pod -n kube-system -l app.kubernetes.io/instance=eks-pod-identity-agent
NAME                                    DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/eks-pod-identity-agent   2         2         2       2            2           <none>          5m21s

NAME                               READY   STATUS    RESTARTS   AGE
pod/eks-pod-identity-agent-7x79s   1/1     Running   0          5m21s
pod/eks-pod-identity-agent-mlttd   1/1     Running   0          5m21s

ExternalDNS

# ExternalDNS의 실행 인자 확인 — policy=sync, provider=aws, txt-owner-id=myeks
$ kubectl describe deploy -n external-dns external-dns | grep Args: -A10
    Args:
      --log-level=info
      --log-format=text
      --interval=1m
      --source=service
      --source=ingress
      --policy=sync
      --registry=txt
      --txt-owner-id=myeks
      --provider=aws
    Limits:

cert-manager

# cert-manager 구성요소 전체 확인 (controller, cainjector, webhook 각 2 replica)
$ kubectl get all -n cert-manager
NAME                                           READY   STATUS    RESTARTS   AGE
pod/cert-manager-b5c55cbd4-54q5c               1/1     Running   0          5m10s
pod/cert-manager-b5c55cbd4-zdgqx               1/1     Running   0          5m10s
pod/cert-manager-cainjector-6fc9d9ddc7-88gcm   1/1     Running   0          5m10s
pod/cert-manager-cainjector-6fc9d9ddc7-t5vds   1/1     Running   0          5m10s
pod/cert-manager-webhook-cf55c69cf-84hfw       1/1     Running   0          5m10s
pod/cert-manager-webhook-cf55c69cf-vlblp       1/1     Running   0          5m10s

NAME                              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)            AGE
service/cert-manager              ClusterIP   10.100.208.54    <none>        9402/TCP           5m11s
service/cert-manager-cainjector   ClusterIP   10.100.145.159   <none>        9402/TCP           5m11s
service/cert-manager-webhook      ClusterIP   10.100.70.20     <none>        443/TCP,9402/TCP   5m11s

NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/cert-manager              2/2     2            2           5m10s
deployment.apps/cert-manager-cainjector   2/2     2            2           5m10s
deployment.apps/cert-manager-webhook      2/2     2            2           5m10s

NAME                                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/cert-manager-b5c55cbd4               2         2         2       5m10s
replicaset.apps/cert-manager-cainjector-6fc9d9ddc7   2         2         2       5m10s
replicaset.apps/cert-manager-webhook-cf55c69cf       2         2         2       5m10s

# cert-manager가 등록한 CRD 확인
$ kubectl get crd | grep cert-manager
certificaterequests.cert-manager.io             2026-04-12T11:47:38Z
certificates.cert-manager.io                    2026-04-12T11:47:38Z
challenges.acme.cert-manager.io                 2026-04-12T11:47:38Z
clusterissuers.cert-manager.io                  2026-04-12T11:47:39Z
issuers.cert-manager.io                         2026-04-12T11:47:39Z
orders.acme.cert-manager.io                     2026-04-12T11:47:38Z

# 노드 정보 가져오기
aws ssm describe-instance-information --query "InstanceInformationList[*].{InstanceId:InstanceId, Status:PingStatus, OS:PlatformName}" --output text            
i-<REDACTED-1>     Amazon Linux    Online
i-<REDACTED-2>     Amazon Linux    Online

필요 지식

K8S 인증/인가 기초 실습

쿠버네티스 API 요청은 크게 API 요청 해석 → 인증(Authentication) → 인가(Authorization) → Admission Control 순서로 처리됩니다. 이번 글에서는 EKS에서 kubectl get nodes를 실행했을 때, AWS IAM 주체가 어떻게 쿠버네티스의 사용자 정보로 변환되고 최종적으로 RBAC 인가를 통과하는지 살펴봅니다.

overview|695

인증-인가-Admission Control 을 거쳐 적절한 API 오브젝트를 호출하고, 그 결과를 오브젝트 스토리지에 저장하는 방식입니다.

그렇다면 이 명령어를 통해 어떻게 이루어지는지 살펴봅시다.

kubectl get nodes
  -> GET /api/v1/nodes
  -> 인증: 이 요청자는 누구인가?
  -> 인가: 이 주체가 nodes 리소스를 list할 수 있는가?
  -> 허용되면 node 목록 반환, 아니면 403 Forbidden

EKS에서는 이 흐름 중 인증 단계에서 AWS IAM bearer token과 인증 웹훅이 등장하고, 인가 단계에서 Access Entry, aws-auth ConfigMap, 쿠버네티스 RBAC이 연결됩니다.

인증방식 살펴보기

인증(Authentication)은 요청을 보낸 주체가 누구인지 확인하는 단계입니다.

쿠버네티스 API 요청은 인증 결과에 따라 보통 아래 중 하나의 주체로 취급됩니다.

구분 예시
일반 사용자 alice, arn:aws:iam::123456789012:user/admin
ServiceAccount system:serviceaccount:default:my-sa
익명 사용자 system:anonymous

인증이 성공하면 kube-apiserver는 요청에 아래와 같은 사용자 정보를 붙입니다.

필드 의미
username 요청자를 식별하는 이름
uid 요청자의 고유 식별자
groups 요청자가 속한 그룹 목록
extra 인증 플러그인이 추가한 부가 정보

중요한 점은, 인증은 권한을 부여하는 단계가 아니라는 것입니다. 인증은 "이 사람이 누구인지"를 확인할 뿐이고, 실제로 nodes를 조회할 수 있는지는 다음 단계인 인가에서 결정됩니다.

EKS에서는 이 인증 단계가 AWS IAM과 연결됩니다. 사용자가 kubectl을 실행하면 AWS CLI가 aws eks get-token으로 bearer token을 만들고, kube-apiserver는 이 token을 인증 웹훅으로 검증합니다. 검증이 성공하면 AWS IAM principal 정보가 쿠버네티스의 username, groups 같은 사용자 정보로 이어집니다.

인가방식 살펴보기

인가(Authorization)는 인증된 주체가 해당 API 동작을 수행할 수 있는지 판단하는 단계입니다.

쿠버네티스의 인가 모드에는 RBAC, Node, Webhook, ABAC 등이 있습니다. AlwaysAllow, AlwaysDeny 같은 테스트용 모드도 있지만 운영 설명에서는 보통 제외합니다.

EKS에서 사람이 주로 다루는 권한 제어는 RBAC과 EKS Access Entry/Access Policy입니다. 여기서 Access Entry는 쿠버네티스의 일반 Authorization mode라기보다, AWS IAM principal을 쿠버네티스 접근 주체나 EKS Access Policy에 연결하기 위한 EKS 전용 접근 관리 방식으로 보는 편이 정확합니다.

인가 판단을 이해하려면 먼저 쿠버네티스 API 요청이 어떤 속성으로 나뉘는지 보면 좋습니다. kubectl api-resources -o wide로 확인할 수 있는 항목들은 API group, version, resource 이름, namespaced 여부, kind, verb 같은 정보로 구성됩니다.

Kubernetes API Structure - API group, resource, namespace, API object의 관계

API 경로 기준으로 보면 core group은 /api/v1, named group은 /apis/{GROUP_NAME}/{VERSION} 아래에 위치합니다. 예를 들어 nodes는 core group의 cluster-scoped resource이고, deployments나 CRD는 named API group 아래의 resource입니다. 실제 YAML의 apiVersionkind는 이 API group/version과 object schema를 가리키며, GET, POST, DELETE, PATCH 같은 verb가 해당 resource에 대한 동작이 됩니다.

Kubernetes API - resource, API group/version, kind, verb 관계

인가 판단에는 앞에서 얻은 사용자 정보와 API 요청 속성이 함께 사용됩니다.

구분 예시
user arn:aws:iam::123456789012:user/admin
groups system:authenticated, eks-admins
verb list
resource nodes
apiGroup ""
namespace 없음. nodes는 cluster-scoped 리소스

따라서 kubectl get nodes의 인가 질문은 다음처럼 바꿔 말할 수 있습니다.

이 user 또는 group이 core API group의 cluster-scoped nodes 리소스에 대해 list 권한을 가지고 있는가?

RBAC을 사용하는 클러스터에서는 Role/ClusterRole과 RoleBinding/ClusterRoleBinding이 이 질문에 답합니다. 허용 규칙이 있으면 요청이 통과하고, 명시적으로 허용된 규칙이 없으면 거부됩니다.

실습에서는 아래 명령으로 현재 주체가 특정 동작을 수행할 수 있는지 확인할 수 있습니다.

kubectl auth can-i list nodes
kubectl auth can-i get pods -n default

Admission Controller에 대해

Admission Control은 인증과 인가가 끝난 뒤, API 요청을 최종적으로 받아들일지 판단하거나 요청 내용을 변경하는 단계입니다.

주로 create, update, patch, delete처럼 클러스터 상태를 바꾸는 요청에서 중요합니다. 예를 들어 Pod 생성 요청에 기본값을 채우거나, 특정 정책을 위반한 리소스 생성을 막을 수 있습니다.

이번 글의 중심 예시인 kubectl get nodes는 조회 요청이므로 Admission Control보다 인증과 인가 흐름이 핵심입니다.

참고 - RBAC 관련 krew 플러그인 모음집

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

주의

rbac-view는 ARM64 환경에서는 작동하지 않습니다!

관리자가 kubectl get node 실행 시의 과정 살펴보기

이제부터는 관리자가 kubectl get node를 실행할 때 어떤 식의 과정이 이루어지는지 살펴보겠습니다.

IAM과 EKS가 모두 AWS 전담인가?

아닙니다. 쿠버네티스의 영역은 별도로 있습니다만, IAM과 잘 연동하기 위한 구조를 가진 것 뿐입니다.

AWS의 IAM 자격주체가 쿠버네티스 플랫폼 사용 시 호환되게끔 작업을 하는 것이 필요한데요. 그 과정을 kube-apiserver가 처리하는 것입니다.

아래에서 어떤 과정을 거쳐 kubectl get nodes 를 쳤을 때 결과가 나오는지 살펴봅시다.

I. 토큰 생성

aws eks get-token --cluster-name <클러스터 이름> 으로 입력했던 그게 토큰생성 과정입니다.

이 토큰 수동으로 발급받으면 아래와 같습니다:

$ aws eks get-token --cluster-name <클러스터> | jq

# JWT 구조라, 간략히 쉘로 분해해서 봐봅시다.
#   알고리즘, 크리덴셜, STS, 날짜, 헤더, 시그니처가 잘 있네요.

AWS STS (Security Token Service)는 AWS 리소스에 대한 액세스를 제어할 수 있는 임시 보안 자격 증명을 생성하여 신뢰받는 사용자에게 제공합니다. AWS CLI 버전 1.16.156 이상에서는 별도 aws-iam-authenticator 설치 없이 aws eks get-token으로 사용 가능합니다.

여기서 만들어주는 SigV4(Signature Version 4)는 AWS API 요청에 인증 정보를 추가하기 위한 서명 프로토콜입니다. Secret Access Key를 직접 전송하지 않고, 대칭 서명을 통해 특정 날짜·리전·서비스로 범위가 지정된 키를 생성합니다. AWS SDK/CLI를 사용하면 이 서명 과정이 자동으로 처리되지만, EKS 인증 흐름을 이해하려면 내부 동작을 알아둘 필요가 있습니다.

토큰 생성 상세

HMAC-SHA256으로 해시기반 메시지 인증코드 연산으로 계층적인 구조를 만듭니다.

aws eks get-token이 내부적으로 수행하는 SigV4(Signature Version 4) 서명 및 토큰 변환 과정은 크게 4단계입니다.

flowchart TB
    subgraph Step1["1단계: 정규 요청 생성"]
        A1["HTTP 메서드: GET"] --> A2["엔드포인트: sts.ap-northeast-2.amazonaws.com"]
        A2 --> A3["쿼리 파라미터:\nAction=GetCallerIdentity\nVersion=2011-06-15"]
        A3 --> A4["필수 헤더:\nx-k8s-aws-id=클러스터명"]
        A4 --> A5["SHA256 해시\n→ Hashed Canonical Request"]
    end

    subgraph Step2["2단계: 서명할 문자열 생성"]
        B1["알고리즘: AWS4-HMAC-SHA256"]
        B2["타임스탬프: 20260401T120000Z"]
        B3["Credential Scope:\n20260401/ap-northeast-2/sts/aws4_request"]
        B1 --> B4["+ Hashed Canonical Request\n→ String to Sign"]
        B2 --> B4
        B3 --> B4
    end

    subgraph Step3["3단계: 서명 키 유도 및 서명"]
        direction LR
        C1["HMAC(AWS4+SecretKey, 날짜)\n→ DateKey"]
        C1 --> C2["HMAC(DateKey, 리전)\n→ RegionKey"]
        C2 --> C3["HMAC(RegionKey, sts)\n→ ServiceKey"]
        C3 --> C4["HMAC(ServiceKey, aws4_request)\n→ Signing Key"]
    end

    subgraph Step4["4단계: Pre-signed URL → 토큰 변환"]
        D1["Signing Key + String to Sign\n→ 최종 서명"]
        D1 --> D2["Pre-signed URL 조립\nsts.amazonaws.com/?Action=GetCallerIdentity\n&X-Amz-Signature=..."]
        D2 --> D3["Base64 인코딩"]
        D3 --> D4["k8s-aws-v1. 접두사 추가\n→ 최종 토큰"]
    end

    Step1 --> Step2 --> Step3 --> Step4

II. kubectl이 EKS의 kube-apiserver에 호출

kubectl의 Client-Go 라이브러리는 Pre-Signed URL을 Bearer Token으로 k8s action 과 함께 EKS API Cluster Endpoint로 요청을 보내줍니다. 이게 자동이니 쉬운데, 실제로 kubectl이 하는지 보려면 로그레벨을 매우 낮춰서 봐야합니다.

로그레벨 낮춰서 살펴보기

$ kubectl get node -v=10

# 1) kubeconfig 로드
I0412 20:59:39.536503  262307 loader.go:402] Config loaded from file:  /home/l4in/.kube/config

# 2) 캐시된 API 리소스 디스커버리 정보 로드 (대량 반복 — 축약)
I0412 20:59:39.537593  262307 cached_discovery.go:77] returning cached discovery info from .../.kube/cache/discovery/.../v1/serverresources.json
... (약 90줄 이상의 cached_discovery 로그)

# 3) 실제 API 요청 — Authorization 헤더가 마스킹되어 보이지 않음!
I0412 20:59:39.541641  262307 helper.go:113] "Request Body" body=""
I0412 20:59:39.541695  262307 round_trippers.go:527] "Request" curlCommand=<
        curl -v -XGET  -H "User-Agent: kubectl/v1.34.1 (linux/amd64) kubernetes/93248f9" -H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json" 'https://<EKS_API_SERVER>/api/v1/nodes?limit=500'
 >

# 4) DNS 해석 → TCP 연결 → 응답 수신
I0412 20:59:40.402308  262307 round_trippers.go:547] "HTTP Trace: DNS Lookup resolved" host="<EKS_API_SERVER>" address=[{"IP":"3.35.101.114","Zone":""},{"IP":"54.116.91.27","Zone":""}]
I0412 20:59:40.409715  262307 round_trippers.go:562] "HTTP Trace: Dial succeed" network="tcp" address="3.35.101.114:443"
I0412 20:59:40.449503  262307 round_trippers.go:632] "Response" verb="GET" url="https://<EKS_API_SERVER>/api/v1/nodes?limit=500" status="200 OK" headers=<
        Audit-Id: 3ba73e0a-0989-41c8-97f3-a607e5de5ffe
        Cache-Control: no-cache, private
        Content-Type: application/json
        ...
 > milliseconds=907 dnsLookupMilliseconds=222 dialMilliseconds=7 tlsHandshakeMilliseconds=8 serverProcessingMilliseconds=30

# 5) 최종 결과
NAME                                                STATUS   ROLES    AGE   VERSION
ip-192-168-13-117.ap-northeast-2.compute.internal   Ready    <none>   12m   v1.35.2-eks-f69f56f
ip-192-168-16-198.ap-northeast-2.compute.internal   Ready    <none>   12m   v1.35.2-eks-f69f56f
Authorization 헤더는 왜 안 보이나요?

kubectl의 round_trippers.go는 curl 커맨드를 재구성할 때 Authorization 헤더를 의도적으로 제외합니다. -v=10까지 올려도 Bearer 토큰은 로그에 노출되지 않습니다. 실제 토큰이 어떻게 전달되는지 보려면 직접 토큰을 꺼내서 curl로 호출해봐야 합니다.

실제로 토큰으로 k8s API 호출해보기

# 1. 토큰 확보 (15분 후 만료 — 이후 재발급 필요)
TOKEN_DATA=$(aws eks get-token --cluster-name myeks | jq -r '.status.token')
echo $TOKEN_DATA

# 2. curl -v로 호출 — Authorization: Bearer 헤더가 실제로 전달되는지 확인
curl -k -v -XGET \
  -H "Authorization: Bearer $TOKEN_DATA" \
  -H "Accept: application/json" \
  'https://<EKS_API_SERVER>/api/v1/nodes?limit=500'
# 3. jq로 깔끔하게 확인
curl -k -s -XGET \
  -H "Authorization: Bearer $TOKEN_DATA" \
  -H "Accept: application/json" \
  'https://<EKS_API_SERVER>/api/v1/nodes?limit=500' | jq

III. kube-apiserver가 Webhook Token Authentication으로 토큰을 검증하는 과정

웹훅을 aws-iam-authenticator 서버에 호출합니다. 이후 토큰 리뷰 결과를 받습니다.

쿠버네티스는 kube-apiserver에 대한 요청을 인증하기 위해 bearer tokens, X.509 인증서, OIDC 등 다양한 인증 방식을 지원합니다.

이 중에서 Amazon EKS는 현재 아래 세 가지를 기본으로 지원합니다:

EKS에서 핵심이 되는 것은 Webhook token authentication입니다. kubectl 명령을 실행하면 AWS CLI 또는 aws-iam-authenticator가 bearer token을 생성하고, 이 토큰이 kube-apiserver로 전달됩니다. kube-apiserver는 이 토큰을 인증 웹훅(aws-iam-authenticator 서버)으로 전달하고, 웹훅은 토큰 본문에 포함된 Pre-signed URL을 호출하여 서명을 검증합니다. 검증이 성공하면 사용자의 Account, ARN, UserId 등의 정보를 kube-apiserver로 반환합니다.

IV. 토큰 검증 - AuthN(인증) 완료

aws-iam-authenticator 서버는 토큰에 포함된 Pre-signed URL로 AWS STS GetCallerIdentity를 호출합니다. STS가 서명을 검증하고 사용자 정보(Account, ARN, UserId)를 반환하면, 여기까지가 AuthN(인증) 입니다. "이 사람이 누구인지"가 확인된 상태입니다.

V. IAM 보안주체 → K8S Subject 매핑 - AuthZ(인가) 시작

인증이 완료되면 "이 IAM 사용자/역할이 K8S에서 어떤 주체(Subject)에 해당하는가?"를 매핑해야 합니다. 이 매핑 결과를 바탕으로 K8S RBAC이 인가(AuthZ)를 수행합니다. 매핑 방식은 두 가지가 있습니다:

방안 1: EKS Access Entry (권장)

AWS EKS API를 통해 IAM 주체를 K8S Subject에 매핑합니다. AWS 생태계 안에서 관리되므로 별도의 K8S RBAC 리소스(ClusterRole/ClusterRoleBinding) 없이도 EKS 전용 Access Policy를 사용할 수 있습니다. - Docs

방안 2: aws-auth ConfigMap (deprecated)

aws-auth ConfigMap에 IAM User/Role ARN과 K8S Username/Group을 매핑합니다. 해당 IAM 주체가 확인되면 ConfigMap의 매핑 정보를 조회하고, 매핑된 K8S Subject에 대해 RBAC 인가를 수행합니다. - Docs

aws-auth ConfigMap의 문제점

  • deprecated 상태입니다. 신규 프로젝트라면 사용할 이유가 없습니다.
  • K8S Subject에 매칭되는 ClusterRole/ClusterRoleBinding을 직접 생성·관리해야 합니다 → 관리 리소스 증가, 휴먼 에러 발생 가능
  • ConfigMap을 잘못 수정하면 클러스터 접근 자체가 잠길 수 있습니다
  • 정책이 중복 설정된 경우 EKS API(Access Entry)가 우선되며 ConfigMap은 무시됩니다
  • 미리 생성된 K8S group 등 subject가 없을 경우 (Cluster)Role/(Cluster)RoleBinding을 직접 생성해야 합니다

EKS를 생성한 IAM principal

EKS 클러스터를 생성한 IAM principal은 aws-auth와 관계없이 kubernetes-admin Username으로 system:masters 그룹 권한을 갖습니다.

두 방식 비교

구분 aws-auth ConfigMap 방식 Access Entry (EKS API) 방식
인증 요청 TokenReview (동일) TokenReview (동일)
데이터 저장소 K8s 내부의 ConfigMap (etcd) AWS EKS 관리형 API (Internal DB)
관리 주체 사용자가 YAML로 직접 관리 AWS API/콘솔에서 직접 관리
권한 부여(인가) RBAC RoleBinding 직접 생성 필요 Access Policy (EKS 전용 정책) 사용 가능
장애 위험 ConfigMap 오편집 시 접근 잠김 AWS API 기반으로 안전
상태 deprecated 권장

EKS를 운영한다면 EKS API(Access Entry)를 사용하여 AWS 생태계 안에서 관리하는 것이 편의성과 안전성 모두 우수합니다.

VI. 쿠버네티스/EKS authorizer의 AuthZ 판단과 최종 응답

인증이 끝나면 kube-apiserver는 요청을 인가합니다. kubectl get nodes는 쿠버네티스 관점에서 core API group의 cluster-scoped nodes 리소스에 대한 list 요청입니다.

EKS에서는 이 인가 판단이 두 경로로 이뤄질 수 있습니다. 첫째, Access Entry 또는 aws-auth ConfigMap이 IAM principal을 쿠버네티스 group으로 매핑하고, 쿠버네티스 RBAC authorizer가 해당 group에 연결된 RoleBinding/ClusterRoleBinding을 보고 허용할 수 있습니다. 둘째, Access Entry에 EKS Access Policy가 연결되어 있으면 Amazon EKS authorizer가 그 정책을 보고 허용할 수 있습니다.

즉 Access Entry는 단순히 "IAM ARN을 쿠버네티스 subject로 매핑"하는 데서 끝나지 않고, 선택적으로 EKS Access Policy를 통해 AWS 관리형 쿠버네티스 권한을 부여하는 진입점이기도 합니다. 다만 세밀한 커스텀 권한이 필요하면 Access Entry에는 쿠버네티스 group만 매핑하고, 실제 권한은 쿠버네티스 RBAC으로 관리하는 편이 적합합니다.

최종적으로 어떤 authorizer도 요청을 허용하지 않으면 kube-apiserver는 403 Forbidden을 반환합니다. 허용되면 nodes 리소스를 조회해 kubectl에 응답합니다.

항목 일반 쿠버네티스 Amazon EKS
API 요청 대상 kube-apiserver kube-apiserver. 단, 인증/인가 일부가 AWS 관리 영역과 연동
인증 방식 X.509, bearer token, ServiceAccount token, OIDC/JWT, webhook 등 구성에 따라 다름 주로 AWS CLI/aws-iam-authenticator가 만든 bearer token을 EKS 인증 웹훅이 검증. ServiceAccount/OIDC도 사용 가능
인가 모드 RBAC, Node, Webhook, ABAC 등 구성 가능 EKS 관리형 컨트롤 플레인에서 Node/RBAC과 Amazon EKS authorizer를 사용해 Access Policy 기반 인가 지원
IAM principal 매핑 쿠버네티스 자체 개념 아님 Access Entry 또는 deprecated aws-auth ConfigMap으로 IAM principal을 쿠버네티스 username/group에 연결
관리형 권한 정책 기본 user-facing ClusterRole(admin, edit, view 등)은 있으나 직접 Binding 필요 AmazonEKSClusterAdminPolicy, AmazonEKSAdminPolicy, AmazonEKSEditPolicy, AmazonEKSViewPolicy 등 Access Policy 연결 가능
커스텀 권한 Role/ClusterRole + RoleBinding/ClusterRoleBinding Access Entry에 쿠버네티스 group을 매핑한 뒤 쿠버네티스 RBAC으로 구현

참고사항: 관리자 권한의 인가우회

systems:masters 그룹에 속한 사용자는 인증 후 별도 인가없이 바로 수행합니다.

EC2에서 Node IAM Role(EC2 instance profile)을 사용하여 k8s API를 호출할 때의 과정 살펴보기

EC2에 붙은 Node IAM Role(instance profile) 자격증명으로 EKS 쿠버네티스 API를 호출하는 흐름을 확인하는 실습입니다. 사용자가 명시적으로 sts assume-role을 호출하는 예제가 아니라, EC2 메타데이터 서비스(IMDS)를 통해 제공되는 instance profile 임시 자격증명을 AWS CLI가 사용합니다.

aws eks get-token으로 생성된 token은 EKS 인증 웹훅에서 IAM role로 검증되고, 워커 노드 역할은 쿠버네티스의 system:node:<nodeName>system:nodes 주체로 매핑됩니다. 이후 자기 Node 객체 조회 같은 kubelet 성격의 요청은 Node authorizer가 제한적으로 허용할 수 있습니다. NodeRestriction은 authorizer가 아니라 Admission Controller로, kubelet이 자기 Node/Pod 범위를 넘어 수정하지 못하게 제한합니다.

pre-requisites

임시 자격증명 요청 및 사용에 대해

AWS에서 IAM Role은 장기 Access Key를 직접 들고 다니는 방식이 아니라, STS가 발급한 임시 자격증명으로 사용됩니다.

실제 AWS API 요청 서명에 사용하는 임시 자격증명 값은 아래 세 가지입니다.

항목 의미
AccessKeyId 임시 Access Key. 보통 ASIA로 시작합니다.
SecretAccessKey 임시 Secret Key
SessionToken 임시 세션을 증명하는 토큰

STS AssumeRole 응답에는 여기에 더해 Expiration이 함께 내려옵니다. Expiration은 요청 서명에 직접 넣는 자격증명 값이 아니라, 해당 임시 자격증명이 언제 만료되는지를 알려주는 메타데이터입니다.

assume-role-101

access entry 정보 확인

# 관리형 노드 그룹으로 배포된 EC2의 IAM Role을 확인합니다
$ aws eks list-access-entries --cluster-name myeks | jq
{
  "accessEntries": [
    "arn:aws:iam::<ACCOUNT_ID>:role/aws-service-role/eks.amazonaws.com/AWSServiceRoleForAmazonEKS",
    "arn:aws:iam::<ACCOUNT_ID>:role/myeks-ng-1",
    "arn:aws:iam::<ACCOUNT_ID>:user/<USER_NAME>"
  ]
}

# 해당 EC2 는 k8s group 으로 system:nodes 에 매핑되어 있습니다
$ aws eks describe-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID\:role/myeks-ng-1
{
    "accessEntry": {
        "clusterName": "myeks",
        "principalArn": "arn:aws:iam::<ACCOUNT_ID>:role/myeks-ng-1",
        "kubernetesGroups": [
            "system:nodes"
        ],
        "accessEntryArn": "arn:aws:eks:ap-northeast-2:<ACCOUNT_ID>:access-entry/myeks/role/<ACCOUNT_ID>/myeks-ng-1/<ACCESS_ENTRY_ID>",
        "createdAt": "2026-04-12T20:46:12.811000+09:00",
        "modifiedAt": "2026-04-12T20:46:12.811000+09:00",
        "tags": {},
        "username": "system:node:{{EC2PrivateDNSName}}",
        "type": "EC2_LINUX"
    }
}

Node IAM Role 자격증명으로 EKS API 호출 확인

워커 노드 EC2에 접속한 뒤 현재 AWS CLI가 어떤 IAM 주체로 동작하는지 확인합니다.

$ aws ssm start-session --target $NODE1
$ sudo su -

# EC2 instance profile로 연결된 Node IAM Role의 임시 자격증명을 사용 중입니다.
$ aws sts get-caller-identity --query Arn
"arn:aws:sts::<ACCOUNT_ID>:assumed-role/myeks-ng-1/<INSTANCE_ID>"

이 ARN에서 assumed-role/myeks-ng-1/i-... 형태를 확인할 수 있습니다. myeks-ng-1은 Node IAM Role이고, i-...는 EC2 인스턴스 세션 이름입니다. 즉 사용자가 직접 aws sts assume-role을 실행한 것이 아니라, EC2 instance profile을 통해 제공된 임시 자격증명을 AWS CLI가 사용하고 있습니다.

AWS IAM 권한은 여전히 Node IAM Role에 붙은 정책의 영향을 받습니다. 예를 들어 S3 권한이 없으면 아래처럼 거부될 수 있습니다.

$ aws s3 ls
An error occurred (AccessDenied) when calling the ListBuckets operation: User: arn:aws:sts::<ACCOUNT_ID>:assumed-role/myeks-ng-1/<INSTANCE_ID> is not authorized to perform: s3:ListAllMyBuckets

이제 같은 자격증명으로 kubeconfig를 만들고 EKS용 bearer token을 발급합니다.

$ aws eks --region ap-northeast-2 update-kubeconfig --name myeks
Added new context arn:aws:eks:ap-northeast-2:<ACCOUNT_ID>:cluster/myeks to /root/.kube/config

$ export CLUSTER_NAME=myeks
$ aws eks get-token --cluster-name $CLUSTER_NAME | jq
{
  "kind": "ExecCredential",
  "apiVersion": "client.authentication.k8s.io/v1beta1",
  "status": {
    "expirationTimestamp": "2026-04-12T13:36:30Z",
    "token": "k8s-aws-v1.<REDACTED>"
  }
}

--debug 로그에서 볼 핵심은 아래 정도입니다.

IMDS ENDPOINT: http://169.254.169.254/
GET /latest/meta-data/iam/security-credentials/
GET /latest/meta-data/iam/security-credentials/myeks-ng-1
Found credentials from IAM Role: myeks-ng-1
Event before-sign.sts.GetCallerIdentity: ... _inject_k8s_aws_id_header
Calculating signature using v4 auth.

정리하면, 워커 노드 EC2에서 aws eks get-token을 실행하면 AWS CLI는 IMDS에서 myeks-ng-1 Role의 임시 자격증명을 가져옵니다. 이후 STS GetCallerIdentity 요청을 SigV4로 서명하고, x-k8s-aws-id: myeks 헤더가 포함된 presigned request를 k8s-aws-v1. 토큰으로 변환합니다. kube-apiserver는 이 토큰을 EKS 인증 웹훅으로 검증하고, Access Entry의 username: system:node:{{EC2PrivateDNSName}}, kubernetesGroups: ["system:nodes"] 매핑을 통해 쿠버네티스 주체로 인식합니다.

II. 클라이언트가 k8s(EKS) API 서버 엔드포인트에 액션 요청

일반적인 쿠버네티스 API를 요청하고, 이때 Bearer 토큰을 포함하여 요청하는 것을 확인해봅시다.

# kubectl 설치
# https://docs.aws.amazon.com/eks/latest/userguide/install-kubectl.html
curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.35.2/2026-02-27/bin/linux/amd64/kubectl
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
kubectl version

# 실습 환경 변수
export AWS_REGION=ap-northeast-2
export CLUSTER_NAME=myeks
export NODE_NAME=$(hostname -f)
export API_SERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')

# 현재 노드 이름 예시:
# ip-192-168-13-117.ap-northeast-2.compute.internal
echo $NODE_NAME

# kubectl은 kubeconfig의 exec 설정에 따라 내부적으로 aws eks get-token을 실행하고,
# 발급된 Bearer token으로 EKS Kubernetes API 서버에 요청합니다.
kubectl get node "$NODE_NAME" -v=10

# 참고: 자기 Node 객체 조회는 Node authorizer 설명에 잘 맞습니다.
# NodeRestriction은 authorizer가 아니라 Admission Controller입니다.
# kubelet이 자기 Node/Pod 범위를 넘어 수정하지 못하게 제한하는 역할에 가깝습니다.

# 전체 노드 목록이나 전체 Pod 목록 조회는 별도 RBAC 또는 EKS Access Policy 권한이 필요할 수 있습니다.
kubectl get node
kubectl get pod -A

# kubectl 대신 직접 token을 사용해서 Kubernetes API를 호출해봅니다.
TOKEN_DATA=$(aws eks get-token --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" | jq -r '.status.token')
echo "Bearer token: k8s-aws-v1.<REDACTED>"

# 실습 편의상 -k를 사용합니다. 운영 환경에서는 kubeconfig의 CA를 사용해 검증하는 편이 좋습니다.
curl -k -v -XGET \
  -H "Authorization: Bearer $TOKEN_DATA" \
  -H "Accept: application/json" \
  "$API_SERVER/api/v1/nodes/$NODE_NAME"

curl -k -s -XGET \
  -H "Authorization: Bearer $TOKEN_DATA" \
  -H "Accept: application/json" \
  "$API_SERVER/api/v1/nodes/$NODE_NAME" | jq

결과에서 볼 부분만 추리면 아래와 같습니다.

$ kubectl get node "$NODE_NAME" -v=10
...
I0412 13:32:17.859638   38015 round_trippers.go:527] "Request" curlCommand=<
        curl -v -XGET ... 'https://<EKS_API_SERVER>/api/v1/nodes/ip-192-168-13-117.ap-northeast-2.compute.internal'
 >
I0412 13:32:18.955330   38015 round_trippers.go:632] "Response" verb="GET" url="https://<EKS_API_SERVER>/api/v1/nodes/ip-192-168-13-117.ap-northeast-2.compute.internal" status="200 OK"
...
NAME                                                STATUS   ROLES    AGE    VERSION
ip-192-168-13-117.ap-northeast-2.compute.internal   Ready    <none>   105m   v1.35.2-eks-f69f56f

현재 주체는 system:node:ip-192-168-13-117.ap-northeast-2.compute.internal로 매핑된 노드 주체입니다. 따라서 자기 Node 객체는 조회할 수 있지만, 전체 Node 목록이나 전체 Pod 목록은 조회할 수 없습니다.

$ kubectl get node
Error from server (Forbidden): nodes is forbidden: User "system:node:ip-192-168-13-117.ap-northeast-2.compute.internal" cannot list resource "nodes" in API group "" at the cluster scope: node 'ip-192-168-13-117.ap-northeast-2.compute.internal' cannot read all nodes, only its own Node object

$ kubectl get pod -A
Error from server (Forbidden): pods is forbidden: User "system:node:ip-192-168-13-117.ap-northeast-2.compute.internal" cannot list resource "pods" in API group "" at the cluster scope: can only list/watch pods with spec.nodeName field selector

같은 요청을 kubectl 대신 직접 발급한 bearer token으로 호출해도 자기 Node 객체는 조회됩니다. 실제 token은 출력하지 않습니다.

$ TOKEN_DATA=$(aws eks get-token --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" | jq -r '.status.token')

$ curl -k -v -XGET \
  -H "Authorization: Bearer $TOKEN_DATA" \
  -H "Accept: application/json" \
  "$API_SERVER/api/v1/nodes/$NODE_NAME"
...
> GET /api/v1/nodes/ip-192-168-13-117.ap-northeast-2.compute.internal HTTP/2
> Authorization: Bearer k8s-aws-v1.<REDACTED>
< HTTP/2 200
< content-type: application/json
...
{
  "kind": "Node",
  "apiVersion": "v1",
  "metadata": {
    "name": "ip-192-168-13-117.ap-northeast-2.compute.internal"
  },
  "spec": {
    "providerID": "aws:///ap-northeast-2a/<INSTANCE_ID>"
  },
  "status": {
    "addresses": [
      {
        "type": "InternalIP",
        "address": "192.168.13.117"
      }
    ],
    "nodeInfo": {
      "osImage": "Amazon Linux 2023.10.20260330",
      "kubeletVersion": "v1.35.2-eks-f69f56f"
    }
  }
}

즉 이 실습에서 확인되는 권한은 “Node IAM Role로 클러스터 전체를 조회할 수 있다”가 아니라, EKS가 Node IAM Role을 system:node:<nodeName> 주체로 인증하고, Node authorizer가 자기 Node 객체 조회만 제한적으로 허용한다는 점입니다.

III. TokenReview로 인증 결과 확인

위에서 만든 EKS bearer token이 쿠버네티스에서 어떤 주체로 인증되는지 TokenReview로 확인할 수 있습니다.

먼저 워커 노드 EC2에서 token을 발급받아 메모합니다. 실제 글에는 token 원문을 남기지 않습니다.

# 워커 노드 EC2에서 실행
TOKEN_DATA=$(aws eks get-token --cluster-name myeks | jq -r '.status.token')
echo "$TOKEN_DATA"
# k8s-aws-v1.<REDACTED>

이후 관리자 권한이 있는 로컬 PC에서 TokenReview 요청을 만듭니다. TokenReview는 인증 API이므로 요청을 생성하는 현재 사용자에게 tokenreviews.authentication.k8s.io 생성 권한이 필요합니다.

TOKEN_DATA=<REDACTED>

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

-v=9 로그에서는 TokenReview 요청이 아래 API로 전달되는 것을 확인할 수 있습니다.

POST /apis/authentication.k8s.io/v1/tokenreviews?fieldManager=kubectl-create&fieldValidation=Strict
status="201 Created"

응답 본문에서 중요한 부분은 status.authenticatedstatus.user입니다.

{
  "kind": "TokenReview",
  "apiVersion": "authentication.k8s.io/v1",
  "status": {
    "authenticated": true,
    "user": {
      "username": "system:node:<NODE_NAME>",
      "uid": "aws-iam-authenticator:<ACCOUNT_ID>:<ROLE_PRINCIPAL_ID>",
      "groups": [
        "system:nodes",
        "system:authenticated"
      ],
      "extra": {
        "accessKeyId": [
          "ASIA<REDACTED>"
        ],
        "arn": [
          "arn:aws:sts::<ACCOUNT_ID>:assumed-role/myeks-ng-1/<INSTANCE_ID>"
        ],
        "canonicalArn": [
          "arn:aws:iam::<ACCOUNT_ID>:role/myeks-ng-1"
        ],
        "principalId": [
          "<ROLE_PRINCIPAL_ID>"
        ],
        "sessionName": [
          "<INSTANCE_ID>"
        ]
      }
    }
  }
}

여기서 확인할 점은 세 가지입니다.

extra.arn에는 STS assumed-role ARN이 보이고, extra.canonicalArn에는 원본 IAM Role ARN이 보입니다. EKS Access Entry는 이 원본 IAM Role ARN인 arn:aws:iam::<ACCOUNT_ID>:role/myeks-ng-1을 기준으로 매핑됩니다.

IV. Server-side 검증 흐름 정리

TokenReview 결과는 EKS 인증 웹훅이 token을 검증한 뒤 kube-apiserver에 돌려준 인증 결과입니다. 흐름을 다시 정리하면 다음과 같습니다.

  1. 클라이언트는 aws eks get-token으로 k8s-aws-v1. bearer token을 만듭니다.
  2. 이 token 안에는 STS GetCallerIdentity 요청을 위한 presigned URL이 들어 있습니다.
  3. kube-apiserver는 bearer token을 직접 해석해 권한을 주는 것이 아니라, Webhook Token Authentication을 통해 EKS의 인증 웹훅으로 TokenReview를 요청합니다.
  4. server-side의 aws-iam-authenticator는 token에 들어 있는 presigned URL을 AWS STS GetCallerIdentity에 제출합니다.
  5. STS가 서명을 검증하고 caller identity를 반환하면, 인증 웹훅은 이를 쿠버네티스 user info로 변환해 kube-apiserver에 반환합니다.

이번 실습의 반환 결과는 아래처럼 요약됩니다.

즉 이 단계까지가 AuthN입니다. 아직 “무엇을 할 수 있는가”는 결정되지 않았고, 이후 nodes/<nodeName> 조회를 허용할지는 Node authorizer/RBAC/EKS authorizer 같은 AuthZ 단계에서 판단합니다.

CloudTrail에서도 같은 흐름을 확인할 수 있습니다. EKS 인증 웹훅이 token 안의 presigned URL을 STS에 제출하면, STS GetCallerIdentity 관리 이벤트가 남습니다.

{
  "eventVersion": "1.11",
  "userIdentity": {
    "type": "AssumedRole",
    "principalId": "<ROLE_PRINCIPAL_ID>:<INSTANCE_ID>",
    "arn": "arn:aws:sts::<ACCOUNT_ID>:assumed-role/myeks-ng-1/<INSTANCE_ID>",
    "accountId": "<ACCOUNT_ID>",
    "accessKeyId": "ASIA<REDACTED>",
    "sessionContext": {
      "sessionIssuer": {
        "type": "Role",
        "principalId": "<ROLE_PRINCIPAL_ID>",
        "arn": "arn:aws:iam::<ACCOUNT_ID>:role/myeks-ng-1",
        "accountId": "<ACCOUNT_ID>",
        "userName": "myeks-ng-1"
      },
      "attributes": {
        "creationDate": "2026-04-12T13:22:38Z",
        "mfaAuthenticated": "false"
      },
      "ec2RoleDelivery": "2.0"
    },
    "inScopeOf": {
      "issuerType": "AWS::EC2::Instance",
      "credentialsIssuedTo": "arn:aws:ec2:ap-northeast-2:<ACCOUNT_ID>:instance/<INSTANCE_ID>"
    }
  },
  "eventTime": "2026-04-12T13:37:34Z",
  "eventSource": "sts.amazonaws.com",
  "eventName": "GetCallerIdentity",
  "awsRegion": "ap-northeast-2",
  "sourceIPAddress": "<EKS_AUTHENTICATOR_SOURCE_IP>",
  "userAgent": "Go-http-client/1.1",
  "requestParameters": null,
  "responseElements": null,
  "readOnly": true,
  "eventType": "AwsApiCall",
  "managementEvent": true,
  "recipientAccountId": "<ACCOUNT_ID>",
  "eventCategory": "Management",
  "tlsDetails": {
    "tlsVersion": "TLSv1.3",
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "sts.ap-northeast-2.amazonaws.com"
  }
}

여기서 userIdentity.arn은 STS 세션 ARN이고, sessionContext.sessionIssuer.arn은 Access Entry에 등록된 원본 Node IAM Role ARN입니다. inScopeOf.credentialsIssuedTo를 보면 이 임시 자격증명이 어떤 EC2 인스턴스에 발급된 것인지도 확인할 수 있습니다.

V. Access Entry 매핑과 AuthZ 경로 확인

앞에서 TokenReview로 확인한 것처럼, Node IAM Role은 EKS 인증 웹훅을 거쳐 쿠버네티스의 노드 주체로 변환됩니다.

arn:aws:iam::<ACCOUNT_ID>:role/myeks-ng-1
  -> system:node:<NODE_NAME>
  -> groups: system:nodes, system:authenticated

이때 Access Entry가 하는 일은 IAM Role을 쿠버네티스에서 사용할 username/group으로 연결하는 것입니다. 이후 “무엇을 할 수 있는가”는 별도의 AuthZ 단계에서 판단됩니다.

EKS Access Entry에서 권한을 붙이는 방법은 크게 두 갈래입니다.

방식 의미
Access Policy 연결 EKS 관리형 Access Policy를 Amazon EKS authorizer가 판단
쿠버네티스 group 매핑 매핑된 group을 기준으로 쿠버네티스 RBAC 또는 Node authorizer가 판단

이번 워커 노드 실습은 Access Policy 방식이 아니라, system:nodes group과 system:node:<nodeName> username을 사용하는 노드 주체 흐름입니다.

먼저 워커 노드에서는 현재 kubeconfig가 어떤 쿠버네티스 주체로 인증되는지만 확인합니다. 여기서는 rbac-tool을 독립 실행 파일로 설치해서 whoami만 실행합니다.

# 워커 노드 EC2에서 실행
aws ssm start-session --target $NODE1
sudo su -

# rbac-tool 설치
# https://github.com/alcideio/rbac-tool
curl https://raw.githubusercontent.com/alcideio/rbac-tool/master/download.sh | bash
rbac-tool -h

# 현재 context의 인증 주체 확인
rbac-tool whoami

출력 예시는 아래처럼 정리할 수 있습니다. 실제 계정 ID, Access Key, principal ID, instance ID는 마스킹합니다.

{
  Username: "system:node:<NODE_NAME>",
  UID:      "aws-iam-authenticator:<ACCOUNT_ID>:<ROLE_PRINCIPAL_ID>",
  Groups:   [
    "system:nodes",
    "system:authenticated"
  ],
  Extra: {
    accessKeyId:  ["ASIA<REDACTED>"],
    arn:          ["arn:aws:sts::<ACCOUNT_ID>:assumed-role/myeks-ng-1/<INSTANCE_ID>"],
    canonicalArn: ["arn:aws:iam::<ACCOUNT_ID>:role/myeks-ng-1"],
    principalId:  ["<ROLE_PRINCIPAL_ID>"],
    sessionName:  ["<INSTANCE_ID>"]
  }
}

이 결과는 앞의 TokenReview 결과와 같은 의미입니다. EKS는 STS assumed-role ARN을 확인한 뒤, canonical IAM Role ARN을 기준으로 Access Entry를 찾고, 이를 쿠버네티스 username/group으로 변환합니다.

반면 워커 노드의 Node IAM Role context에서 RBAC 전역 조회를 시도하면 실패합니다.

$ kubectl rbac-tool lookup system:nodes
error: unknown command "rbac-tool" for "kubectl"

$ rbac-tool lookup system:nodes
Error: serviceaccounts is forbidden: User "system:node:<NODE_NAME>" cannot list resource "serviceaccounts" in API group "" at the cluster scope: can only create tokens for individual service accounts

$ rolesum
-bash: rolesum: command not found

첫 번째 오류는 워커 노드에서 설치한 rbac-toolkubectl 플러그인이 아니라 독립 실행 파일이기 때문입니다. 두 번째 오류는 더 중요합니다. 현재 context가 노드 주체이므로, 클러스터 전체의 ServiceAccount/RBAC 리소스를 list할 권한이 없습니다.

따라서 실행 위치를 나눕니다.

실행 위치 목적 명령
워커 노드 EC2 현재 노드가 어떤 쿠버네티스 주체로 인증되는지 확인 rbac-tool whoami
관리자 PC system:nodes group에 연결된 RBAC 조회 kubectl rbac-tool lookup system:nodes, kubectl rolesum -k Group system:nodes

관리자 PC에는 krew 플러그인으로 rbac-toolrolesum을 설치한 뒤 조회합니다.

# 관리자 PC에서 실행
kubectl krew install rbac-tool rolesum

kubectl rbac-tool lookup system:nodes
  SUBJECT      | SUBJECT TYPE | SCOPE       | NAMESPACE | ROLE                  | BINDING
---------------+--------------+-------------+-----------+-----------------------+------------------------
  system:nodes | Group        | ClusterRole |           | eks:node-bootstrapper | eks:node-bootstrapper

kubectl rolesum -k Group system:nodes
Group: system:nodes

Policies:
 [CRB] */eks:node-bootstrapper ⟶  [CR] */eks:node-bootstrapper
  Resource                                                       Name  Exclude  Verbs  G L W C U P D DC
  certificatesigningrequests.certificates.k8s.io/selfnodeclient  [*]     [-]     [-]   ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖
  certificatesigningrequests.certificates.k8s.io/selfnodeserver  [*]     [-]     [-]   ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖

여기서 주의할 점은 eks:node-bootstrapper가 노드의 모든 API 접근 권한을 설명하지 않는다는 것입니다. 앞에서 확인한 것처럼 Node IAM Role로는 전체 노드 목록을 조회할 수 없습니다.

$ kubectl get node
Error from server (Forbidden): nodes is forbidden: User "system:node:<NODE_NAME>" cannot list resource "nodes" in API group "" at the cluster scope: node '<NODE_NAME>' cannot read all nodes, only its own Node object

반면 자기 Node 객체는 조회할 수 있습니다.

$ kubectl get node "$NODE_NAME"
NAME          STATUS   ROLES    AGE    VERSION
<NODE_NAME>   Ready    <none>   105m   v1.35.2-eks-f69f56f

즉 이번 실습의 AuthZ 흐름은 아래처럼 정리할 수 있습니다.

Access Entry
  -> IAM Role을 system:node:<NODE_NAME>, system:nodes로 매핑

AuthZ
  -> 자기 Node 객체 조회: Node authorizer가 제한적으로 허용
  -> 전체 nodes list: 거부
  -> 전체 pods list: 거부
  -> CSR bootstrap 관련 권한: system:nodes -> eks:node-bootstrapper RBAC에서 확인 가능

EKS Access Policy를 붙인 관리자/개발자 IAM Role 시나리오라면 Amazon EKS authorizer와 SubjectAccessReview 기반 Webhook 인가 설명을 이어서 보면 됩니다. 하지만 이번 Node IAM Role 실습은 “Access Policy로 권한을 받은 사용자”가 아니라 “노드 주체가 자기 자신에 대해 제한된 권한을 갖는 흐름”입니다.

참고:

별도 AWS CLI 환경에서, 신규 관리자를 위한 EKS 자격증명 설정

이번에는 워커 노드의 Node IAM Role이 아니라, 별도 IAM 사용자를 만들어 EKS 접근 권한을 부여하는 흐름을 확인합니다. 먼저 AWS IAM 사용자와 Access Key를 만들고, 이후 EKS Access Entry를 통해 쿠버네티스 API 접근 권한을 연결합니다.

사전준비 1. 테스트 계정 생성

실습용 계정

아래 예시는 실습 편의를 위해 AdministratorAccess를 사용합니다. 운영 환경에서는 사용자에게 직접 장기 Access Key를 발급하기보다 IAM Identity Center 또는 역할 기반 접근을 우선 검토하고, 필요한 최소 권한만 부여하는 편이 안전합니다.

# testuser 사용자 생성
aws iam create-user --user-name testuser
{
    "User": {
        "Path": "/",
        "UserName": "testuser",
        "UserId": "AIDA<REDACTED>",
        "Arn": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
        "CreateDate": "2026-04-12T14:31:12+00:00"
    }
}

# 사용자에게 프로그래밍 방식 액세스 권한 부여
aws iam create-access-key --user-name testuser
{
    "AccessKey": {
        "UserName": "testuser",
        "AccessKeyId": "AKIA<REDACTED>",
        "Status": "Active",
        "SecretAccessKey": "<REDACTED>",
        "CreateDate": "2026-04-12T14:31:21+00:00"
    }
}

# 실습 편의를 위해 testuser 사용자에 AdministratorAccess 정책을 추가
aws iam attach-user-policy \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
  --user-name testuser

# 현재 AWS CLI가 어떤 IAM 주체로 동작하는지 확인
# 여기서는 testuser가 아니라, 위 IAM 작업을 수행한 관리자 주체가 출력됩니다.
aws sts get-caller-identity --query Arn
"arn:aws:iam::<ACCOUNT_ID>:user/<ADMIN_USER>"

사전준비 2. AWS IAM 자격증명 설정 및 확인 (aws-cli 컨테이너에서 테스트)

이번에는 내 PC에서 AWS CLI 컨테이너를 새로 실행하고, 방금 만든 testuser Access Key만 설정합니다. 이렇게 하면 기존 로컬 AWS profile과 분리된 깨끗한 환경에서 testuser 권한을 확인할 수 있습니다.

# 내 PC에서 실행
docker run -it --name aws-cli --entrypoint /bin/sh amazon/aws-cli

컨테이너 안에는 아직 AWS 자격증명이 없으므로 AWS API 호출이 실패합니다.

sh-5.2# aws s3 ls

aws: [ERROR]: An error occurred (NoCredentials): Unable to locate credentials. You can configure credentials by running "aws login".

이제 testuser의 Access Key를 설정합니다(실제 Access Key와 Secret Access Key는 글에 남기지 않습니다).

sh-5.2# aws configure

AWS Access Key ID [None]: AKIA<REDACTED>
AWS Secret Access Key [None]: <REDACTED>
Default region name [None]: ap-northeast-2
Default output format [None]: json

현재 컨테이너의 AWS CLI가 testuser로 동작하는지 확인합니다.

sh-5.2# aws sts get-caller-identity --query Arn
"arn:aws:iam::<ACCOUNT_ID>:user/testuser"

앞에서 실습 편의를 위해 AdministratorAccess를 붙였으므로, S3 목록 조회도 성공합니다.

sh-5.2# aws s3 ls
2026-02-12 15:48:19 <BUCKET_NAME>
2026-02-12 15:56:29 <BUCKET_NAME>

I. testuser의 kubeconfig 생성 및 EKS 인증 실패 확인

이제 testuser 자격증명으로 kubeconfig를 생성하고 kubectl을 실행해봅니다. 여기서 확인할 핵심은 AWS IAM 사용자로는 인증되지만, 아직 EKS Access Entry가 없으면 쿠버네티스 API에서는 인증 주체로 받아들여지지 않는다는 점입니다.

sh-5.2# CLUSTER_NAME=myeks

sh-5.2# aws eks update-kubeconfig --name $CLUSTER_NAME --user-alias testuser
Added new context testuser to /root/.kube/config

생성된 kubeconfig는 testuser context를 만들고, 사용자 인증에는 aws eks get-token exec plugin을 사용합니다. 인증서 데이터와 EKS endpoint는 길어서 축약합니다.

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: <REDACTED>
    server: https://<EKS_API_SERVER>
  name: arn:aws:eks:ap-northeast-2:<ACCOUNT_ID>:cluster/myeks
contexts:
- context:
    cluster: arn:aws:eks:ap-northeast-2:<ACCOUNT_ID>:cluster/myeks
    user: testuser
  name: testuser
current-context: testuser
kind: Config
users:
- name: testuser
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      command: aws
      args:
      - --region
      - ap-northeast-2
      - eks
      - get-token
      - --cluster-name
      - myeks
      - --output
      - json

컨테이너에는 kubectl이 없으므로 설치합니다.

sh-5.2# curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.35.2/2026-02-27/bin/linux/amd64/kubectl
sh-5.2# install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

sh-5.2# kubectl version --client=true
Client Version: v1.35.2-eks-f69f56f
Kustomize Version: v5.7.1

이 상태에서 쿠버네티스 API를 호출하면 실패합니다.

sh-5.2# kubectl get node -v6
I0412 14:39:11.900502      43 loader.go:405] Config loaded from file:  /root/.kube/config
I0412 14:39:13.350666      43 round_trippers.go:632] "Response" verb="GET" url="https://<EKS_API_SERVER>/api?timeout=32s" status="401 Unauthorized"
E0412 14:39:13.352334      43 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: the server has asked for the client to provide credentials"
...
error: You must be logged in to the server (the server has asked for the client to provide credentials)

rbac-tool whoami도 같은 이유로 실패합니다. 컨테이너에는 tar, gzip이 없어 먼저 설치했습니다.

sh-5.2# yum install tar gzip -y

sh-5.2# curl https://raw.githubusercontent.com/alcideio/rbac-tool/master/download.sh | bash
alcideio/rbac-tool info found version: 1.20.0 for v1.20.0/linux/amd64
alcideio/rbac-tool info installed ./bin/rbac-tool

sh-5.2# ./bin/rbac-tool whoami
Error: Failed to create kubernetes client - the server has asked for the client to provide credentials

여기서 실패 원인은 testuser의 AWS IAM 권한이 부족해서가 아닙니다. 앞에서 aws sts get-caller-identityaws s3 ls는 성공했습니다. 문제는 testuser가 아직 EKS Access Entry나 aws-auth ConfigMap을 통해 쿠버네티스 접근 주체로 등록되지 않았다는 점입니다.

즉 이 단계의 상태는 아래처럼 정리할 수 있습니다.

AWS IAM
  -> testuser 인증 성공
  -> AdministratorAccess 때문에 S3 조회 가능

EKS Kubernetes API
  -> aws eks get-token 기반 kubeconfig 생성 가능
  -> 하지만 testuser가 EKS 접근 주체로 등록되지 않아 Kubernetes 인증 실패
  -> kubectl get node: 401 Unauthorized
  -> rbac-tool whoami: 인증 실패

II. EKS API로 Access Entry 생성 및 AmazonEKSAdminPolicy 연결

이제 testuser를 EKS 접근 주체로 등록합니다. 여기서 사용하는 AmazonEKSAdminPolicy는 IAM 정책이 아니라 EKS 관리형 Access Policy입니다. 앞에서 붙인 IAM AdministratorAccess는 AWS API 호출 권한이고, 아래에서 연결하는 Access Policy는 쿠버네티스 API 요청에 대한 EKS 측 인가 권한입니다.

다만 이름과 달리 AmazonEKSAdminPolicy가 곧바로 쿠버네티스의 cluster-admin과 동일하다는 뜻은 아닙니다. AWS 문서 기준으로 이 정책은 대부분의 쿠버네티스 리소스에 대한 권한을 주는 관리자급 정책이고, 클러스터의 모든 리소스에 대한 완전한 관리자 권한이 필요하면 AmazonEKSClusterAdminPolicy를 사용해야 합니다.

이 과정을 거치면 앞 단계의 401 Unauthorized가 아래 흐름으로 바뀝니다. 핵심은 testuser가 EKS Access Entry에 등록되어 쿠버네티스 username으로 매핑되고, AmazonEKSAdminPolicy가 EKS authorizer의 인가 판단 근거로 사용된다는 점입니다. 단, 허용 범위는 연결한 Access Policy가 가진 규칙 안으로 제한됩니다.

flowchart LR
  subgraph Before["Before: Access Entry 없음"]
    B1["testuser
IAM 인증 가능"] --> B2["aws eks get-token
token 생성"] B2 --> B3["kube-apiserver
TokenReview"] B3 --> B4["EKS 인증 웹훅"] B4 --> B5["매핑된 EKS 접근 주체 없음"] B5 --> B6["401 Unauthorized"] end subgraph After["After: Access Entry + Access Policy"] A1["testuser
IAM 인증 가능"] --> A2["aws eks get-token
token 생성"] A2 --> A3["kube-apiserver
TokenReview"] A3 --> A4["EKS 인증 웹훅"] A4 --> A5["Access Entry 조회
principalArn = user/testuser"] A5 --> A6["Kubernetes username 매핑
arn:aws:iam::ACCOUNT_ID:user/testuser"] A6 --> A7["Authorization
SubjectAccessReview 성격의 인가 판단"] A7 --> A8["EKS Access Policy 확인
AmazonEKSAdminPolicy
scope = cluster"] A8 --> A9["정책 범위 내
Kubernetes API 요청 허용"] end

먼저 testuser에 대한 Access Entry를 생성합니다.

sh-5.2# export CLUSTER_NAME=myeks
sh-5.2# export ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)

sh-5.2# aws eks create-access-entry \
  --cluster-name $CLUSTER_NAME \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser
{
    "accessEntry": {
        "clusterName": "myeks",
        "principalArn": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
        "kubernetesGroups": [],
        "accessEntryArn": "arn:aws:eks:ap-northeast-2:<ACCOUNT_ID>:access-entry/myeks/user/<ACCOUNT_ID>/testuser/<ACCESS_ENTRY_ID>",
        "createdAt": "2026-04-12T14:44:21.977000+00:00",
        "modifiedAt": "2026-04-12T14:44:21.977000+00:00",
        "tags": {},
        "username": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
        "type": "STANDARD"
    }
}

Access Entry가 추가되었는지 확인합니다.

sh-5.2# aws eks list-access-entries --cluster-name $CLUSTER_NAME | jq -r .accessEntries[]
arn:aws:iam::<ACCOUNT_ID>:role/aws-service-role/eks.amazonaws.com/AWSServiceRoleForAmazonEKS
arn:aws:iam::<ACCOUNT_ID>:role/myeks-ng-1
arn:aws:iam::<ACCOUNT_ID>:user/<ADMIN_USER>
arn:aws:iam::<ACCOUNT_ID>:user/testuser

이 상태의 Access Entry를 보면 kubernetesGroups가 비어 있습니다. 즉 testuser를 특정 쿠버네티스 RBAC group에 넣은 것이 아닙니다. 대신 다음 단계에서 EKS 관리형 Access Policy를 연결합니다.

sh-5.2# aws eks associate-access-policy \
  --cluster-name $CLUSTER_NAME \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSAdminPolicy \
  --access-scope type=cluster
{
    "clusterName": "myeks",
    "principalArn": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
    "associatedAccessPolicy": {
        "policyArn": "arn:aws:eks::aws:cluster-access-policy/AmazonEKSAdminPolicy",
        "accessScope": {
            "type": "cluster",
            "namespaces": []
        },
        "associatedAt": "2026-04-12T14:44:36.348000+00:00",
        "modifiedAt": "2026-04-12T14:44:36.348000+00:00"
    }
}

연결된 Access Policy와 Access Entry를 다시 확인합니다.

sh-5.2# aws eks list-associated-access-policies \
  --cluster-name $CLUSTER_NAME \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser | jq
{
  "associatedAccessPolicies": [
    {
      "policyArn": "arn:aws:eks::aws:cluster-access-policy/AmazonEKSAdminPolicy",
      "accessScope": {
        "type": "cluster",
        "namespaces": []
      },
      "associatedAt": "2026-04-12T14:44:36.348000+00:00",
      "modifiedAt": "2026-04-12T14:44:36.348000+00:00"
    }
  ],
  "clusterName": "myeks",
  "principalArn": "arn:aws:iam::<ACCOUNT_ID>:user/testuser"
}

sh-5.2# aws eks describe-access-entry \
  --cluster-name $CLUSTER_NAME \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser | jq
{
  "accessEntry": {
    "clusterName": "myeks",
    "principalArn": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
    "kubernetesGroups": [],
    "accessEntryArn": "arn:aws:eks:ap-northeast-2:<ACCOUNT_ID>:access-entry/myeks/user/<ACCOUNT_ID>/testuser/<ACCESS_ENTRY_ID>",
    "createdAt": "2026-04-12T14:44:21.977000+00:00",
    "modifiedAt": "2026-04-12T14:44:21.977000+00:00",
    "tags": {},
    "username": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
    "type": "STANDARD"
  }
}

정리하면 이 단계에서 바뀐 점은 아래와 같습니다.

Before
  IAM user: arn:aws:iam::<ACCOUNT_ID>:user/testuser
  -> AWS API 호출 가능
  -> EKS Kubernetes API에서는 인증 주체로 등록되지 않음
  -> kubectl: 401 Unauthorized

After
  Access Entry
    principalArn: arn:aws:iam::<ACCOUNT_ID>:user/testuser
    username: arn:aws:iam::<ACCOUNT_ID>:user/testuser
    kubernetesGroups: []

  Associated Access Policy
    policyArn: arn:aws:eks::aws:cluster-access-policy/AmazonEKSAdminPolicy
    accessScope: cluster

여기서 중요한 점은 kubernetesGroups가 비어 있다는 것입니다. 이 예시는 쿠버네티스 RoleBinding/ClusterRoleBindingtestuser를 직접 묶는 방식이 아니라, EKS Access Policy를 통해 인가를 부여하는 방식입니다.

따라서 이 단계 이후에는 401 Unauthorized처럼 “누군지 모르겠다”는 인증 실패는 사라질 수 있습니다. 하지만 모든 쿠버네티스 API가 무조건 허용되는 것은 아닙니다. AmazonEKSAdminPolicy의 규칙에 없는 리소스나 동작은 여전히 403 Forbidden이 날 수 있습니다. 완전한 클러스터 관리자 권한을 확인하려는 실습이라면 AmazonEKSClusterAdminPolicy를 연결해야 합니다.

III. kubectl 사용 확인하기

이제 같은 testuser kubeconfig로 쿠버네티스 API를 다시 호출합니다. 앞 단계에서 AmazonEKSAdminPolicy를 연결했으므로, 이 정책에 포함된 리소스/동작은 허용됩니다.

AWS의 Access Policy 권한 목록을 기준으로 보면 아래처럼 판단할 수 있습니다.

명령 필요한 쿠버네티스 권한 AmazonEKSAdminPolicy 기준
kubectl get node -v6 core API group의 nodes에 대한 list 안 됨. AmazonEKSAdminPolicynodes 리소스가 포함되어 있지 않음
kubectl get deploy -A apps API group의 deployments에 대한 list . deploymentsget, list, watch 권한 포함
kubectl auth can-i delete pods --all-namespaces core API group의 pods에 대한 delete . podscreate, delete, deletecollection, patch, update 권한 포함

따라서 kubectl get node가 실패하고, kubectl get deploy -Akubectl auth can-i delete pods --all-namespaces가 성공하는 것이 이 정책의 기대 동작입니다.

sh-5.2# kubectl get deploy -A
NAMESPACE      NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
cert-manager   cert-manager              2/2     2            2           3h7m
cert-manager   cert-manager-cainjector   2/2     2            2           3h7m
cert-manager   cert-manager-webhook      2/2     2            2           3h7m
external-dns   external-dns              1/1     1            1           3h7m
kube-system    coredns                   2/2     2            2           3h7m
kube-system    metrics-server            2/2     2            2           3h7m

sh-5.2# kubectl auth can-i delete pods --all-namespaces
yes

이 결과를 보면 Access Entry와 EKS Access Policy 연결은 정상적으로 동작하고 있습니다. 앞에서의 401 Unauthorized처럼 인증 자체가 실패하는 상태라면 kubectl get deploy -A도 성공하지 못했을 것입니다.

다만 kubectl get node가 실패한다면 그건 별개의 문제입니다. AmazonEKSAdminPolicy는 이름과 달리 클러스터 전체 cluster-admin이 아니라 “대부분의 쿠버네티스 리소스”에 대한 관리자급 정책입니다. 노드 조회처럼 정책에 포함되지 않은 리소스는 여전히 거부됩니다.

# 권한 범위 확인
kubectl auth can-i list deployments.apps --all-namespaces
kubectl auth can-i delete pods --all-namespaces
kubectl auth can-i list nodes

만약 nodes까지 포함한 완전한 클러스터 관리자 권한을 실습하려면 AmazonEKSAdminPolicy 대신 AmazonEKSClusterAdminPolicy를 연결해야 합니다. AmazonEKSClusterAdminPolicy는 API group, non-resource URL, resource, verb가 모두 *인 정책입니다.

export CLUSTER_NAME=myeks
export ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
export PRINCIPAL_ARN=arn:aws:iam::$ACCOUNT_ID:user/testuser

aws eks disassociate-access-policy \
  --cluster-name $CLUSTER_NAME \
  --principal-arn $PRINCIPAL_ARN \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSAdminPolicy

aws eks associate-access-policy \
  --cluster-name $CLUSTER_NAME \
  --principal-arn $PRINCIPAL_ARN \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy \
  --access-scope type=cluster

디버깅할 때는 먼저 실패 종류를 구분합니다.

401 Unauthorized
  -> Access Entry 미등록, kubeconfig/token 문제, 인증 실패

403 Forbidden
  -> 인증은 됐지만 현재 Access Policy/RBAC 권한 밖의 요청

exec: executable aws not found
  -> kubeconfig의 exec plugin이 aws CLI를 실행하지 못함

커스텀 RBAC을 적용해보기

사전작업 1. access entry 제거하기

위 작업에서 이어진 Access entry는 제거합니다.

# 기존 testuser access entry 제거
sh-5.2# aws eks delete-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser
aws eks list-access-entries --cluster-name $CLUSTER_NAME | jq -r .accessEntries[]
arn:aws:iam::<ACCOUNT_ID>:role/aws-service-role/eks.amazonaws.com/AWSServiceRoleForAmazonEKS
arn:aws:iam::<ACCOUNT_ID>:role/myeks-ng-1
arn:aws:iam::<ACCOUNT_ID>:user/<ADMIN_USER>
sh-5.2#

1. ClusterRole, ClusterRoleBinding 생성하기

이번에는 EKS 관리형 Access Policy를 쓰지 않고, 쿠버네티스 RBAC으로 직접 권한을 구성합니다. 먼저 Pod 조회 전용 ClusterRole과 Pod 전체 권한을 가진 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
clusterrole.rbac.authorization.k8s.io/pod-viewer-role created
clusterrole.rbac.authorization.k8s.io/pod-admin-role created

생성된 ClusterRole을 확인합니다.

$ kubectl get clusterrole | grep ^pod
pod-admin-role    2026-04-12T14:59:48Z
pod-viewer-role   2026-04-12T14:59:48Z

이제 각각의 쿠버네티스 group에 ClusterRole을 연결합니다.

$ kubectl create clusterrolebinding viewer-role-binding \
  --clusterrole=pod-viewer-role \
  --group=pod-viewer
clusterrolebinding.rbac.authorization.k8s.io/viewer-role-binding created

$ kubectl create clusterrolebinding admin-role-binding \
  --clusterrole=pod-admin-role \
  --group=pod-admin
clusterrolebinding.rbac.authorization.k8s.io/admin-role-binding created

$ kubectl get clusterrolebinding | grep -E 'admin-role|viewer-role'
admin-role-binding    ClusterRole/pod-admin-role    1s
viewer-role-binding   ClusterRole/pod-viewer-role   2s

rbac-tool로 group과 RoleBinding 연결을 확인합니다.

$ kubectl rbac-tool lookup pod-viewer
  SUBJECT    | SUBJECT TYPE | SCOPE       | NAMESPACE | ROLE            | BINDING
-------------+--------------+-------------+-----------+-----------------+----------------------
  pod-viewer | Group        | ClusterRole |           | pod-viewer-role | viewer-role-binding

$ kubectl rbac-tool lookup pod-admin
  SUBJECT   | SUBJECT TYPE | SCOPE       | NAMESPACE | ROLE           | BINDING
------------+--------------+-------------+-----------+----------------+---------------------
  pod-admin | Group        | ClusterRole |           | pod-admin-role | admin-role-binding

rolesum으로 각 group의 실제 권한 차이를 보면 더 명확합니다.

$ kubectl rolesum -k Group pod-viewer
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      [*]     [-]     [-]   ✔ ✔ ✔ ✖ ✖ ✖ ✖ ✖

$ kubectl rolesum -k Group pod-admin
Group: pod-admin

Policies:
 [CRB] */admin-role-binding ⟶  [CR] */pod-admin-role
  Resource  Name  Exclude  Verbs  G L W C U P D DC
  pods      [*]     [-]     [-]   ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔

정리하면 pod-viewer group은 Pod에 대해 get, list, watch만 가능하고, pod-admin group은 Pod에 대한 모든 verb가 가능합니다. 다음 단계에서는 EKS Access Entry에서 testuser를 이 쿠버네티스 group 중 하나에 매핑합니다.

2. Access Entry 설정하기 - pod-viewer 로 만들기

이제 testuser의 Access Entry를 다시 생성하되, EKS Access Policy를 연결하지 않고 쿠버네티스 group만 pod-viewer로 지정합니다. 앞에서 만든 ClusterRoleBinding 때문에 pod-viewer group은 Pod 조회 권한만 갖습니다.

sh-5.2# aws eks create-access-entry \
  --cluster-name $CLUSTER_NAME \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser \
  --kubernetes-group pod-viewer
{
    "accessEntry": {
        "clusterName": "myeks",
        "principalArn": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
        "kubernetesGroups": [
            "pod-viewer"
        ],
        "accessEntryArn": "arn:aws:eks:ap-northeast-2:<ACCOUNT_ID>:access-entry/myeks/user/<ACCOUNT_ID>/testuser/<ACCESS_ENTRY_ID>",
        "createdAt": "2026-04-12T15:04:22.964000+00:00",
        "modifiedAt": "2026-04-12T15:04:22.964000+00:00",
        "tags": {},
        "username": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
        "type": "STANDARD"
    }
}

Access Policy가 연결되어 있지 않은 것도 확인합니다. 이번 단계의 인가는 EKS Access Policy가 아니라 쿠버네티스 RBAC이 담당합니다.

sh-5.2# aws eks list-associated-access-policies \
  --cluster-name $CLUSTER_NAME \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser
{
    "associatedAccessPolicies": [],
    "clusterName": "myeks",
    "principalArn": "arn:aws:iam::<ACCOUNT_ID>:user/testuser"
}

Access Entry에는 pod-viewer group만 들어 있습니다.

sh-5.2# aws eks describe-access-entry \
  --cluster-name $CLUSTER_NAME \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser | jq
{
  "accessEntry": {
    "clusterName": "myeks",
    "principalArn": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
    "kubernetesGroups": [
      "pod-viewer"
    ],
    "accessEntryArn": "arn:aws:eks:ap-northeast-2:<ACCOUNT_ID>:access-entry/myeks/user/<ACCOUNT_ID>/testuser/<ACCESS_ENTRY_ID>",
    "createdAt": "2026-04-12T15:04:22.964000+00:00",
    "modifiedAt": "2026-04-12T15:04:22.964000+00:00",
    "tags": {},
    "username": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
    "type": "STANDARD"
  }
}

3. kubectl로 확인하기

testuser는 이제 pod-viewer group으로 인증됩니다. 따라서 Pod 조회는 가능하지만, Node 조회나 Pod 삭제는 불가능해야 합니다.

sh-5.2# kubectl get node -A
Error from server (Forbidden): nodes is forbidden: User "arn:aws:iam::<ACCOUNT_ID>:user/testuser" cannot list resource "nodes" in API group "" at the cluster scope

sh-5.2# kubectl get pod -A
NAMESPACE      NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager   cert-manager-b5c55cbd4-54q5c               1/1     Running   0          3h16m
cert-manager   cert-manager-b5c55cbd4-zdgqx               1/1     Running   0          3h16m
cert-manager   cert-manager-cainjector-6fc9d9ddc7-88gcm   1/1     Running   0          3h16m
cert-manager   cert-manager-cainjector-6fc9d9ddc7-t5vds   1/1     Running   0          3h16m
cert-manager   cert-manager-webhook-cf55c69cf-84hfw       1/1     Running   0          3h16m
cert-manager   cert-manager-webhook-cf55c69cf-vlblp       1/1     Running   0          3h16m
external-dns   external-dns-79bc6f9c7c-fxltl              1/1     Running   0          3h17m
kube-system    aws-node-6vnfc                             2/2     Running   0          3h17m
kube-system    aws-node-rddww                             2/2     Running   0          3h17m
kube-system    coredns-7b7dc46964-b2664                   1/1     Running   0          3h17m
kube-system    coredns-7b7dc46964-cn858                   1/1     Running   0          3h17m
kube-system    eks-pod-identity-agent-7x79s               1/1     Running   0          3h17m
kube-system    eks-pod-identity-agent-mlttd               1/1     Running   0          3h17m
kube-system    kube-proxy-9mckb                           1/1     Running   0          3h17m
kube-system    kube-proxy-ng866                           1/1     Running   0          3h17m
kube-system    metrics-server-69cfcf7444-qnkkq            1/1     Running   0          3h17m
kube-system    metrics-server-69cfcf7444-vmdn7            1/1     Running   0          3h17m

sh-5.2# kubectl auth can-i get pods --all-namespaces
yes

sh-5.2# kubectl auth can-i delete pods --all-namespaces
no

이 결과는 의도한 대로입니다.

Access Entry
  testuser -> kubernetesGroups: ["pod-viewer"]

Kubernetes RBAC
  pod-viewer -> pod-viewer-role
  pod-viewer-role -> pods get/list/watch only

결과
  nodes list       -> no
  pods get/list    -> yes
  pods delete      -> no

4. Access Entry 설정하기 - pod-admin 으로 만들기

이번에는 같은 testuser Access Entry의 쿠버네티스 group을 pod-viewer에서 pod-admin으로 변경합니다. Access Entry 자체를 새로 만드는 것이 아니라 update-access-entry로 group 매핑만 바꿉니다.

sh-5.2# aws eks update-access-entry \
  --cluster-name $CLUSTER_NAME \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser \
  --kubernetes-group pod-admin | jq -r .accessEntry
{
  "clusterName": "myeks",
  "principalArn": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
  "kubernetesGroups": [
    "pod-admin"
  ],
  "accessEntryArn": "arn:aws:eks:ap-northeast-2:<ACCOUNT_ID>:access-entry/myeks/user/<ACCOUNT_ID>/testuser/<ACCESS_ENTRY_ID>",
  "createdAt": "2026-04-12T15:04:22.964000+00:00",
  "modifiedAt": "2026-04-12T15:06:42.840000+00:00",
  "tags": {},
  "username": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
  "type": "STANDARD"
}

변경 결과를 다시 확인합니다.

sh-5.2# aws eks describe-access-entry \
  --cluster-name $CLUSTER_NAME \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser | jq
{
  "accessEntry": {
    "clusterName": "myeks",
    "principalArn": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
    "kubernetesGroups": [
      "pod-admin"
    ],
    "accessEntryArn": "arn:aws:eks:ap-northeast-2:<ACCOUNT_ID>:access-entry/myeks/user/<ACCOUNT_ID>/testuser/<ACCESS_ENTRY_ID>",
    "createdAt": "2026-04-12T15:04:22.964000+00:00",
    "modifiedAt": "2026-04-12T15:06:42.840000+00:00",
    "tags": {},
    "username": "arn:aws:iam::<ACCOUNT_ID>:user/testuser",
    "type": "STANDARD"
  }
}

이제 testuser는 쿠버네티스 RBAC 관점에서 pod-admin group에 속합니다. 앞에서 pod-admin group은 pod-admin-role에 바인딩되어 있고, 해당 Role은 Pod 리소스에 대해 모든 verb를 허용합니다.

5. kubectl로 확인하기

다시 같은 명령으로 권한 차이를 확인합니다.

sh-5.2# kubectl get node -A
Error from server (Forbidden): nodes is forbidden: User "arn:aws:iam::<ACCOUNT_ID>:user/testuser" cannot list resource "nodes" in API group "" at the cluster scope

sh-5.2# kubectl get pod -A
NAMESPACE      NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager   cert-manager-b5c55cbd4-54q5c               1/1     Running   0          3h19m
cert-manager   cert-manager-b5c55cbd4-zdgqx               1/1     Running   0          3h19m
cert-manager   cert-manager-cainjector-6fc9d9ddc7-88gcm   1/1     Running   0          3h19m
cert-manager   cert-manager-cainjector-6fc9d9ddc7-t5vds   1/1     Running   0          3h19m
cert-manager   cert-manager-webhook-cf55c69cf-84hfw       1/1     Running   0          3h19m
cert-manager   cert-manager-webhook-cf55c69cf-vlblp       1/1     Running   0          3h19m
external-dns   external-dns-79bc6f9c7c-fxltl              1/1     Running   0          3h19m
kube-system    aws-node-6vnfc                             2/2     Running   0          3h19m
kube-system    aws-node-rddww                             2/2     Running   0          3h19m
kube-system    coredns-7b7dc46964-b2664                   1/1     Running   0          3h19m
kube-system    coredns-7b7dc46964-cn858                   1/1     Running   0          3h19m
kube-system    eks-pod-identity-agent-7x79s               1/1     Running   0          3h19m
kube-system    eks-pod-identity-agent-mlttd               1/1     Running   0          3h19m
kube-system    kube-proxy-9mckb                           1/1     Running   0          3h19m
kube-system    kube-proxy-ng866                           1/1     Running   0          3h19m
kube-system    metrics-server-69cfcf7444-qnkkq            1/1     Running   0          3h19m
kube-system    metrics-server-69cfcf7444-vmdn7            1/1     Running   0          3h19m

sh-5.2# kubectl auth can-i get pods --all-namespaces
yes

sh-5.2# kubectl auth can-i delete pods --all-namespaces
yes

pod-viewer일 때와 달라진 점은 delete podsno에서 yes로 바뀐 것입니다. 반면 kubectl get node는 여전히 실패합니다. pod-admin-role은 이름 그대로 Pod 리소스에 대한 권한만 주기 때문입니다.

Access Entry
  testuser -> kubernetesGroups: ["pod-admin"]

Kubernetes RBAC
  pod-admin -> pod-admin-role
  pod-admin-role -> pods *

결과
  nodes list       -> no
  pods get/list    -> yes
  pods delete      -> yes

즉 Access Entry는 IAM principal을 쿠버네티스 group에 매핑하고, 실제 허용/거부는 그 group에 연결된 쿠버네티스 RBAC이 결정합니다.

실습 리소스 정리

실습이 끝나면 테스트용 IAM 사용자와 쿠버네티스 RBAC 리소스를 정리합니다. 특히 testuser는 장기 Access Key를 가진 실습용 사용자이므로 남겨두지 않습니다.

# aws-cli 컨테이너 제거
docker rm -f aws-cli

# EKS Access Entry 제거
aws eks delete-access-entry \
  --cluster-name $CLUSTER_NAME \
  --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser

# 실습용 RBAC 리소스 제거
kubectl delete clusterrolebinding viewer-role-binding admin-role-binding
kubectl delete clusterrole pod-viewer-role pod-admin-role

# testuser Access Key 확인 후 삭제
aws iam list-access-keys --user-name testuser
aws iam delete-access-key --user-name testuser --access-key-id <ACCESS_KEY_ID>

# testuser에 붙인 IAM 정책 분리 및 사용자 삭제
aws iam detach-user-policy \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
  --user-name testuser
aws iam delete-user --user-name testuser

관리 콘솔에서 정리한다면 IAM → Users → testuser에서 Access Key와 사용자 삭제 여부를 한 번 더 확인합니다.