ข้ามไปยังเนื้อหา

รีวิวจริง 3: platform namespace พร้อม RBAC, quota และ NetworkPolicy

Prompt คือ: “ตั้ง platform namespace ใหม่ให้ทีม: enforce security baseline, จำกัด resource usage, ให้ ServiceAccount ของแอปมี access ที่ต้องใช้ แล้วล็อค network ให้แน่นหนา” นี่คือ bundle ที่ AI generate แน่นที่สุดแบบหนึ่ง — object 7 ชนิดต่างกัน แต่ละอันแยกดูก็เป็น snippet เล็ก ๆ ที่ดูสมเหตุสมผลก็อปมาจาก tutorial คนละแหล่ง มี bug จริง 7 ตัว เช็ค AI bug catalog สำหรับ pattern ทั่วไปเบื้องหลังแต่ละตัว ทุกข้อความอ้างด้านล่างพิสูจน์จริงบน kind cluster 2 node เดียวกับที่ใช้ใน รีวิวจริง 1 (reading-b, Kubernetes 1.37.0) พร้อมติดตั้ง Calico v3.31.5 เพื่อบังคับใช้ NetworkPolicy จริง — kubectl apply จริง, admission rejection จริง, kubectl auth can-i จริง และ network test แบบจับเวลาจริง

# platform Namespace + Pod Security Admission labels, as generated -- see review below
apiVersion: v1
kind: Namespace
metadata:
name: platform
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted
# platform-quota ResourceQuota, as generated -- see review below
apiVersion: v1
kind: ResourceQuota
metadata:
name: platform-quota
namespace: platform
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "20"
# platform-app ServiceAccount + ClusterRoleBinding, as generated -- see review below
apiVersion: v1
kind: ServiceAccount
metadata:
name: platform-app
namespace: platform
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: platform-app-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: platform-app
namespace: platform
# platform-app agent Pod, as generated -- see review below
apiVersion: v1
kind: Pod
metadata:
name: agent
namespace: platform
spec:
serviceAccountName: platform-app
containers:
- name: c
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests: { cpu: 20m, memory: 32Mi }
limits: { cpu: 50m, memory: 64Mi }
securityContext:
privileged: true
# default-deny NetworkPolicy, as generated -- see review below
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: platform
spec:
podSelector: {}
policyTypes:
- Ingress
# @expect-error no matches for kind "PodSecurityPolicy" in version "policy/v1beta1"
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: platform-restricted
spec:
privileged: false
runAsUser: { rule: MustRunAsNonRoot }
seLinux: { rule: RunAsAny }
fsGroup: { rule: RunAsAny }
supplementalGroups: { rule: RunAsAny }
volumes: ["*"]

หน้าที่เดียวของ platform-app คือรันแอปของทีมในเนมสเปซตัวเอง แต่ถูกผูกกับ ClusterRole built-in cluster-admin ทั่วทั้ง cluster ผ่าน ClusterRoleBinding พิสูจน์ด้วยตัวตนจริงที่ container ที่ถูก compromise แล้วรันด้วย ServiceAccount นี้จะมี:

$ kubectl auth can-i delete secrets --as=system:serviceaccount:platform:platform-app -A
yes
$ kubectl auth can-i delete nodes --as=system:serviceaccount:platform:platform-app
yes

🐛 หา bug ในโค้ดนี้

roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
เฉลย

