infra
Platform

모듈 맵

[Kubernetes] ServiceAccount를 이용한 컨테이너 내부의 API 서버 안전 통신

0 / 29 완료

펼치기
0 / 29 완료0%

쿠버네티스 & GitOps · 16 / 29

[Kubernetes] ServiceAccount를 이용한 컨테이너 내부의 API 서버 안전 통신

파드가 Kubernetes API를 호출할 수 있도록 ServiceAccount와 토큰을 설계하고, IRSA로 AWS 권한까지 연동합니다

🚨INCIDENT ALERT
HIGH

파드 내부의 배치 잡이 Kubernetes API를 호출하려는데 401 Unauthorized가 발생합니다. 운영팀은 사람 계정과 파드가 사용하는 ServiceAccount를 구분해야 권한을 안전하게 부여할 수 있습니다. ServiceAccount는 워크로드가 클러스터와 통신할 때 쓰는 신원입니다.

ServiceAccount — 파드의 K8s API 접근 권한

운영팀에서 배포 자동화 도구를 개발한다고 상상해보세요. 이 도구는 파드 안에서 실행되면서 다른 Deployment의 레플리카 수를 동적으로 조정해야 합니다. 아무 설정 없이 K8s API를 호출하면 403 Forbidden이 돌아옵니다. 단순히 파드가 실행 중이라는 사실만으로는 클러스터를 제어할 권한이 없기 때문입니다. 여기에 더해, AWS S3 버킷에서 설정 파일을 내려받아야 한다면 어떻게 할까요? 예전 방식처럼 Access Key를 Secret에 넣어두면 키가 노출될 위험이 있습니다. ServiceAccount는 파드에 클러스터 내부 ID를 부여하고, IRSA는 그 ID를 AWS IAM 권한과 연결하여 이 두 문제를 모두 해결합니다. 이 모듈에서는 ServiceAccount의 동작 원리부터 실무에서 자주 쓰는 IRSA 패턴까지 단계적으로 익힙니다.

이번 챕터에서 배울 것
  • 1ServiceAccount의 역할과 default 계정의 한계를 설명할 수 있다
  • 2ServiceAccount 토큰의 자동 마운트 경로와 구조를 설명할 수 있다
  • 3전용 ServiceAccount를 생성하고 RBAC를 연결할 수 있다
  • 4automountServiceAccountToken으로 공격 표면을 최소화할 수 있다
  • 5IRSA(IAM Roles for Service Accounts)의 원리를 이해하고 설정할 수 있다
  • 6파드 내부에서 K8s API를 직접 호출할 수 있다
실습 환경 준비
클러스터 연결 확인
kubectl cluster-info
현재 네임스페이스 ServiceAccount 목록
kubectl get serviceaccount
default SA의 마운트 상태 확인
kubectl get sa default -o yaml
실습용 네임스페이스 생성
kubectl create namespace sa-demo

ServiceAccount 토큰이 파드에 마운트되는 구조

K8s는 파드가 생성될 때 ServiceAccount 토큰을 /var/run/secrets/kubernetes.io/serviceaccount/ 경로에 자동으로 마운트합니다. 이 디렉토리에는 세 파일이 있습니다.

로컬 터미널
# 파드 안에서 실행
ls /var/run/secrets/kubernetes.io/serviceaccount/
# ca.crt   namespace   token

# token: JWT 형태의 ServiceAccount 토큰
cat /var/run/secrets/kubernetes.io/serviceaccount/token
# eyJhbGciOiJSUzI1NiIsImtpZCI6Ii...

# namespace: 현재 파드가 속한 네임스페이스
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace
# default

# ca.crt: K8s API 서버의 TLS 인증서 검증용 CA
ls -la /var/run/secrets/kubernetes.io/serviceaccount/ca.crt

이 토큰을 Bearer 헤더에 넣고 API 서버 주소로 요청을 보내면 K8s API를 호출할 수 있습니다.

💡개념

파드가 API를 호출하면 실제로 무슨 일이 — 인증부터 인가까지 5단계

