콘텐츠로 이동

14. 인증 · 인가 · RBAC

누가 무엇을 할 수 있는가

flowchart LR
    R["요청"] --> A["① Authentication<br/>너는 누구인가"]
    A -->|"실패 401"| X1["Unauthorized"]
    A --> Z["② Authorization<br/>해도 되는가 · RBAC"]
    Z -->|"실패 403"| X2["Forbidden"]
    Z --> M["③ Admission<br/>이 내용이 규칙에 맞는가"]
    M -->|"위반"| X3["다양한 에러"]
    M --> E[("etcd 저장")]

    classDef authn fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef authz fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef adm fill:#ede9fe,stroke:#7c3aed,color:#4c1d95
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    class A authn
    class Z authz
    class M adm
    class X1,X2,X3 bad
    class E ok
단계 실패 시
인증 401 Unauthorized 신원을 확인할 수 없다
인가 403 Forbidden 신원은 확인됐지만 권한이 없다
admission 다양한 에러 정책·검증에 걸렸다

에러 문장을 RBAC 필드로 옮기면 그대로 답이 된다.

flowchart LR
    M["User dev cannot list resource pods<br/>in API group  in the namespace prod"]
    M --> S["dev → subjects.name"]
    M --> V["list → rules.verbs"]
    M --> RS["pods → rules.resources"]
    M --> G["'' → rules.apiGroups"]
    M --> NS["prod → RoleBinding 의 namespace"]

    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class S,V,RS,G,NS key
    class M mute

클라이언트 인증서

사람(관리자)용. kubeadm의 admin.conf가 이것이다.

ServiceAccount 토큰

Pod 안의 워크로드용. JWT.

OIDC

사람 · 조직 SSO. 실무 표준.

Webhook

외부 시스템에 위임. EKS의 IAM 연동 등.

Bootstrap 토큰은 노드 조인 전용이다 (kubeadm join 때).

중요: Kubernetes에는 “User” 오브젝트가 없다.

flowchart LR
    CRT["클라이언트 인증서"] --> CN["CN 필드<br/>= 사용자 이름"]
    CRT --> O["O 필드<br/>= 그룹"]
    CN --> API["API 서버가 문자열로 인식"]
    O --> API
    NO["kubectl get users"] -.->|"그런 명령은 없다 ❌"| X["User 오브젝트 부재"]

    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class CN,O key
    class X bad
    class CRT,API,NO mute
  • kubectl get users 같은 것은 존재하지 않는다
  • 사용자는 인증서의 CN 필드OIDC 토큰의 클레임에서 문자열로 나온다
  • 그룹은 인증서의 O(Organization) 필드에서 나온다

그래서 **“사용자를 만든다”는 곧 “인증서를 발급한다”**는 뜻이다.

  1. 키와 CSR 생성CN이 사용자, O가 그룹이다

    Terminal window
    openssl genrsa -out dev.key 2048
    openssl req -new -key dev.key -out dev.csr -subj "/CN=dev/O=developers"
    # ^사용자 ^그룹
  2. Kubernetes CSR 오브젝트로 제출

    Terminal window
    cat <<EOF | kubectl apply -f -
    apiVersion: certificates.k8s.io/v1
    kind: CertificateSigningRequest
    metadata:
    name: dev
    spec:
    request: $(cat dev.csr | base64 | tr -d '\n')
    signerName: kubernetes.io/kube-apiserver-client
    expirationSeconds: 86400
    usages: ["client auth"]
    EOF
  3. 승인

    Terminal window
    kubectl get csr
    kubectl certificate approve dev
    kubectl certificate deny dev # 거부할 때
  4. 서명된 인증서 꺼내기

    Terminal window
    kubectl get csr dev -o jsonpath='{.status.certificate}' | base64 -d > dev.crt