cluster-admin เป็น ClusterRole แบบ pre-aggregated ที่ให้ทุก verb บนทุก resource ทั่วทั้ง cluster — การผูกมันเข้ากับ ServiceAccount ของ workload คือหนึ่งใน blast radius ที่ใหญ่ที่สุดในทั้ง object model พิสูจน์แล้วด้านบน: ตัวตนนี้ลบ Secret ในทุกnamespace ได้ หรือลบ Node ได้ตรง ๆ ซึ่งไม่มีอันไหนที่ “รันแอปของเรา” ต้องการเลย API server รับ binding นี้เนียนพอ ๆ กับตัวที่ scope ถูกต้อง — ไม่มีอะไรตอน apply แยก “access ที่พอดี” ออกจาก “ทั้งหมด” เลย วิธีแก้คือ Role แบบ namespaced ที่ระบุแค่ verb กับ resource ที่โค้ดของแอปเรียกใช้จริง (เช่น get/list/watch บน ConfigMap ของตัวเอง) ผูกด้วย RoleBinding ที่ scope แค่ namespace platform ไม่ใช่ ClusterRoleBinding ไปที่ cluster-admin เด็ดขาด

Namespace enforce pod-security.kubernetes.io/enforce: restricted แต่ agent Pod ยังตั้ง privileged: true อยู่ดี พิสูจน์ตรง:

$ kubectl apply -f agent-pod.yaml
Error from server (Forbidden): error when creating "agent-pod.yaml": pods "agent" is forbidden: violates PodSecurity "restricted:latest": privileged (container "c" must not set securityContext.privileged=true), allowPrivilegeEscalation != false (container "c" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "c" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "c" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "c" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")

🐛 หา bug ในโค้ดนี้

securityContext:
privileged: true
เฉลย

นี่คือ API server เองปฏิเสธ object ตรง ๆ ไม่ใช่ความเห็นของ linter — Pod Security Admission (GA ตั้งแต่ 1.25 และเป็นตัวแทน built-in ของ PodSecurityPolicy ที่ถูกลบไป) enforce Pod Security Standard restricted ที่ label ของ namespace ประกาศไว้ แล้วบอก field ที่ container นี้ fail ทั้งห้าพร้อมกันเป๊ะใน error เดียว privileged: true ที่เอามาแก้ permission error อื่นที่ไม่เกี่ยวกันเป็น “fix” แบบที่ AI generate บ่อยที่สุดแบบหนึ่งที่ทำให้ security posture แย่ลงกว่าเดิมแทนที่จะแก้ปัญหาจริง วิธีแก้คือระบุ capability หรือ UID จริงที่ container ต้องการ — แทบไม่เคยเป็น privileged: true เลย บวก field ที่เหลืออีกสี่ตัวที่ error บอกไว้แล้ว: allowPrivilegeEscalation: false, capabilities.drop: ["ALL"], runAsNonRoot: true, seccompProfile.type: RuntimeDefault

ทั้ง ServiceAccount และ Pod ไม่ได้ตั้ง automountServiceAccountToken: false เลย ทั้งที่ agent ตัวนี้ไม่เคยเรียก Kubernetes API เลย พิสูจน์ด้วยการดู volume จริงของ Pod ตอนรันอยู่:

$ kubectl get pod agent-ok -o jsonpath='{.spec.volumes[*].name}'
kube-api-access-bsgwq
$ kubectl get pod agent-ok -o jsonpath='{.spec.containers[0].volumeMounts[*].mountPath}'
/var/run/secrets/kubernetes.io/serviceaccount

🐛 หา bug ในโค้ดนี้

apiVersion: v1
kind: Pod
spec:
serviceAccountName: platform-app
# no automountServiceAccountToken: false anywhere
เฉลย

โดย default ทุก Pod จะได้ bound token ของ ServiceAccount ตัวเอง project เข้ามาเป็น volume mount ที่ /var/run/secrets/kubernetes.io/serviceaccount อัตโนมัติ — เช็คแล้วด้านบน volume kube-api-access-* มีอยู่จริงบน spec ของ Pod นี้ ทั้งที่ไม่มีตรงไหนใน container อ่านมันเลยสักครั้ง token ที่ mount ไว้นั้นคือ attack surface ที่ไม่ได้ประโยชน์อะไรเลยสำหรับ workload ที่ไม่เคยเรียก Kubernetes API: ถ้า container ถูก compromise เมื่อไหร่ RBAC ที่ ServiceAccount นั้นถืออยู่ใช้ได้ทันทีจากข้างในเลย วิธีแก้คือ automountServiceAccountToken: false บน Pod (หรือบน ServiceAccount เอง) ซึ่งเอา volume กับ mount ทั้งก้อนออกไปเลย ไม่ใช่แค่เนื้อหาข้างใน — แล้วค่อยเปิดกลับมาชัดเจนเมื่อ workload ต้องเรียก API จริง ๆ

Namespace มี ResourceQuota ที่ track requests.cpu/requests.memory/limits.cpu/limits.memory แต่ไม่มี LimitRange ให้ default พิสูจน์ด้วย Pod ที่ไม่มี resources block เลยจริง ๆ:

$ kubectl apply -f norequest-pod.yaml
Error from server (Forbidden): error when creating "norequest-pod.yaml": pods "norequest" is forbidden: failed quota: platform-quota: must specify limits.cpu for: c; limits.memory for: c; requests.cpu for: c; requests.memory for: c

🐛 หา bug ในโค้ดนี้

apiVersion: v1
kind: ResourceQuota
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "20"
# no LimitRange anywhere in this namespace
เฉลย

ResourceQuota ที่ครอบ requests.cpu/requests.memory/limits.cpu/limits.memory ตัวใดตัวหนึ่ง บังคับให้ Pod ทุกตัวที่สร้างใน namespace นั้นต้องระบุครบทั้งสี่ตัวเอง ไม่มี fallback เลย พิสูจน์แล้วด้านบนด้วย Forbidden จริงทันทีที่บอกครบทั้งสี่ field ที่หายไปบน Pod ที่ถ้าไม่มี quota นี้ก็ valid สมบูรณ์ LimitRange คือตัวที่ให้ default เมื่อคนเขียน Pod ไม่ได้ระบุไว้ — ถ้าไม่มีมัน ทุกทีมที่ deploy เข้า namespace นี้ต้องรู้ shape ของ quota แบบเป๊ะ ๆ ก่อนถึงจะสร้าง Pod อะไรก็ได้ผ่านเลย ซึ่งเป็น friction แบบที่ทำให้คนก็อปตัวเลขมาแปะมั่ว ๆ แทนที่จะคิดจริง วิธีแก้คือใส่ LimitRange ใน namespace เดียวกันด้วยค่า default/defaultRequest ที่สมเหตุสมผล เพื่อให้ Pod spec เปล่า ๆ ยังได้ค่าที่ใช้ได้แทนที่จะโดนปฏิเสธตรง ๆ

policy default-deny ที่ generate มาเลือก Pod ทุกตัว (podSelector: {}) แต่ policyTypes มีแค่ Ingress — ไม่มี Egress เลย พิสูจน์ด้วย Pod จริงที่ policy นี้เลือกตรง ๆ บน cluster ที่ Calico บังคับใช้ NetworkPolicy จริง:

$ kubectl exec deploy/web -- wget -q -T 3 -O /dev/null -S https://1.1.1.1
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
x-frame-options: DENY
alt-svc: h3=":443"; ma=86400

🐛 หา bug ในโค้ดนี้

spec:
podSelector: {}
policyTypes:
- Ingress
# Egress is not listed here at all
เฉลย

policyTypes ของ NetworkPolicy ไม่ใช่แค่ documentation ว่ามี rule list ไหนตามมา — มันคุมว่าทิศทางไหนบ้างที่ถูกจำกัดเลย การระบุ Pod ใน podSelector ไม่ได้จำกัด egress ของมันเลยถ้าไม่มี Egress อยู่ใน policyTypes ด้วย พิสูจน์แล้วด้านบน: Pod ที่ policy “default-deny” ตัวนี้เลือกตรง ๆ ยังไปถึง host ภายนอกผ่าน HTTPS ได้แบบไม่มีข้อจำกัดเลย พร้อม response header จริงครบ manifest ที่ชื่อ default-deny แต่เปิดทิศทาง traffic หนึ่งไว้กว้างเงียบ ๆ คือความผิดพลาดแบบที่อันตราย เพราะมันดูเหมือนสมบูรณ์แล้ว วิธีแก้คือระบุทั้งสองทิศทางชัดเจน: policyTypes: [Ingress, Egress] พร้อม egress: [] เปล่า (หรือ scope ไว้) เพื่อจำกัด outbound traffic จริง ๆ ด้วย

พอเพิ่ม Egress เข้า policyTypes ถูกต้องแล้วแต่ไม่มี egress rule เลยสักตัว ทุก connection ขาออก — รวมถึง DNS lookup ของ cluster เอง — ก็ถูกบล็อคหมด พิสูจน์ด้วย client Pod จริง ก่อนและหลัง:

$ kubectl exec client -- wget -q -T 3 -O- http://web.netpol-demo.svc.cluster.local/
<!DOCTYPE html>
... # ก่อนมี NetworkPolicy ใด ๆ: ทำงานปกติ
$ kubectl apply -f default-deny-all.yaml # policyTypes: [Ingress, Egress], ไม่มี egress rule
$ kubectl exec client -- wget -q -T 3 -O- http://web.netpol-demo.svc.cluster.local/
wget: bad address 'web.netpol-demo.svc.cluster.local'

🐛 หา bug ในโค้ดนี้

spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
# no egress rules at all -- including no DNS
เฉลย

bad address ตรงนี้ไม่ใช่ failure ระดับ HTTP เลย — เป็น DNS resolution failure: พอ Egress ถูกปฏิเสธและไม่มี rule อนุญาตเลย lookup ของ client ไปหา CoreDNS (UDP/TCP port 53) เองไม่หลุดออกจาก Pod เลย ชื่อเลย resolve ไม่ออกก่อนที่จะพยายาม connect HTTP ด้วยซ้ำ พิสูจน์แล้วด้านบนว่า request ไปไม่ถึงขั้น timeout บน connection จริงเลยด้วยซ้ำ นี่คือสาเหตุ outage แบบ default-deny ที่พบบ่อยที่สุดในโลกจริง วิธีแก้ที่ default-deny policy ทุกตัวต้องมีคือ rule คู่กันที่อนุญาต DNS egress โดยเฉพาะ — egress: [{ to: [{namespaceSelector: {}}], ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}] }] — ซึ่งคืนความสามารถ resolve ชื่อโดยไม่เปิดอะไรอื่นเพิ่ม (test แยกอีกอันยืนยันว่า DNS-only egress ทำให้ resolve ชื่อได้ แต่ HTTP connection ถัดมาบน port 80 ยัง timeout อยู่ดี เพราะไม่มี rule อนุญาต port นั้น)