파드 안에서 curl로 K8s API를 부르면 401이 날 때도, 403이 날 때도 있는데 이 둘은 완전히 다른 단계에서 막힌 것입니다. 토큰이 만들어져 마운트되고 → 파드가 그 토큰을 제시하고 → API server가 토큰을 검증하고(인증) → RBAC이 권한을 확인하는(인가) 전체 경로를 단계로 보면, 401("너 누구야"가 실패)과 403("누군진 알겠는데 권한 없음")을 바로 구분해 고칠 수 있습니다. 앞에서 본 토큰 파일 세 개(token·ca.crt·namespace)가 이 흐름의 어디서 쓰이는지도 함께 보입니다.

TEXT
[ServiceAccount 생성]  kubectl create sa → 클러스터에 SA 신원 등록
   │
   ① kubelet이 파드 생성 시 projected 토큰(JWT)을 발급받아
   │      /var/run/secrets/.../token 에 마운트 (기본 1시간, 자동 갱신)
   │
   ② 파드 앱이 API 호출 시 그 토큰을 Authorization: Bearer 헤더에 실음
   │
   ③ API server가 토큰 검증(인증) — 서명·발급자(iss)·만료(exp) 확인
   │      → 통과하면 "너는 system:serviceaccount:<ns>:<sa>"로 신원 확정
   │
   ④ RBAC 인가 — 그 신원이 요청한 동사·리소스에 Role/RoleBinding이 있는지
   │
   ⑤ 허용 → 요청 실행 후 결과 반환
   ▼
[결과]  200/201(허용)  ·  401(인증 실패)  ·  403(인가 실패)

각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:

단계하는 일여기서 막히면
① 토큰 마운트kubelet이 SA의 projected JWT를 발급해 /var/run/secrets/kubernetes.io/serviceaccount/token에 마운트automountServiceAccountToken: false거나 마운트 경로 없음 → 토큰 자체가 없어 호출 불가
② 토큰 제시앱이 토큰을 Authorization: Bearer 헤더에 넣어 API server로 요청앱이 시작 시 토큰을 캐시했다가 만료된 값 사용 → 401 (매 요청 파일에서 다시 읽거나 공식 SDK 사용)
③ 인증(서명·발급자)API server가 토큰의 서명·iss·exp를 검증해 신원 확정토큰 만료·서명 불일치 → 401 Unauthorized (신원 증명 실패)
④ 인가(RBAC)확정된 신원에 대해 요청한 동사·리소스의 Role/RoleBinding 존재 여부 확인바인딩 없음·default SA라 권한 없음 → 403 Forbidden (cannot list ... 메시지)
⑤ 실행·응답인가 통과 시 요청을 실행하고 결과 반환default SA에 과한 권한을 바인딩해두면 파드 침해 시 그 권한만큼 횡적 이동 가능

401403을 헷갈리지 않는 것이 이 흐름의 핵심입니다 — 401은 ②③(토큰·인증) 문제라 토큰 만료·마운트를 보고, 403은 ④(RBAC) 문제라 kubectl auth can-i <동사> <리소스> --as=system:serviceaccount:<ns>:<sa>로 어떤 권한이 빠졌는지 좁힙니다. 403 에러 메시지의 cannot <동사> resource ... 문구가 곧 Role에 추가해야 할 정확한 권한입니다.

💡개념

전용 ServiceAccount 생성과 RBAC 연결

파드 안에서 Kubernetes API를 호출하는 도구를 배포할 때, 아무 ServiceAccount 설정 없이 실행하면 default 계정이 사용됩니다. default 계정은 기본적으로 권한이 거의 없어 403이 반환됩니다. 그렇다고 cluster-admin을 부여하면 이 파드가 탈취됐을 때 클러스터 전체가 위험해집니다. 전용 ServiceAccount를 만들고 필요한 최소 권한만 Role로 정의해 바인딩하는 것이 올바른 방법입니다. 파드별로 독립된 신원을 부여하면 사고 발생 시 피해 범위도 해당 Role로 제한됩니다.