sequenceDiagram
    participant U as 사용자
    participant K as kubectl
    participant API as kube-apiserver
    participant CA as 클러스터 CA
    U->>U: openssl 로 key + csr 생성 (CN=dev, O=developers)
    U->>API: CertificateSigningRequest 제출
    Note over API: status: Pending
    K->>API: kubectl certificate approve dev
    API->>CA: 서명 요청
    CA-->>API: 서명된 인증서
    API-->>U: status.certificate (base64)
    U->>U: kubeconfig 에 등록
    Note over U: 여기까지는 인증만 · 권한은 아직 0
Terminal window
kubectl config set-credentials dev \
--client-certificate=dev.crt \
--client-key=dev.key \
--embed-certs=true
kubectl config set-context dev-ctx \
--cluster=kubernetes \
--user=dev \
--namespace=dev
kubectl config use-context dev-ctx
kubectl auth whoami # 내가 누구로 인식되는지
Terminal window
kubectl create sa deploy-bot
kubectl get sa
kubectl describe sa deploy-bot
spec:
serviceAccountName: deploy-bot
automountServiceAccountToken: false # 토큰을 안 넣는다
flowchart LR
    SA["ServiceAccount<br/>deploy-bot"] --> POD["Pod<br/>serviceAccountName: deploy-bot"]
    POD --> VOL["projected 볼륨<br/>/var/run/secrets/kubernetes.io/serviceaccount/"]
    VOL --> F1["token"]
    VOL --> F2["ca.crt"]
    VOL --> F3["namespace"]
    F1 --> API["kube-apiserver 호출 시<br/>Bearer 토큰으로"]
    KL["kubelet"] -.->|"만료 전 자동 갱신"| F1

    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class SA,F1 key
    class POD,VOL,F2,F3,API,KL mute
  • 모든 네임스페이스에 default SA가 자동으로 만들어진다
  • Pod에 SA를 지정하지 않으면 default가 붙는다
  • 토큰은 /var/run/secrets/kubernetes.io/serviceaccount/ 에 마운트된다
Terminal window
kubectl exec -it web -- ls /var/run/secrets/kubernetes.io/serviceaccount/
# ca.crt namespace token

API를 호출하지 않는 앱이라면 automountServiceAccountToken: false가 안전하다.

  • TokenRequest API수명이 있는 토큰이 projected 볼륨으로 주입된다
  • kubelet이 만료 전에 자동으로 갱신한다
Terminal window
# 수동으로 토큰 발급 (기본 1시간)
kubectl create token deploy-bot
kubectl create token deploy-bot --duration=24h

핵심 구조는 하나다 — 주체 → 바인딩 → 역할 → 리소스.

flowchart LR
    subgraph SUBJ["주체 (subjects)"]
      U["User<br/>인증서 CN"]
      G["Group<br/>인증서 O"]
      S["ServiceAccount"]
    end
    subgraph BIND["연결 (Binding)"]
      RB["RoleBinding<br/>네임스페이스 안"]
      CRB["ClusterRoleBinding<br/>클러스터 전체"]
    end
    subgraph ROLE["권한 정의 (Role)"]
      R["Role<br/>네임스페이스 안"]
      CR["ClusterRole<br/>클러스터 전체"]
    end
    RES["리소스 + 동사<br/>pods · get,list,watch"]

    U --> RB
    G --> RB
    S --> RB
    U --> CRB
    RB -->|roleRef| R
    RB -->|roleRef| CR
    CRB -->|roleRef| CR
    R --> RES
    CR --> RES

    classDef subj fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef bind fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef role fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class U,G,S subj
    class RB,CRB bind
    class R,CR role
    class RES mute

권한을 정의한다Role(네임스페이스 안) · ClusterRole(클러스터 전체)
권한을 부여한다RoleBinding(네임스페이스 안) · ClusterRoleBinding(클러스터 전체)

  • RBAC은 누적(additive) 이다. 거부 규칙이 없다
  • 어느 규칙에도 안 걸리면 기본 거부

NetworkPolicy와 같은 철학이다 — “막는다”가 아니라 “허용하지 않는다”.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: dev
rules:
- apiGroups: [""] # "" = core 그룹 (Pod, Service, ConfigMap …)
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "update", "patch"]
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-secret"] # 특정 이름만
verbs: ["get"]
Terminal window
kubectl create role pod-reader --verb=get,list,watch --resource=pods -n dev
kubectl create role pod-reader --verb=get --resource=pods --resource-name=web -n dev