manifest set ที่ generate มามี object PodSecurityPolicy ที่ตั้งใจ enforce baseline เดียวกับที่ PSA label ของ namespace ประกาศไว้แล้ว พิสูจน์ตรง:

$ kubectl apply -f psp.yaml
error: resource mapping not found for name: "platform-restricted" namespace: "" from "psp.yaml": no matches for kind "PodSecurityPolicy" in version "policy/v1beta1"
ensure CRDs are installed first

🐛 หา bug ในโค้ดนี้

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
เฉลย

PodSecurityPolicy ถูกลบไป (ไม่ใช่แค่ deprecate) ใน Kubernetes 1.25 release เดียวกับที่ Pod Security Admission กลายเป็น GA ในฐานะตัวแทน built-in ของมันเป๊ะ — บทเรียน Pod and Cluster Security ของคอร์สนี้ยืนยันข้อเท็จจริงนี้ไว้แล้ว API server ไม่มี route ให้ kind นี้เลยบน cluster ปัจจุบัน เลย fail ที่ discovery-layer stage เดียวกับ API version ที่ถูกลบในรีวิวอื่นสองอันของโมดูลนี้ วิธีแก้ไม่ใช่ PSP shape ใหม่ — เพราะไม่มีแล้ว — แต่คือลบ object นี้ทิ้งไปเลย เพราะ label pod-security.kubernetes.io/enforce: restricted ของ namespace (มีอยู่แล้วและพิสูจน์แล้วด้านบนว่าปฏิเสธ privileged: true ได้จริง) ทำหน้าที่นี้ถูกต้องด้วยตัวเองอยู่แล้ว