API를 호출해야 하는 파드는 반드시 전용 ServiceAccount를 만들고, 필요한 권한만 Role로 묶어 바인딩해야 합니다.

default vs 전용 ServiceAccount — 파드에 SA를 지정하지 않으면 네임스페이스의 default SA가 자동 할당되는데, 여기에 권한을 주면 모든 파드가 공유해 과도한 권한이 퍼짐. API 호출이 필요한 파드는 전용 SA를 만들고 필요한 권한만 Role로 묶어 RoleBinding으로 연결 — 최소 권한 원칙확대

YAML
# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: deployment-scaler
  namespace: sa-demo
  annotations:
    description: "Deployment 레플리카 수 조정용 서비스 계정"
---
# role.yaml — 필요한 최소 권한만 부여
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployment-scaler-role
  namespace: sa-demo
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "patch", "update"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]
---
# rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: deployment-scaler-binding
  namespace: sa-demo
subjects:
  - kind: ServiceAccount
    name: deployment-scaler
    namespace: sa-demo
roleRef:
  kind: Role
  name: deployment-scaler-role
  apiGroup: rbac.authorization.k8s.io
Kubernetes
kubectl apply -f serviceaccount.yaml -f role.yaml -f rolebinding.yaml

# 파드에 ServiceAccount 지정
kubectl run scaler-pod \
  --image=curlimages/curl:latest \
  --overrides='{"spec":{"serviceAccountName":"deployment-scaler"}}' \
  --namespace=sa-demo \
  --command -- sleep 3600

# 파드 안에서 K8s API 호출 테스트
kubectl exec -it scaler-pod -n sa-demo -- sh
# 토큰 원문을 출력하거나 로그에 남기지 않습니다. 필요하면 아래처럼 변수로만 사용합니다.
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
APISERVER="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}"
CA="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"

# Deployment 목록 조회
curl -s --cacert $CA \
  -H "Authorization: Bearer $TOKEN" \
  "$APISERVER/apis/apps/v1/namespaces/sa-demo/deployments" | jq '.items[].metadata.name'
💡개념

automountServiceAccountToken: false — 불필요한 토큰 제거

보안 감사에서 "모든 파드에 ServiceAccount 토큰이 마운트되어 있고, 그 중 일부는 K8s API를 전혀 사용하지 않음에도 불구하고 토큰이 노출되어 있습니다"라는 지적이 나왔습니다. 컨테이너 취약점을 통해 공격자가 파드에 접근하면 /var/run/secrets/kubernetes.io/serviceaccount/token을 바로 읽어 클러스터 API 호출에 악용할 수 있습니다. 사용하지 않는 토큰은 처음부터 마운트하지 않는 것이 가장 확실한 방어입니다.

K8s API를 호출하지 않는 파드에는 토큰을 마운트하지 않는 것이 보안 모범 사례입니다. 컨테이너가 탈취되어도 공격자가 클러스터 API를 악용할 수 없습니다.

automountServiceAccountToken: false — 불필요한 토큰 제거확대

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-frontend
  namespace: sa-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-frontend
  template:
    metadata:
      labels:
        app: web-frontend
    spec:
      # 토큰 마운트 비활성화 — K8s API 호출 없는 파드
      automountServiceAccountToken: false
      containers:
        - name: nginx
          image: nginx:1.25
          ports:
            - containerPort: 80
Kubernetes
kubectl apply -f web-frontend.yaml

# 토큰 마운트 여부 확인
kubectl exec -it $(kubectl get pod -n sa-demo -l app=web-frontend -o name | head -1) \
  -n sa-demo -- ls /var/run/secrets/ 2>&1
# ls: cannot access '/var/run/secrets/': No such file or directory
# ← 토큰이 없어서 디렉토리 자체가 존재하지 않음
💡개념

IRSA — ServiceAccount로 AWS 권한 얻기