주요 verb

verb HTTP
get GET (단일)
list GET (목록)
watch GET (스트림)
create POST
update PUT
patch PATCH
delete DELETE
deletecollection DELETE (다수)
* 전부

apiGroups

리소스
"" Pod, Service, ConfigMap, Secret, Node, PV…
"apps" Deployment, ReplicaSet, StatefulSet, DaemonSet
"batch" Job, CronJob
"networking.k8s.io" Ingress, NetworkPolicy
"rbac.authorization.k8s.io" Role, RoleBinding
"storage.k8s.io" StorageClass, CSIDriver
Terminal window
kubectl api-resources # APIVERSION 열에서 그룹을 확인
kubectl api-resources --api-group=apps

일부 동작은 별도의 하위 리소스로 취급된다.

flowchart LR
    P["pods 에 get·list 권한"] --> OK["kubectl get pods ✅"]
    P -.->|"이것만으로는 안 된다 ❌"| L["kubectl logs"]
    L --> NEED1["pods/log 가 따로 필요"]
    P -.->|"안 된다 ❌"| E["kubectl exec"]
    E --> NEED2["pods/exec · verb 는 create"]

    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    class OK ok
    class L,E bad
    class NEED1,NEED2 key
하고 싶은 것 필요한 리소스
kubectl logs pods/log
kubectl exec pods/exec (verb는 create)
kubectl port-forward pods/portforward
kubectl scale deploy deployments/scale
Pod 상태 갱신 pods/status
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"] # exec은 create 동사다
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-can-read-pods
namespace: dev
subjects:
- kind: User # 인증서의 CN
name: dev
apiGroup: rbac.authorization.k8s.io
- kind: Group # 인증서의 O
name: developers
apiGroup: rbac.authorization.k8s.io
- kind: ServiceAccount # SA는 apiGroup을 쓰지 않는다
name: deploy-bot
namespace: dev
roleRef:
kind: Role # Role 또는 ClusterRole
name: pod-reader
apiGroup: rbac.authorization.k8s.io
Terminal window
kubectl create rolebinding dev-can-read-pods --role=pod-reader --user=dev -n dev
kubectl create rolebinding sa-binding --role=pod-reader --serviceaccount=dev:deploy-bot -n dev
Terminal window
kubectl create clusterrole node-reader --verb=get,list,watch --resource=nodes
kubectl create clusterrolebinding ops-nodes --clusterrole=node-reader --user=ops

ClusterRole이 필요한 경우

클러스터 스코프 리소스

Node, PersistentVolume, StorageClass, Namespace, ClusterRole — 네임스페이스가 없는 것들.

여러 네임스페이스 재사용

같은 권한 정의를 여러 곳에서 RoleBinding으로 붙여 쓴다.

비(非)리소스 URL

/healthz, /metrics 같은 리소스가 아닌 경로.

rules:
- nonResourceURLs: ["/healthz", "/metrics"]
verbs: ["get"]
flowchart TD
    Q1{"roleRef 가 무엇인가"}
    Q1 -->|Role| Q2{"바인딩 종류"}
    Q1 -->|ClusterRole| Q3{"바인딩 종류"}
    Q2 -->|RoleBinding| A1["그 네임스페이스 안에서만 ✅"]
    Q2 -->|ClusterRoleBinding| A2["불가능 · 허용되지 않는다 ❌"]
    Q3 -->|RoleBinding| A3["그 네임스페이스 안에서만<br/>★ 권한 정의를 재사용 ✅"]
    Q3 -->|ClusterRoleBinding| A4["모든 네임스페이스<br/>+ 클러스터 스코프 ✅"]

    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef star fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class A1,A4 ok
    class A3 star
    class A2 bad
    class Q1,Q2,Q3 mute