k8s/worked-review-3/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: platform
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted
k8s/worked-review-3/quota-and-limitrange.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: platform-quota
namespace: platform
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "20"
---
apiVersion: v1
kind: LimitRange
metadata:
name: platform-limits
namespace: platform
spec:
limits:
- type: Container
default: { cpu: 200m, memory: 256Mi }
defaultRequest: { cpu: 100m, memory: 128Mi }
k8s/worked-review-3/rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: platform-app
namespace: platform
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: platform-app-role
namespace: platform
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: platform-app-binding
namespace: platform
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: platform-app-role
subjects:
- kind: ServiceAccount
name: platform-app
namespace: platform
k8s/worked-review-3/agent-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: agent
namespace: platform
spec:
# serviceAccountName omitted -- uses the namespace's real "default" SA so this
# fence dry-runs standalone; a real deployment would set
# serviceAccountName: platform-app (defined in rbac.yaml above)
automountServiceAccountToken: false
containers:
- name: c
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests: { cpu: 20m, memory: 32Mi }
limits: { cpu: 50m, memory: 64Mi }
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1000
capabilities: { drop: ["ALL"] }
seccompProfile: { type: RuntimeDefault }
k8s/worked-review-3/networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: platform
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: platform
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {}
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
k8s/worked-review-3/vap.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: platform-registry-allowlist
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
validations:
- expression: >-
object.spec.containers.all(c,
c.image.startsWith('registry.k8s.io/') ||
c.image.startsWith('docker.io/library/'))
message: "container images must come from an allowlisted registry"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: platform-registry-allowlist-binding
spec:
policyName: platform-registry-allowlist
validationActions: ["Deny"]
matchResources:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: platform

npm run verify dry-run ทั้ง 6 ไฟล์ด้านบนกับ verify-k8s cluster ที่ใช้ร่วมกัน ได้ 0 error ทุก fence ในนี้ประกาศ metadata.namespace: platform ของตัวเอง (namespace bundle จริงต้องทำแบบนี้) เลย — เหมือนกับบทเรียน Namespaces and Resource Quotas ของคอร์สนี้เอง — --apply ก็แค่ dry-run มันแทนที่จะ apply จริงลง namespace ชั่วคราว (เช็คตรงแล้ว: node tools/verify-manifests.mjs --apply reading-k8s/worked-review-3 กับ --apply storage-and-config/namespaces-and-resource-quotas ทั้งคู่ print kubectl wait: error: no matching resources found และ get all ว่างเปล่า เพราะ harness apply จริงได้แค่ fence ที่ปล่อยให้มัน inject -n <ephemeral> เองเท่านั้น) หลักฐาน apply จริงแบบไม่ dry-run สำหรับ fixed set ของรีวิวนี้คือทุกอย่างที่พิสูจน์ไปแล้วด้านบนบน reading-b: namespace platform จริงที่ label restricted PSA ปฏิเสธ privileged: true จริง, ResourceQuota+LimitRange คู่จริงที่ admit Pod ไม่มี resources ได้จริง, RoleBinding จริงที่ทำให้ can-i delete secrets -A แคบลงจาก yes เป็น no จริง และพฤติกรรม NetworkPolicy ที่ Calico บังคับใช้จริงทั้งสำหรับ egress fix กับ DNS fix