EKS에서 파드가 S3, DynamoDB, Secrets Manager 같은 AWS 리소스에 접근해야 할 때 IRSA를 사용합니다. 노드 전체에 IAM 역할을 붙이는 방식과 달리 ServiceAccount 단위로 최소 권한을 부여합니다.

로컬 터미널
# 1. EKS OIDC 제공자 확인
aws eks describe-cluster --name my-cluster \
  --query "cluster.identity.oidc.issuer" --output text
# https://oidc.eks.ap-northeast-2.amazonaws.com/id/EXAMPLE1234

# 2. IAM OIDC 제공자 등록 (클러스터당 1회)
eksctl utils associate-iam-oidc-provider \
  --cluster my-cluster \
  --approve

# 3. IAM 역할 생성 (S3 읽기 권한)
eksctl create iamserviceaccount \
  --cluster my-cluster \
  --namespace sa-demo \
  --name s3-reader \
  --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
  --approve
YAML
# eksctl이 자동 생성하는 ServiceAccount
# (수동으로 생성하는 경우 아래와 같이 annotation 추가)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: s3-reader
  namespace: sa-demo
  annotations:
    # IAM 역할 ARN 연결
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/sa-demo-s3-reader
YAML
# IRSA를 사용하는 파드
apiVersion: v1
kind: Pod
metadata:
  name: s3-reader-pod
  namespace: sa-demo
spec:
  serviceAccountName: s3-reader
  containers:
    - name: aws-cli
      image: amazon/aws-cli:latest
      command: ["sleep", "3600"]
      env:
        # AWS SDK가 자동으로 토큰을 읽는 환경 변수 (EKS가 자동 주입)
        - name: AWS_ROLE_ARN
          valueFrom:
            fieldRef:
              fieldPath: metadata.annotations['eks.amazonaws.com/role-arn']
        - name: AWS_WEB_IDENTITY_TOKEN_FILE
          value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
Kubernetes
# 파드 안에서 S3 접근 테스트 (자격증명 없이 동작)
kubectl exec -it s3-reader-pod -n sa-demo -- \
  aws s3 ls s3://my-config-bucket/
# 2024-01-15 09:30:00        1234 app-config.yaml

실습: 파드 내부에서 K8s API 직접 호출

ServiceAccount 토큰을 사용해 K8s API를 직접 호출하고, RBAC 권한이 어떻게 동작하는지 확인합니다.

Kubernetes
# 1. 실습 환경 준비
kubectl create namespace api-test

# 권한이 없는 SA로 API 호출 시도
kubectl run no-perm-pod \
  --image=curlimages/curl:latest \
  --namespace=api-test \
  --command -- sleep 3600

kubectl exec -it no-perm-pod -n api-test -- sh -c '
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
APISERVER="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}"
CA="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
  "$APISERVER/api/v1/namespaces/api-test/pods"
'

예상 결과 (권한 없을 때):

JSON
{
  "kind": "Status",
  "apiVersion": "v1",
  "status": "Failure",
  "message": "pods is forbidden: User \"system:serviceaccount:api-test:default\" cannot list resource \"pods\" in API group \"\" in the namespace \"api-test\"",
  "reason": "Forbidden",
  "code": 403
}
로컬 터미널
# 2. Pod 읽기 권한 부여
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: api-test
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: default-pod-reader
  namespace: api-test
subjects:
  - kind: ServiceAccount
    name: default
    namespace: api-test
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
EOF

# 3. 다시 API 호출 — 이제 성공
kubectl exec -it no-perm-pod -n api-test -- sh -c '
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
APISERVER="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}"
CA="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
  "$APISERVER/api/v1/namespaces/api-test/pods" | python3 -m json.tool | grep '"name"'
'
# "name": "no-perm-pod"
Kubernetes
# 4. 권한 확인 명령어
kubectl auth can-i list pods \
  --as=system:serviceaccount:api-test:default \
  --namespace=api-test
# yes

kubectl auth can-i delete pods \
  --as=system:serviceaccount:api-test:default \
  --namespace=api-test