roleRef 바인딩 종류 결과
Role RoleBinding 그 네임스페이스 안에서만
ClusterRole RoleBinding 그 네임스페이스 안에서만 (권한 정의를 재사용)
ClusterRole ClusterRoleBinding 모든 네임스페이스 + 클러스터 스코프
Role ClusterRoleBinding 불가능 — 허용되지 않는다

세 번째 줄(파란 칸)이 핵심이다. ClusterRole을 RoleBinding으로 묶으면 권한 범위는 그 네임스페이스로 좁혀진다.

Terminal window
kubectl get clusterroles
kubectl describe clusterrole view
flowchart LR
    CA["cluster-admin<br/>전부 · * on *"] --> AD["admin<br/>네임스페이스 안의 거의 전부"]
    AD --> ED["edit<br/>대부분 읽기·쓰기<br/>RBAC 은 못 만짐"]
    ED --> VI["view<br/>읽기 전용<br/>Secret 은 못 봄"]

    classDef top fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef mid fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef low fill:#dcfce7,stroke:#16a34a,color:#14532d
    class CA top
    class AD,ED mid
    class VI low
이름 권한
cluster-admin 전부. * on *
admin 네임스페이스 안의 거의 전부 (ResourceQuota·Namespace 자체는 제외)
edit 대부분의 리소스 읽기·쓰기. RBAC은 못 만진다
view 읽기 전용. Secret은 못 본다
Terminal window
kubectl create clusterrolebinding me-admin --clusterrole=cluster-admin --user=alice
kubectl create rolebinding dev-edit --clusterrole=edit --user=bob -n dev

system: 접두사가 붙은 것들은 컴포넌트용이다 (system:node, system:kube-scheduler …). 수정하지 말 것.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring
labels:
rbac.authorization.k8s.io/aggregate-to-view: "true" # view에 자동 합쳐진다
rules:
- apiGroups: ["monitoring.coreos.com"]
resources: ["prometheuses"]
verbs: ["get", "list", "watch"]
flowchart LR
    NEW["새 ClusterRole<br/>라벨: aggregate-to-view=true"] --> AGG["view 의 aggregationRule 이<br/>이 라벨을 수집"]
    AGG --> VIEW["ClusterRole view<br/>규칙이 자동으로 합쳐진다"]
    VIEW --> USERS["기존 view 사용자 전원에게<br/>즉시 반영 ✅"]

    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class NEW key
    class USERS ok
    class AGG,VIEW mute
  • 라벨을 붙이면 기존 view/edit/admin에 규칙이 자동으로 합쳐진다
  • CRD를 설치할 때 기본 역할에 권한을 얹는 표준 방법이다
  • admin/edit/viewaggregationRule이 이 라벨을 수집한다
Terminal window
kubectl get clusterrole view -o yaml | grep -A5 aggregationRule
Terminal window
kubectl auth can-i create deployments -n dev
kubectl auth can-i delete pods --all-namespaces
kubectl auth can-i '*' '*' # 클러스터 관리자인가
# 남의 권한을 대신 확인 (impersonation)
kubectl auth can-i list secrets -n dev --as=dev
kubectl auth can-i get pods --as=system:serviceaccount:dev:deploy-bot -n dev
# 내가 가진 권한 전부
kubectl auth can-i --list -n dev
kubectl auth whoami
Terminal window
# 임시 kubeconfig로 테스트
kubectl --as=dev get pods -n dev
kubectl --as=dev --as-group=developers get pods -n dev
# SA 토큰으로 Pod 안에서
kubectl run tmp --image=curlimages/curl --rm -it --restart=Never \
--overrides='{"spec":{"serviceAccountName":"deploy-bot"}}' -- sh
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -s --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $TOKEN" \
https://kubernetes.default.svc/api/v1/namespaces/dev/pods

인가를 통과한 뒤 내용을 검사하거나 고치는 단계다.