#Bugวิธีแก้
1ServiceAccount ผูกกับ cluster-admin ผ่าน ClusterRoleBindingRole/RoleBinding แบบ namespaced ที่ระบุแค่ verb ที่ใช้จริง — can-i delete secrets -A พิสูจน์จริง yes ก่อน no หลัง
2securityContext.privileged: truesecurityContext ที่ compliant กับ restricted เต็มรูปแบบ — admission rejection ห้า field จริงพิสูจน์ก่อน
3automountServiceAccountToken ถูกปล่อย default สำหรับ workload ที่ไม่เรียก API เลยautomountServiceAccountToken: false — volume kube-api-access-* จริงพิสูจน์ว่ามีอยู่ก่อน
4ResourceQuota ไม่มี LimitRangeเพิ่ม LimitRange พร้อม default/defaultRequest — must specify limits.cpu... จริงพิสูจน์ก่อน
5NetworkPolicy policyTypes ไม่มี Egressระบุทั้งสองทิศทาง — HTTPS egress ที่ไม่ถูกจำกัดจริงพิสูจน์ก่อน
6Default-deny ไม่มี DNS egress rulepolicy คู่กัน allow-dns-egress — bad address (DNS fail) จริงพิสูจน์ก่อน
7Object PodSecurityPolicy (API ที่ถูกลบ)ลบทิ้งทั้งหมด — PSA label ของ namespace ทำหน้าที่นี้อยู่แล้ว; no matches for kind จริงพิสูจน์

มีแค่ 1 ใน bug เจ็ดตัวนี้ (7, PSP) ที่ API server ปฏิเสธ object ไม่รู้จักตรง ๆ อีก 2 ตัว (2, Pod privileged; 4, Pod ที่ไม่มี resources) คือ admission rejection จริงทันที — แต่เป็นการปฏิเสธ Pod คนละตัว กับตัวที่ตั้งใจให้ทำงานได้ ซึ่งเป็นเหตุผลที่มันพลาดง่ายเวลารีวิว platform bundle แยกจาก workload จริงที่พยายามใช้มัน อีก 4 ตัวที่เหลือ (1, 3, 5, 6) apply ผ่านเนียนและผ่านทุก check เพราะแต่ละตัวคือการอ้างเรื่อง scope หรือความครบถ้วนที่ไม่มีอะไรตอน apply มา cross-reference: binding ให้ไม่เกินที่ workload ต้องการจริงไหม, token mount มีอะไรใช้จริงไหม, “Ingress” กับ “Egress” ใน policyTypes ถูกพิจารณาทั้งคู่จริงไหมหรือก็อปมาจากตัวอย่างอื่นแค่ตัวเดียว, policy “default-deny” คิดถึง infrastructure dependency ของตัวเอง (DNS) แล้วหรือยัง platform namespace bundle มักมี bug shape นี้แน่นเป็นพิเศษ เพราะเป็น object หลายตัวที่แยกดูสมเหตุสมผลแต่ละอัน แล้วเผยช่องว่างก็ต่อเมื่อ workload จริงพยายามรันข้างในจริง ๆ

  • “ServiceAccount ของ workload ที่ผูกกับ cluster-admin ก็โอเคตราบใดที่ workload นั้นน่าเชื่อถือ” พิสูจน์แล้วด้านบน: ความเสี่ยงไม่ใช่ความน่าเชื่อถือวันนี้ แต่คือ blast radius ถ้า workload นั้นถูก compromise เมื่อไหร่ — binding นี้เป๊ะ ๆ ลบ Secret ได้ทุก namespace หรือลบ Node ได้ตรง ๆ จริงและยืนยันแล้วด้วย kubectl auth can-i
  • “policyTypes: [Ingress] บน NetworkPolicy ที่มีแค่ ingress rule ก็แค่ชัดเจนว่า policy นี้ทำอะไร” พิสูจน์แล้วด้านบน: การไม่ใส่ Egress ใน policyTypes ไม่ได้แปลว่า “แค่ไม่มี egress rule ตรงนี้” แต่แปลว่า egress ของ Pod ที่ถูกเลือกไม่ถูกจำกัดโดย policy นี้เลย — HTTPS request จริงไปหา host ภายนอกสำเร็จผ่าน policy ที่ชื่อ default-deny
  • “ResourceQuota บน cpu/memory แปลว่า Kubernetes เติมค่า default ที่สมเหตุสมผลให้ Pod ที่ไม่ได้ระบุเอง” พิสูจน์แล้วด้านบนด้วย rejection จริงทันที: ResourceQuota ที่ครอบ resource ตัวใดตัวหนึ่งทำให้ resource นั้นบังคับบน Pod ทุกตัวใน namespace — มีแค่ LimitRange เท่านั้นที่ให้ default