# no

K8s 1.21 이후 ServiceAccount 토큰은 기본적으로 시간 제한이 있는 Projected Token으로 발급됩니다(기본 1시간). 오래 실행되는 파드에서 초기화 이후 한참이 지나 토큰을 읽으면 만료된 토큰을 사용해 401 오류가 발생합니다.

Kubernetes
# 증상: 파드가 장시간 실행 후 K8s API 호출 실패
kubectl logs api-caller-pod
# Error: API call failed: 401 Unauthorized
# time="2024-01-15T08:00:00Z" token_expired=true

# 원인 진단: 토큰 만료 시각 확인
kubectl exec -it api-caller-pod -- sh -c '
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# JWT 페이로드 디코딩 (base64)
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
'
# {
#   "aud": ["https://kubernetes.default.svc"],
#   "exp": 1705298400,   ← Unix 타임스탬프 (이미 만료됨)
#   "iat": 1705294800,
#   "iss": "https://oidc.eks...",
# }
로컬 터미널
# 해결책 1: 토큰을 캐시하지 말고 매번 파일에서 새로 읽기
# 잘못된 방식 (시작 시 한 번만 읽음)
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# 올바른 방식 (매 요청마다 새로 읽음)
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# 해결책 2: K8s 공식 SDK 사용 — 토큰 갱신 자동 처리
# Python: kubernetes 라이브러리가 자동으로 토큰을 갱신합니다
pip install kubernetes
# from kubernetes import client, config
# config.load_incluster_config()  # 클러스터 내부에서 자동 설정

# 해결책 3: 토큰 만료 시간 연장 (보안상 권장하지 않음)
# ServiceAccount에 TokenRequest API로 장기 토큰 발급
kubectl create token deployment-scaler \
  --namespace=sa-demo \
  --duration=24h

근본 원인: Projected Service Account Token(K8s 1.21+)은 kubelet이 자동으로 갱신하지만, 애플리케이션이 토큰을 메모리에 캐시하면 오래된 토큰을 계속 사용합니다. K8s 공식 클라이언트 라이브러리(Go k8s.io/client-go, Python kubernetes)는 이 갱신을 자동으로 처리하므로 가능하면 직접 curl 대신 공식 SDK를 사용하세요.

심화 — annotation은 달았는데 왜 AWS는 거부할까

💡개념

심화: IRSA는 어드미션 웹훅과 신뢰 정책이 맞물려야 동작한다

IRSA를 "SA에 annotation만 달면 되는 것"으로 이해하면, 정작 annotation이 무시되거나 AWS가 거부하는 순간 원인을 못 찾습니다. IRSA는 K8s와 AWS 두 층이 파드 생성 시점에 맞물려 돌아가는 구조입니다.

파드가 생성되면 EKS Pod Identity 뮤테이팅 어드미션 웹훅이 그 파드의 ServiceAccount를 봅니다. SA에 eks.amazonaws.com/role-arn annotation이 있으면, 웹훅이 파드에 세 가지를 주입합니다.

  • AWS_ROLE_ARN 환경변수 — assume할 IAM 역할.
  • AWS_WEB_IDENTITY_TOKEN_FILE 환경변수 — projected 토큰의 경로.
  • audience가 sts.amazonaws.comprojected service account token 볼륨.

그러면 AWS SDK가 이 토큰으로 STS AssumeRoleWithWebIdentity를 호출하고, STS는 토큰의 발급자(iss)를 클러스터의 IAM OIDC 제공자와 대조한 뒤, 대상 IAM 역할의 신뢰 정책(trust policy) 조건을 검사합니다 — subsystem:serviceaccount:<네임스페이스>:<SA이름>과 같고 audsts.amazonaws.com인지. 여기서 두 가지 실무 함정이 나옵니다.

  • 주입은 파드 admission 시점에만 일어난다: 그래서 실행 중인 SA에 annotation을 나중에 달면 기존 파드엔 소급되지 않습니다. 파드는 주입이 없어 SDK가 노드 인스턴스 역할로 폴백하고, 겉보기엔 조용히 "잘못된 권한"으로 돕니다. 반드시 파드를 재생성해야 합니다.
  • 신뢰 정책 조건이 정확히 맞아야 한다: sub의 네임스페이스·SA 이름이 실제와 한 글자라도 다르거나 aud가 어긋나면, K8s 쪽은 완벽해도 STS가 AccessDenied를 돌려줍니다.