flowchart LR
    Z["인가 통과"] --> MU["Mutating<br/>요청을 고친다"]
    MU --> VA["Validating<br/>거부하거나 통과시킨다"]
    VA --> E[("etcd")]
    MU -.-> M1["DefaultStorageClass<br/>ServiceAccount 주입"]
    VA -.-> V1["ResourceQuota · PodSecurity<br/>LimitRanger"]
    VA -->|"위반"| X["거부"]

    classDef mut fill:#ede9fe,stroke:#7c3aed,color:#4c1d95
    classDef val fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class MU,M1 mut
    class VA,V1 val
    class X bad
    class Z,E mute
종류 하는 일
Mutating 요청을 고친다 DefaultStorageClass, ServiceAccount 주입
Validating 거부하거나 통과시킨다 ResourceQuota, PodSecurity, LimitRanger
Terminal window
# 활성 목록 확인
sudo grep enable-admission-plugins /etc/kubernetes/manifests/kube-apiserver.yaml

중요한 내장 플러그인

  • NamespaceLifecycle — Terminating 네임스페이스에 생성 금지
  • LimitRanger / ResourceQuota — 6장
  • PodSecurity — 7장
  • DefaultStorageClass — PVC에 기본 클래스를 채운다
  • MutatingAdmissionWebhook / ValidatingAdmissionWebhook — 외부 정책 엔진 연동
  1. 에러 메시지를 정확히 읽는다 — 필요한 정보가 다 있다

    Error from server (Forbidden): pods is forbidden:
    User "dev" cannot list resource "pods" in API group "" in the namespace "prod"
  2. 실제로 안 되는지 확인

    Terminal window
    kubectl auth can-i list pods -n prod --as=dev
  3. 바인딩이 있는지

    Terminal window
    kubectl get rolebinding,clusterrolebinding -A -o wide | grep dev
  4. 역할의 내용 확인

    Terminal window
    kubectl describe role pod-reader -n prod
    kubectl describe clusterrole view
flowchart TD
    S{"403 이 났다"}
    S --> C1{"바인딩이 대상<br/>네임스페이스에 있는가"}
    C1 -->|없다| F1["RoleBinding 은 대상 네임스페이스에<br/>있어야 한다"]
    C1 -->|있다| C2{"apiGroup 이 맞는가"}
    C2 -->|"틀렸다"| F2["apps vs 빈 문자열<br/>Deployment 는 apps"]
    C2 -->|맞다| C3{"하위 리소스가<br/>필요한 동작인가"}
    C3 -->|"그렇다"| F3["pods/log · pods/exec 를 추가"]
    C3 -->|아니다| C4{"주체 표기가 맞는가"}
    C4 -->|"SA 인데 형식이 틀림"| F4["system:serviceaccount:ns:name"]
    C4 -->|맞다| F5["roleRef 를 고치려 했는가<br/>→ 변경 불가 · 지우고 다시"]

    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class F1,F2,F3,F4,F5 bad
    class S,C1,C2,C3,C4 mute
흔한 원인 확인
바인딩의 네임스페이스가 다르다 RoleBinding은 대상 네임스페이스에 있어야 한다
apiGroup 오타 apps vs "" — Deployment는 apps
하위 리소스 누락 pods/log, pods/exec
SA 이름 형식 system:serviceaccount:ns:name
roleRef를 고치려 했다 변경 불가 — 지우고 다시
  • 401은 인증, 403은 인가. 403 메시지에 주체·동사·리소스·네임스페이스가 다 있다
  • User 오브젝트는 없다. 사용자 = 인증서의 CN, 그룹 = O
  • 사용자 생성은 CSR 제출 → certificate approve → 인증서 추출 → kubeconfig
  • 인증서만으로는 아무 권한도 없다. 바인딩이 따로 필요하다
  • SA 토큰은 이제 수명 있는 projected 토큰이 기본. kubectl create token
  • Role은 권한 정의, Binding은 연결. 거부 규칙은 없고 기본 거부
  • ClusterRole + RoleBinding = 그 네임스페이스로 범위가 좁혀진다 ← 가장 유용한 조합
  • pods 권한이 있어도 logspods/log가 따로 필요하다
  • 검산은 kubectl auth can-i --as=...