RBAC และ Service Accounts
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”RBAC แยกการตัดสินใจเรื่องสิทธิ์ทุกครั้งออกเป็นสองส่วน คือ “อะไรที่อนุญาต” (Role หรือ ClusterRole) กับ “ใครได้สิทธิ์นั้น” (RoleBinding หรือ ClusterRoleBinding) และทุก Pod ทำงานผ่านตัวตนแบบ ServiceAccount — หนุนหลังด้วย token อายุสั้นแบบ bound ไม่ใช่ Secret แบบเก่าที่อยู่ยืน — ไม่ว่าคุณจะตั้งไว้เองหรือไม่ก็ตาม
Role กับ ClusterRole: อะไรที่อนุญาต
หัวข้อที่มีชื่อว่า “Role กับ ClusterRole: อะไรที่อนุญาต”RBAC (Role-Based Access Control) อยู่ใน API group rbac.authorization.k8s.io/v1 Role กำหนดชุดสิทธิ์ที่ scope อยู่ใน namespace เดียว — verb อย่าง get, list, watch, create, update, delete ที่ apply กับ resource เฉพาะ
apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: namespace: rbac-demo name: pod-readerrules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"]ClusterRole คือไอเดียเดียวกันแต่ scope ระดับ cluster — มันให้สิทธิ์เข้าถึง resource ระดับ cluster (อย่าง nodes หรือ namespaces) หรือให้สิทธิ์เข้าถึง resource ที่ namespace ในแบบที่ reuse ได้ในหลาย namespace ผ่าน RoleBinding หลายตัว
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: secret-readerrules: - apiGroups: [""] resources: ["secrets"] verbs: ["get", "list"]ทั้งสองอย่างไม่ได้ให้สิทธิ์อะไรด้วยตัวเอง — Role หรือ ClusterRole เป็นแค่คำอธิบายสิทธิ์ที่ลอยอยู่เฉย ๆ จนกว่าจะมีอะไรมา bind มันเข้ากับ subject
RoleBinding กับ ClusterRoleBinding: ใครได้สิทธิ์
หัวข้อที่มีชื่อว่า “RoleBinding กับ ClusterRoleBinding: ใครได้สิทธิ์”RoleBinding เชื่อม Role (หรือ ClusterRole ที่ reuse ในหนึ่ง namespace) เข้ากับ subject — User, Group, หรือ ServiceAccount — ภายใน namespace เดียว
apiVersion: v1kind: ServiceAccountmetadata: name: pod-reader-sa namespace: rbac-demo---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: pod-reader-binding namespace: rbac-demosubjects: - kind: ServiceAccount name: pod-reader-sa namespace: rbac-demoroleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.ioRoleBinding อ้างถึง ClusterRole ได้ด้วยในขณะที่ตัวเองยัง scope เป็น namespace — นี่คือวิธีมาตรฐานในการ reuse ชุดสิทธิ์ระดับ cluster หนึ่งชุด (เช่น view ClusterRole ที่ built-in) ในหลาย namespace โดยไม่ต้อง copy-paste rule ของ Role เดิมซ้ำ ๆ — binding ต่างหากที่ยัง namespace-scoped ไม่ใช่ role
ClusterRoleBinding ให้สิทธิ์ของ ClusterRole ทั่วทั้ง cluster ไม่ใช่แค่ namespace เดียว
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: read-all-secretssubjects: - kind: Group name: platform-team apiGroup: rbac.authorization.k8s.ioroleRef: kind: ClusterRole name: secret-reader apiGroup: rbac.authorization.k8s.ioพิสูจน์สิทธิ์จริง ไม่ใช่เดาจากการอ่าน YAML
หัวข้อที่มีชื่อว่า “พิสูจน์สิทธิ์จริง ไม่ใช่เดาจากการอ่าน YAML”คำสั่งสองตัวตอบว่า “ฉันคือใคร” และ “ฉันทำอะไรได้จริง” กับ API server ที่รันอยู่จริง ไม่ใช่แค่อ่าน rule บนกระดาษ
$ kubectl auth whoamiATTRIBUTE VALUEUsername kubernetes-adminGroups [kubeadm:cluster-admins system:authenticated]
$ kubectl auth whoami --as=system:serviceaccount:rbac-demo:pod-reader-saATTRIBUTE VALUEUsername system:serviceaccount:rbac-demo:pod-reader-saGroups [system:serviceaccounts system:serviceaccounts:rbac-demo system:authenticated]kubectl auth whoami เรียก API SelfSubjectReview และรายงานตัวตนกับ group membership ที่ API server สรุปให้ผู้เรียกจริง ๆ — รวมกับ --as มันยังยืนยันว่า impersonated identity resolve ออกมาเป็นอะไรแน่ ๆ ซึ่งมีประโยชน์ตอนที่ group ของ ServiceAccount (system:serviceaccounts:<namespace> โดยเฉพาะ) มีผลต่อ binding
$ kubectl auth can-i --list --as=system:serviceaccount:rbac-demo:pod-reader-sa -n rbac-demoResources Non-Resource URLs Resource Names Verbsselfsubjectreviews.authentication.k8s.io [] [] [create]selfsubjectaccessreviews.authorization.k8s.io [] [] [create]selfsubjectrulesreviews.authorization.k8s.io [] [] [create]pods [] [] [get list watch]--list พิมพ์ชุดกฎที่มีผลจริง ทั้งหมด ของตัวตนนั้นในครั้งเดียว — pods: [get list watch] ตรงนี้คือ rule ของ pod-reader Role เป๊ะ ๆ บวกกับ default ของ API server บางส่วนที่ทุกตัวตนที่ authenticated แล้วได้ (API selfsubject* ที่ kubectl auth whoami/can-i เองก็พึ่งพา)
403 จริง ไม่ใช่การเดา
หัวข้อที่มีชื่อว่า “403 จริง ไม่ใช่การเดา”$ kubectl delete pod nonexistent -n rbac-demo --as=system:serviceaccount:rbac-demo:pod-reader-saError from server (Forbidden): pods "nonexistent" is forbidden: User "system:serviceaccount:rbac-demo:pod-reader-sa" cannot delete resource "pods" in API group "" in the namespace "rbac-demo"
$ kubectl auth can-i delete pods --as=system:serviceaccount:rbac-demo:pod-reader-sa -n rbac-demonoAPI server reject request ด้วย 403 Forbidden จริง และระบุ verb, resource, API group, และ namespace ที่มันเช็คตรง ๆ — pod-reader-sa ลบ Pod จริง ๆ ไม่ได้ เพราะ Role ของมันให้แค่ get/list/watch พิสูจน์แล้วด้านบน
ServiceAccount: token แบบ bound อายุสั้น — ไม่ใช่ Secret แบบเก่า
หัวข้อที่มีชื่อว่า “ServiceAccount: token แบบ bound อายุสั้น — ไม่ใช่ Secret แบบเก่า”Kubernetes ไม่ได้จัดการ human user แบบ native — คนจะ authenticate ผ่าน identity provider ภายนอก (OIDC, cloud IAM integration, client certificate ฯลฯ) ที่ชั้น authentication ของ API server สิ่งที่ Kubernetes จัดการแบบ native จริง ๆ คือ ServiceAccount ตัวตนที่ process ข้างใน Pod ใช้
ทุก Pod รันในฐานะ ServiceAccount บางตัวเสมอ ถ้าไม่ระบุเลยมันรันเป็น ServiceAccount default ของ namespace ตั้งแต่ Kubernetes 1.24 การสร้าง ServiceAccount ไม่ auto-create Secret แบบ kubernetes.io/service-account-token ที่อายุยืนคู่กันอีกต่อไป — พิสูจน์: ServiceAccount ที่เพิ่งสร้างไม่มี Secret ติดมาเลย
$ kubectl create serviceaccount pod-reader-sa -n rbac-demoserviceaccount/pod-reader-sa created
$ kubectl get secrets -n rbac-demoNo resources found in rbac-demo namespace.แทนที่แบบนั้น token ทุกตัวที่ mount ถูก issue ผ่าน API TokenRequest เป็น bound service account token: อายุสั้น, scope ตาม audience, และผูกกับ object Pod ที่มันถูก issue ให้เจาะจง — API server invalidate มันได้ทันทีที่ Pod นั้นถูกลบ ซึ่ง token แบบ Secret ที่อยู่ยืนทำแบบนั้นไม่ได้เลย kubectl create token เรียก API เดียวกันนี้โดยตรง
$ kubectl create token pod-reader-sa -n rbac-demo --duration=10m --audience=custom-audeyJhbGciOiJSUzI1NiIs...
$ # decoding the JWT payload:{ "aud": ["custom-aud"], "exp": 1790418726, "iat": 1790418126, "kubernetes.io": { "namespace": "rbac-demo", "serviceaccount": {"name": "pod-reader-sa", "uid": "d373193f-..."} }, "sub": "system:serviceaccount:rbac-demo:pod-reader-sa"}exp เท่ากับ iat บวก --duration ที่ขอไว้โดยประมาณ (อายุจริง ไม่ใช่ “ตลอดไป”) และ aud scope ตรงกับ audience ที่ขอเป๊ะ ๆ — token ที่ mint มาสำหรับ custom-aud ใช้กับ API server (หรือ service อื่นใด) ที่คาด audience อื่นไม่ได้ token ที่ auto-mount ให้ Pod (นอกจาก automountServiceAccountToken: false จะปิดไว้ พูดถึงในบท Pod and Cluster Security) ก็เป็นกลไกเดียวกันนี้ project เข้าไปเป็น volume
$ kubectl get pod default-mount -n psa-demo -o jsonpath='{.spec.volumes}'[{"name":"kube-api-access-47t24","projected":{"defaultMode":420,"sources":[{"serviceAccountToken":{"expirationSeconds":3607,"path":"token"}}, ...]}}]expirationSeconds: 3607 — ประมาณหนึ่งชั่วโมง — และ kubelet refresh มันให้อัตโนมัติเมื่อใกล้หมดอายุ โดยไม่ต้องเขียน credential อายุยืนลง etcd เป็น Secret เลย
apiVersion: v1kind: Podmetadata: name: checkout namespace: checkoutspec: serviceAccountName: checkout-app containers: - name: checkout image: checkout:2.3.0Aggregated ClusterRole: รวมสิทธิ์ด้วย label ไม่ใช่ copy-paste
หัวข้อที่มีชื่อว่า “Aggregated ClusterRole: รวมสิทธิ์ด้วย label ไม่ใช่ copy-paste”aggregationRule ให้ ClusterRole หนึ่งตัวดูดซับ rule ของ ClusterRole อื่นทุกตัวที่ match label selector เข้ามาอัตโนมัติ — rules: [] ของ aggregate role เริ่มว่างเปล่าแล้ว control plane เติมให้เอง
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: monitoring-aggregateaggregationRule: clusterRoleSelectors: - matchLabels: rbac-demo/aggregate-to-monitoring: "true"rules: []---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: monitoring-pods-part labels: rbac-demo/aggregate-to-monitoring: "true"rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list"]---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: monitoring-nodes-part labels: rbac-demo/aggregate-to-monitoring: "true"rules: - apiGroups: [""] resources: ["nodes"] verbs: ["get", "list"]$ kubectl get clusterrole monitoring-aggregate -o jsonpath='{.rules}'[{"apiGroups":[""],"resources":["nodes"],"verbs":["get","list"]},{"apiGroups":[""],"resources":["pods"],"verbs":["get","list"]}]rule ของ ClusterRole อีกสองตัวปรากฏใน monitoring-aggregate อัตโนมัติ มาจาก label ที่แชร์กันล้วน ๆ — ClusterRole ตัวที่สามที่สร้างทีหลังด้วย label เดียวกันจะเข้าร่วม aggregate ได้โดยไม่ต้องแก้ monitoring-aggregate เองเลย ClusterRole built-in ของ Kubernetes เองอย่าง admin/edit/view ก็ aggregate แบบนี้เป๊ะ ๆ ซึ่งเป็นวิธีที่ operator ของ CRD ขยาย edit/view ให้ครอบคลุม custom resource ของตัวเองได้แค่ label ClusterRole ของตัวเอง
verb impersonate
หัวข้อที่มีชื่อว่า “verb impersonate”verb impersonate ให้ตัวตนหนึ่งทำ request ในนามของ อีกตัวตนได้ — กลไกที่ --as เองก็พึ่งพา การให้สิทธิ์แบบแคบ ด้วย resourceNames scope ไปที่ ServiceAccount ตัวใดตัวหนึ่งเฉพาะ
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: sa-impersonatorrules: - apiGroups: [""] resources: ["serviceaccounts"] verbs: ["impersonate"] resourceNames: ["pod-reader-sa"]---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: sa-impersonator-bindingsubjects: - kind: ServiceAccount name: overprivileged-sa namespace: rbac-demoroleRef: kind: ClusterRole name: sa-impersonator apiGroup: rbac.authorization.k8s.ioพิสูจน์ว่าการ scope ด้วยชื่อเป็นจริงและถูกบังคับใช้ ไม่ใช่แค่คำแนะนำ
$ kubectl auth can-i impersonate serviceaccounts/pod-reader-sa --as=system:serviceaccount:rbac-demo:overprivileged-sa -n rbac-demoyes$ kubectl auth can-i impersonate serviceaccounts/some-other-sa --as=system:serviceaccount:rbac-demo:overprivileged-sa -n rbac-demonooverprivileged-sa impersonate ได้แค่ ServiceAccount ตัวเดียวที่ระบุใน resourceNames และถูกปฏิเสธสำหรับตัวอื่นใด — impersonate เป็น verb ที่ authorize แยกต่างหากจริง ๆ ไม่ใช่ side effect ของสิทธิ์อื่น
cluster-admin: binding ที่แทบไม่มี workload ไหนต้องการจริง ๆ
หัวข้อที่มีชื่อว่า “cluster-admin: binding ที่แทบไม่มี workload ไหนต้องการจริง ๆ”cluster-admin เป็น ClusterRole built-in แบบ pre-aggregated ที่ให้ทุก verb บนทุก resource ทั่ว cluster การผูก ServiceAccount ของ workload เข้ากับมันเป็น RBAC anti-pattern ที่พบบ่อยที่สุดอย่างหนึ่ง เพราะมันให้สิทธิ์มากกว่าที่ workload ไหนควรต้องการมาก
$ kubectl auth can-i delete secrets --as=system:serviceaccount:rbac-demo:overprivileged-sa -Ayes$ kubectl auth can-i delete nodes --as=system:serviceaccount:rbac-demo:overprivileged-sayesServiceAccount ที่ตั้งใจไว้แค่ให้อ่าน ConfigMap ใน namespace ของตัวเอง — พอผูกกับ cluster-admin แล้ว — ลบ Secret ไหนใน namespace ไหนก็ได้ และลบ Node ของ cluster ได้เลย container ที่ถูก compromise ตัวเดียวที่ใช้ตัวตนนั้นก็มีสิทธิ์เท่ากับ human cluster administrator วิธีแก้ก็เป็นหลักการเดียวกับที่พิสูจน์ตลอดทั้งบทนี้ คือ Role แบบ namespace (หรือ ClusterRole ที่ scope แคบ ๆ จำกัดด้วย resourceNames ถ้าทำได้) บวก binding ที่ให้แค่ verb ที่ workload เรียกใช้จริง
flowchart LR pod["Pod\n(identity: ServiceAccount)"] --> req["Request to API server\n(bound token, TokenRequest API)"] req --> authz["API server\nauthorization check"] sa["ServiceAccount"] --> rb["RoleBinding"] role["Role\n(verbs on resources)"] --> rb rb --> authz authz -- "allowed" --> ok["Request proceeds"] authz -- "denied" --> deny["403 Forbidden\n(proven above)"]
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”RBAC ตอบคำถามเดียวเสมอ: given ตัวตนนี้ (User, Group, หรือ ServiceAccount) มี binding สักตัว ที่ผูก Role/ClusterRole ให้กับมัน แล้ว role นั้นให้ verb นี้บน resource นี้ตรงเป๊ะไหม ทุกอย่างที่เหลือในบทนี้คือ variation ของการทำให้ lookup นั้นแคบลง (Role scope แค่ namespace เดียว, resourceNames scope แค่ object เดียว) หรือประกอบง่ายขึ้น (aggregationRule, reuse ClusterRole ผ่าน RoleBinding แบบ namespace) ServiceAccount ขยายคำถามเดียวกันนี้ไปให้ workload แทนที่จะเป็น human และตั้งแต่ 1.24 credential ที่หนุนหลังตัวตนนั้นคือ token แบบ bound อายุสั้น scope ตาม audience ที่ issue ตามที่ต้องการโดย API TokenRequest — ไม่เคยเป็น Secret แบบ static ที่นั่งอยู่ใน etcd ตลอดไป impersonate คือ RBAC เช็คตัวเอง มันก็แค่ verb อีกตัวบน resource type อีกแบบ อยู่ภายใต้กลไก Role/RoleBinding เดียวกันเป๊ะ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
Role แบบ namespace + RoleBinding | scope แคบ พิสูจน์แล้วด้านบนด้วย 403 จริงสำหรับอะไรก็ตามนอก verb ของมัน | ต้องทำซ้ำต่อ namespace ถ้าต้องการสิทธิ์เดียวกันในหลาย namespace |
ClusterRole ที่ reuse ผ่าน RoleBinding แบบ namespace | พิสูจน์แล้วด้านบน: rule ชุดเดียว ผูกแคบต่อ namespace ไม่ต้อง duplicate | มองไม่ชัดทันทีว่า namespace ไหนได้สิทธิ์นั้นจริง ๆ — ต้องเช็ค binding ไม่ใช่แค่ role |
ClusterRoleBinding ไปที่ ClusterRole | ง่าย object เดียวให้สิทธิ์ทั่ว cluster | ทั่ว cluster ตามชื่อเลย — พิสูจน์แล้วด้านบนด้วย cluster-admin นี่คือวิธีที่ binding เดียวที่กว้างเกินไปกลายเป็น blast radius ร้ายแรงได้ |
ClusterRole แบบ aggregated | พิสูจน์แล้วด้านบน: ประกอบได้ ขยายได้โดย component ไหนก็ label ClusterRole ของตัวเอง ไม่ต้องแก้ที่ศูนย์กลาง | audit ยากขึ้นว่า “role นี้ให้อะไรจริง ๆ” ถ้าไม่หา role ที่ label ไว้ทุกตัวด้วย |
bound SA token (TokenRequest, default ตั้งแต่ 1.24) | พิสูจน์แล้วด้านบน: อายุสั้น scope ตาม audience ไม่มี Secret อายุยืนให้หลุด | อะไรที่อ่าน token แบบ out-of-band (log, build step ยาว) ต้องรองรับการหมดอายุ ไม่เหมือน token แบบถาวรรุ่นเก่า |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “Role กับ ClusterRole คือของเดียวกัน แค่ชื่อต่างกัน” พิสูจน์แล้วด้านบน: rule ของ
Roleมีผลแค่ใน namespace ของตัวเองเท่านั้น ส่วนClusterRoleให้สิทธิ์ resource ระดับ cluster (nodes) ที่Roleไหนก็เอ่ยถึงไม่ได้เลย - “ทุก ServiceAccount ได้ Secret token อายุยืนอัตโนมัติเหมือนสมัยก่อน” พิสูจน์แล้วด้านบน: ServiceAccount ที่เพิ่งสร้างไม่มี Secret เลย — ตั้งแต่ Kubernetes 1.24 token เป็นแบบ bound อายุสั้นและขอตามความต้องการผ่าน
TokenRequest/kubectl create tokenเท่านั้น หรือ auto-mount เข้า Pod แบบเดียวกันนี้ - “
kubectl auth can-i --listโชว์แค่สิ่งที่ Role/ClusterRole ให้ตรง ๆ” พิสูจน์แล้วด้านบน: output มี default ของ API server บางส่วน (selfsubjectreviews,selfsubjectaccessreviews) ที่ทุกตัวตนที่ authenticated แล้วได้เสมอ ควบคู่กับสิ่งที่ binding จริงให้ - “ผูก workload กับ
cluster-adminก็โอเคตราบใดที่ workload นั้นเชื่อถือได้” พิสูจน์แล้วด้านบน:cluster-adminให้สิทธิ์ลบ Node และ Secret ทุก namespace ไกลเกินความต้องการจริงของ workload แทบทุกตัว — ความเสี่ยงอยู่ที่จะเกิดอะไรถ้า workload นั้นถูก compromise ไม่ใช่ว่ามันน่าเชื่อถือวันนี้ไหม - “verb
impersonateแค่ทำให้kubectl --asใช้ได้กับทุกคน ตราบใดที่ RBAC ตั้งค่าไว้แล้ว” พิสูจน์แล้วด้านบนด้วยcan-icheck จริง: impersonation ถูก authorize (หรือ deny) ต่อตัวตน และ scope ได้ด้วยresourceNamesไปที่ subject เป้าหมายตัวใดตัวหนึ่งเฉพาะ เหมือน verb อื่นทุกตัว
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review manifest RBAC หรือการตั้งค่า ServiceAccount ที่ AI สร้าง ให้เช็ค
- ServiceAccount ของ workload ที่ผูกกับ
cluster-admin“เพื่อเลี่ยง permission error” — พิสูจน์แล้วด้านบน มันให้สิทธิ์ลบ Node ของ cluster และ Secret ทุก namespace วิธีแก้คือRoleที่ scope แคบ ระบุแค่ verb ที่ใช้จริง - manifest หรือ tutorial ที่สร้าง Secret แบบ
kubernetes.io/service-account-tokenเองแล้ว mount มัน — พิสูจน์แล้วด้านบน นี่คือ pattern ก่อน 1.24 token แบบ bound ผ่าน projected volume default (หรือkubectl create tokenสำหรับใช้นอก cluster) คือแนวทางปัจจุบันและหมดอายุเอง ClusterRoleBindingที่ใช้ในจุดที่RoleBindingแบบ namespace (แม้จะอ้างClusterRoleก็ตาม) scope ให้แค่ namespace เดียวได้เพียงพอแล้ว แทนที่จะเป็นทั้ง clusterRole/ClusterRoleที่ให้ verb"*"หรือ resource"*"ในจุดที่มีแค่ไม่กี่ verb เฉพาะที่ถูกเรียกใช้จริง — wildcard กว้าง ๆ ทำลายจุดประสงค์ของ least privilege ไปเลย และ audit ยากกว่า list ที่ระบุชัด ๆ มาก- verb
impersonateที่ให้โดยไม่มีresourceNamesทั้งที่มีแค่ subject เดียวเฉพาะที่ต้องถูก impersonate จริง ๆ — พิสูจน์แล้วด้านบนว่า scope ด้วยชื่อทำได้และถูกบังคับใช้จริง การให้แบบไม่ scope ทำให้ผู้ถือ impersonate ตัวตนไหนก็ได้ของประเภทนั้น
🐛 หา bug ในโค้ดนี้
apiVersion: v1kind: ServiceAccountmetadata:name: checkout-appnamespace: checkout---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata:name: checkout-app-bindingsubjects:- kind: ServiceAccount name: checkout-app namespace: checkoutroleRef:kind: ClusterRolename: cluster-adminapiGroup: rbac.authorization.k8s.ioเฉลย
checkout-app ต้องการแค่อ่าน Pod กับ ConfigMap ใน namespace ของตัวเอง แต่ตรงนี้ผูกมันกับ cluster-admin ทั่ว cluster พิสูจน์แล้วด้านบนด้วย binding แบบเดียวกัน: kubectl auth can-i delete secrets --as=system:serviceaccount:checkout:checkout-app -A จะได้ yes — ServiceAccount นี้ลบ Secret ไหนใน namespace ไหนก็ได้ หรือลบ Node ของ cluster ได้เลย ซึ่งไม่มีอันไหนที่ “อ่าน Pod กับ ConfigMap ของตัวเอง” ต้องการเลย วิธีแก้คือ Role แบบ namespace ใน checkout ที่ให้แค่ get/list/watch บน pods/configmaps ผูกด้วย RoleBinding ไม่ใช่ ClusterRoleBinding ไปที่ cluster-admin