상황: EKS에서 파드가 S3를 읽어야 해 SA에 eks.amazonaws.com/role-arn annotation을 달았습니다. 그런데 파드는 여전히 AccessDenied를 받거나, aws sts get-caller-identity를 찍어 보면 의도한 역할이 아니라 노드의 인스턴스 IAM 역할이 나옵니다.

원인: 두 갈래입니다. (1) annotation을 실행 중인 파드에 나중에 달았고 파드를 재생성하지 않았다면, 뮤테이팅 웹훅이 주입할 기회가 없어 SDK가 노드 역할로 폴백합니다 — 이 경우 get-caller-identity에 노드 역할이 찍힙니다. (2) 주입은 됐는데(환경변수가 보임) IAM 역할 신뢰 정책의 sub·aud 조건이 실제 네임스페이스·SA와 불일치하면 STS가 토큰을 거부해 AccessDenied가 납니다.

진단: 먼저 파드 안에서 env | grep AWS_ROLE_ARN으로 주입 여부를 봅니다. 없으면 (1) — 파드를 재생성합니다. 있으면 (2) — aws sts get-caller-identity의 에러가 AssumeRoleWithWebIdentity의 AccessDenied인지 확인하고, IAM 역할 신뢰 정책의 조건을 실제 system:serviceaccount:<네임스페이스>:<SA이름>, aud=sts.amazonaws.com과 대조합니다.

해결: (1)이면 kubectl rollout restart로 파드를 재생성해 웹훅 주입을 유발합니다(annotation은 SA에, 파드는 그 SA를 쓰도록). (2)이면 IAM 역할 신뢰 정책의 StringEquals(또는 StringLike) 조건을 올바른 네임스페이스·SA·audience로 수정합니다. K8s 토큰 만료(앞 TroubleCase)와 달리, 이 문제는 K8s가 아니라 AWS 신뢰 관계에서 풀린다는 점을 구분하는 것이 핵심입니다(RBAC(Role-based Access Control) 기반 다중 사용자 보안).

💼
실무 맥락
현업 패턴

시나리오: 모니터링 에이전트를 파드로 배포하는 상황

SRE 팀 막내로서 "클러스터 내 모든 파드의 상태를 수집해 Slack에 보내는 모니터링 에이전트를 배포해줘"라는 요청을 받았습니다. 에이전트가 K8s API로 파드 목록을 조회하고 AWS Secrets Manager에서 Slack Webhook URL을 가져와야 합니다.

로컬 터미널
# 1. 필요 권한 목록 작성 (최소 권한 원칙)
# K8s: 클러스터 전체 파드 읽기 (ClusterRole 필요)
# AWS: Secrets Manager 특정 시크릿 읽기