flowchart TD
  NS["Namespace: platform, PSA restricted"]
  NS --> RBAC["ServiceAccount platform-app -- bound to cluster-admin"]
  RBAC -->|"1: can delete Secrets/Nodes anywhere"| RBAC
  NS --> POD["agent Pod: privileged=true"]
  POD -->|"2: restricted admission rejects"| POD
  NS --> TOK["agent Pod: automountServiceAccountToken unset"]
  TOK -->|"3: token mounted, unused"| TOK
  NS --> Q["ResourceQuota, no LimitRange"]
  Q -->|"4: every Pod must specify everything"| Q
  NS --> NP["NetworkPolicy default-deny: policyTypes=[Ingress]"]
  NP -->|"5: egress wide open"| NP
  NS --> DNS["default-deny-all: no egress rules"]
  DNS -->|"6: DNS egress blocked too"| DNS
  PSP["PodSecurityPolicy: policy/v1beta1"] -->|"7: kind removed in 1.25"| PSP
จุดที่แต่ละ bug อยู่ใน admission กับ traffic path ของ platform namespace นี้
ServiceAccount ของ workload ที่แอปอ่านแค่ ConfigMap ใน namespace ตัวเองถูกผูกกับ cluster-admin ผ่าน ClusterRoleBinding ผลลัพธ์ can-i จริงนี้พิสูจน์อะไร แล้วทำไม API server ถึงรับ binding นี้โดยไม่ว่าอะไรเลย?
$ kubectl auth can-i delete secrets --as=system:serviceaccount:platform:platform-app -A
yes
นี่คือ rejection จริงภายใต้ namespace ที่ label pod-security.kubernetes.io/enforce: restricted อะไรทำให้เกิด error นี้ แล้วมันบอก requirement แยกกันกี่ข้อพร้อมกัน?
Error from server (Forbidden): ... violates PodSecurity "restricted:latest": privileged (container "c" must not set securityContext.privileged=true), allowPrivilegeEscalation != false (...), unrestricted capabilities (...), runAsNonRoot != true (...), seccompProfile (...)
NetworkPolicy ชื่อ default-deny ตั้ง podSelector: {} กับ policyTypes: [Ingress] เท่านั้น (ไม่มี Egress) Pod ที่ policy นี้เลือกถูกทดสอบด้วย request จริงไปหา host ภายนอกผ่าน HTTPS จะเกิดอะไรขึ้น แล้วทำไม?
apply default-deny policy ที่มี policyTypes: [Ingress, Egress] และไม่มี egress rule เลย request ของ client Pod fail ด้วย bad address แทนที่จะเป็น connection timeout ข้อความนี้พิสูจน์อะไรเจาะจง?
$ kubectl exec client -- wget -q -T 3 -O- http://web/
wget: bad address 'web...'
Namespace มี ResourceQuota ครอบ cpu/memory requests กับ limits แต่ไม่มี LimitRange Pod ที่ไม่มี resources block เลยถูกปฏิเสธด้วยข้อความนี้ อะไรจะป้องกัน rejection นี้ได้?
Error from server (Forbidden): ... pods "norequest" is forbidden: failed quota: platform-quota: must specify limits.cpu for: c; limits.memory for: c; requests.cpu for: c; requests.memory for: c