클라이언트 인증서
사람(관리자)용.
kubeadm의 admin.conf가 이것이다.
누가 무엇을 할 수 있는가
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 같은 것은 존재하지 않는다그래서 **“사용자를 만든다”는 곧 “인증서를 발급한다”**는 뜻이다.
키와 CSR 생성 — CN이 사용자, O가 그룹이다
openssl genrsa -out dev.key 2048openssl req -new -key dev.key -out dev.csr -subj "/CN=dev/O=developers"# ^사용자 ^그룹Kubernetes CSR 오브젝트로 제출
cat <<EOF | kubectl apply -f -apiVersion: certificates.k8s.io/v1kind: CertificateSigningRequestmetadata: name: devspec: request: $(cat dev.csr | base64 | tr -d '\n') signerName: kubernetes.io/kube-apiserver-client expirationSeconds: 86400 usages: ["client auth"]EOF승인
kubectl get csrkubectl certificate approve devkubectl certificate deny dev # 거부할 때서명된 인증서 꺼내기
kubectl get csr dev -o jsonpath='{.status.certificate}' | base64 -d > dev.crtsequenceDiagram
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
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-ctxkubectl auth whoami # 내가 누구로 인식되는지kubectl create sa deploy-botkubectl get sakubectl describe sa deploy-botspec: 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가 자동으로 만들어진다default가 붙는다/var/run/secrets/kubernetes.io/serviceaccount/ 에 마운트된다kubectl exec -it web -- ls /var/run/secrets/kubernetes.io/serviceaccount/# ca.crt namespace tokenAPI를 호출하지 않는 앱이라면 automountServiceAccountToken: false가 안전하다.
# 수동으로 토큰 발급 (기본 1시간)kubectl create token deploy-botkubectl create token deploy-bot --duration=24h예전에는 SA를 만들면 만료 없는 토큰 Secret이 자동으로 생겼다. 지금도 꼭 필요하면 직접 만들 수 있다.
apiVersion: v1kind: Secretmetadata: name: deploy-bot-token annotations: kubernetes.io/service-account.name: deploy-bottype: kubernetes.io/service-account-tokenCI/CD 등 클러스터 밖에서 쓸 토큰이 이 경우다. 다만 수명 없는 자격증명이라 관리가 필요하다.
핵심 구조는 하나다 — 주체 → 바인딩 → 역할 → 리소스.
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(클러스터 전체)
NetworkPolicy와 같은 철학이다 — “막는다”가 아니라 “허용하지 않는다”.
apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: pod-reader namespace: devrules: - 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"]kubectl create role pod-reader --verb=get,list,watch --resource=pods -n devkubectl 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 |
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/v1kind: RoleBindingmetadata: name: dev-can-read-pods namespace: devsubjects: - 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: devroleRef: kind: Role # Role 또는 ClusterRole name: pod-reader apiGroup: rbac.authorization.k8s.iokubectl create rolebinding dev-can-read-pods --role=pod-reader --user=dev -n devkubectl create rolebinding sa-binding --role=pod-reader --serviceaccount=dev:deploy-bot -n devkubectl create clusterrole node-reader --verb=get,list,watch --resource=nodeskubectl create clusterrolebinding ops-nodes --clusterrole=node-reader --user=opsClusterRole이 필요한 경우
클러스터 스코프 리소스
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으로 묶으면 권한 범위는 그 네임스페이스로 좁혀진다.
kubectl get clusterroleskubectl describe clusterrole viewflowchart 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은 못 본다 |
kubectl create clusterrolebinding me-admin --clusterrole=cluster-admin --user=alicekubectl create rolebinding dev-edit --clusterrole=edit --user=bob -n devsystem: 접두사가 붙은 것들은 컴포넌트용이다 (system:node, system:kube-scheduler …).
수정하지 말 것.
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: 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에 규칙이 자동으로 합쳐진다admin/edit/view의 aggregationRule이 이 라벨을 수집한다kubectl get clusterrole view -o yaml | grep -A5 aggregationRulekubectl auth can-i create deployments -n devkubectl auth can-i delete pods --all-namespaceskubectl auth can-i '*' '*' # 클러스터 관리자인가
# 남의 권한을 대신 확인 (impersonation)kubectl auth can-i list secrets -n dev --as=devkubectl auth can-i get pods --as=system:serviceaccount:dev:deploy-bot -n dev
# 내가 가진 권한 전부kubectl auth can-i --list -n devkubectl auth whoami# 임시 kubeconfig로 테스트kubectl --as=dev get pods -n devkubectl --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 |
# 활성 목록 확인sudo grep enable-admission-plugins /etc/kubernetes/manifests/kube-apiserver.yaml중요한 내장 플러그인
NamespaceLifecycle — Terminating 네임스페이스에 생성 금지LimitRanger / ResourceQuota — 6장PodSecurity — 7장DefaultStorageClass — PVC에 기본 클래스를 채운다MutatingAdmissionWebhook / ValidatingAdmissionWebhook — 외부 정책 엔진 연동에러 메시지를 정확히 읽는다 — 필요한 정보가 다 있다
Error from server (Forbidden): pods is forbidden: User "dev" cannot list resource "pods" in API group "" in the namespace "prod"실제로 안 되는지 확인
kubectl auth can-i list pods -n prod --as=dev바인딩이 있는지
kubectl get rolebinding,clusterrolebinding -A -o wide | grep dev역할의 내용 확인
kubectl describe role pod-reader -n prodkubectl describe clusterrole viewflowchart 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를 고치려 했다 |
변경 불가 — 지우고 다시 |
certificate approve → 인증서 추출 → kubeconfigkubectl create tokenpods 권한이 있어도 logs는 pods/log가 따로 필요하다kubectl auth can-i --as=...