# 2. ClusterRole 생성 (네임스페이스 경계 없이 파드 조회)
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-monitor
rules:
  - apiGroups: [""]
    resources: ["pods", "nodes"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list"]
EOF

# 3. ServiceAccount + IRSA 생성
eksctl create iamserviceaccount \
  --cluster production \
  --namespace monitoring \
  --name pod-monitor-sa \
  --attach-policy-arn arn:aws:iam::123456789012:policy/SlackSecretReadOnly \
  --approve

# 4. ClusterRoleBinding
kubectl create clusterrolebinding pod-monitor-binding \
  --clusterrole=pod-monitor \
  --serviceaccount=monitoring:pod-monitor-sa

# 5. 배포
kubectl run pod-monitor \
  --image=myrepo/pod-monitor:v1.2 \
  --namespace=monitoring \
  --serviceaccount=pod-monitor-sa

실무 포인트: ClusterRole은 필요한 경우에만 사용하고, 가능하면 네임스페이스 범위의 Role을 선호합니다. 모니터링 에이전트처럼 클러스터 전체를 봐야 하는 경우가 ClusterRole의 정당한 사용 사례입니다. IRSA를 사용하면 Access Key 없이도 AWS 리소스에 안전하게 접근할 수 있습니다.

핵심 요약

개념명령/설정실무 사용 빈도
SA 조회kubectl get sa자주
전용 SA 생성kubectl create sa <name>서비스 배포 시
토큰 수동 발급kubectl create token <sa>디버깅 시
토큰 마운트 경로/var/run/secrets/kubernetes.io/serviceaccount/API 호출 시
토큰 비활성화automountServiceAccountToken: false일반 파드에
IRSA 설정eks.amazonaws.com/role-arn annotationEKS + AWS 연동
권한 확인kubectl auth can-i <verb> <resource> --as=system:serviceaccount:<ns>:<sa>RBAC 디버깅
실습 단계
1

전용 ServiceAccount 생성 및 확인

kubectl create namespace sa-demo kubectl create serviceaccount deployment-scaler -n sa-demo kubectl get serviceaccount -n sa-demo

예상 출력

NAME                 SECRETS   AGE
default              0         5s
deployment-scaler    0         2s
2

Role 및 RoleBinding 생성

kubectl create role deployment-scaler-role \ --verb=get,list,patch,update \ --resource=deployments \ -n sa-demo kubectl create rolebinding deployment-scaler-binding \ --role=deployment-scaler-role \ --serviceaccount=sa-demo:deployment-scaler \ -n sa-demo

예상 출력

role.rbac.authorization.k8s.io/deployment-scaler-role created
rolebinding.rbac.authorization.k8s.io/deployment-scaler-binding created
3

ServiceAccount 권한 확인

kubectl auth can-i list deployments \ --as=system:serviceaccount:sa-demo:deployment-scaler \ -n sa-demo kubectl auth can-i delete pods \ --as=system:serviceaccount:sa-demo:deployment-scaler \ -n sa-demo

예상 출력

yes
no
4

파드에 ServiceAccount 지정 후 토큰 마운트 확인

kubectl run scaler-pod \ --image=curlimages/curl:latest \ --serviceaccount=deployment-scaler \ --namespace=sa-demo \ --command -- sleep 3600 kubectl exec -it scaler-pod -n sa-demo -- ls /var/run/secrets/kubernetes.io/serviceaccount/

예상 출력

ca.crt    namespace    token
🔍실행 후 확인할 것
  • kubectl get sa -n <namespace> 에서 생성한 ServiceAccount가 목록에 있어야 함 — 없으면 kubectl apply 또는 create 명령이 올바른 네임스페이스에 실행됐는지 확인
  • kubectl exec -it <pod> -- ls /var/run/secrets/kubernetes.io/serviceaccount/ 에서 ca.crt, namespace, token 세 파일이 있으면 토큰 정상 마운트. 디렉토리가 없으면 automountServiceAccountToken: false 설정 확인
  • kubectl auth can-i list pods --as=system:serviceaccount:<ns>:<sa> -n <ns> 결과가 yes 이면 권한 바인딩 성공, no 이면 RoleBinding 또는 ClusterRoleBinding 누락
  • 403 Forbidden 발생 시: kubectl exec 에서 API 호출 후 에러 메시지에 포함된 "cannot <verb> resource" 부분이 Role에 추가해야 할 정확한 권한 — verbs와 resources 매핑 확인
  • IRSA 동작 확인: kubectl exec -it <pod> -- aws sts get-caller-identity 에서 Arn 필드에 role-arn annotation의 IAM 역할이 보이면 IRSA 정상. AssumeRole 에러면 OIDC 제공자 등록 또는 Trusted Relationship 설정 오류
  • 조합 해석 — 401 Unauthorized + 토큰 파일은 있음 → 토큰 만료 가능성 높음. token 파일 내 JWT payload의 exp 필드(base64 디코딩)를 확인하고, K8s 공식 SDK로 전환하여 자동 갱신 처리

명령어·단축키 빠른 참조

이 모듈에서 파드 신원(SA)·토큰·IRSA를 다룰 때 쓴 kubectl/aws 명령을 모았습니다.

명령어/단축키용도자주 쓰는 예
kubectl get saServiceAccount 목록 조회kubectl get sa -n <ns>
kubectl create serviceaccount전용 SA 생성kubectl create serviceaccount deployment-scaler -n <ns>
kubectl create tokenSA 토큰 수동 발급(디버깅)kubectl create token <sa> -n <ns> --duration=24h
kubectl auth can-i --asSA 실효 권한 확인kubectl auth can-i list pods --as=system:serviceaccount:<ns>:<sa> -n <ns>
kubectl run --serviceaccountSA 지정해 파드 실행kubectl run p --image=curlimages/curl --serviceaccount=<sa> --command -- sleep 3600
kubectl exec … ls토큰 마운트 여부 확인kubectl exec -it X -- ls /var/run/secrets/kubernetes.io/serviceaccount/
env | grep AWS_ROLE_ARNIRSA 웹훅 주입 여부 확인값이 없으면 파드 재생성 필요(주입은 생성 시점에만)
aws sts get-caller-identityIRSA 역할 assume 검증kubectl exec -it X -- aws sts get-caller-identity (Arn 확인)
kubectl rollout restartannotation 후 웹훅 재주입kubectl rollout restart deployment/X
JWT exp 디코딩토큰 만료 확인(401)… token | cut -d. -f2 | base64 -dexp 확인

관련 모듈로 더 깊이:

다음 모듈 network-policy에서는 ServiceAccount에서 정의한 신원(ID)을 기반으로 파드 간 네트워크 트래픽을 제어하는 방법을 다룹니다. Ingress/Egress 규칙으로 허용된 서비스끼리만 통신하게 만들고, Deny-All 정책으로 보안 경계를 구성하는 실습을 진행합니다.

지식 확인

퀴즈 — 8문제

Q1

파드에 별도 ServiceAccount를 지정하지 않으면 어떤 계정이 사용되나요?

Q2

automountServiceAccountToken: false 설정이 필요한 상황은?

Q3

IRSA(IAM Roles for Service Accounts)의 핵심 동작 원리는?

Q4

파드 내부에서 K8s API 서버 주소를 확인하는 환경 변수는?

Q5

파드가 K8s API를 호출할 필요가 없는데도 기본적으로 ServiceAccount 토큰이 마운트된다. 보안상 권장 조치는?

Q6

CI 파이프라인용 ServiceAccount에 권한을 줄 때 올바른 원칙은?

Q7

[심화] 이미 실행 중인 파드의 ServiceAccount에 eks.amazonaws.com/role-arn annotation을 추가했는데도 파드가 여전히 IRSA 역할을 못 쓴다. 원리상 이유는?

Q8

[심화] IRSA를 설정한 파드에서 aws sts get-caller-identity가 AssumeRoleWithWebIdentity AccessDenied로 실패한다. 파드는 재생성했고 AWS_ROLE_ARN 환경변수도 보인다. 다음으로 확인할 것은?

0 / 8 답변

🧪 실습으로 확인하기

Kubernetes RBAC — "Forbidden" 진단과 최소 권한 부여

중급

CI의 서비스 계정이 "forbidden"으로 막혔다. RBAC은 주체(ServiceAccount)–역할(Role/ClusterRole)–바인딩(RoleBinding)의 연결이다. forbidden 메시지를 해독하고(누가·어떤 동작·어떤 리소스·어떤 네임스페이스), can-i로 권한을 확인하고, 필요한 동작만 담은 Role을 만들어 바인딩한다. cluster-admin 남발 없이 최소 권한으로 푼다.

45📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요