วิธีตั้งค่าความปลอดภัย k3s บน VPS ให้ปลอดภัยจากช่องโหว่
เรียนรู้วิธีปิดช่องโหว่ความปลอดภัยบน k3s cluster แบบโหนดเดียว ทั้งการจำกัดพอร์ต 6443 และ 10250 การจัดการสิทธิ์ kubeconfig และการตั้งค่า Firewall เพื่อป้องกันการเข้าถึงจากภายนอก
สิ่งที่ k3s cluster แบบโหนดเดียวเปิดเผยออกมาในวันแรก
k3s cluster แบบโหนดเดียวบน VPS สาธารณะจะถูกเปิดเผยใน 5 จุดสำคัญทันทีหลังจากติดตั้งด้วยคำสั่งเดียวเสร็จสิ้น ได้แก่ Kubernetes API server บน TCP 6443, kubelet บน TCP 10250, ไฟล์ kubeconfig ที่วางอยู่บนดิสก์, ช่วงพอร์ต NodePort ที่ firewall ของคุณมองไม่เห็น และ pod ใดก็ตามที่ได้รับอนุญาตให้ร้องขอ privileged หรือ hostPath แต่ละจุดมีวิธีแก้ไขที่ใช้เวลาเพียงไม่กี่นาที คู่มือนี้สมมติว่า k3s กำลังทำงานอยู่แล้ว ดังนั้นหากยังไม่ได้เริ่ม ให้เริ่มจาก การติดตั้ง k3s แบบโหนดเดียวบน VPS แล้วค่อยกลับมาที่นี่
ตรวจสอบสิ่งที่กำลังเปิดพอร์ตฟังอยู่ก่อนที่คุณจะทำการเปลี่ยนแปลงใดๆ
sudo ss -tulpn | grep -E '6443|10250|10256|8472'การติดตั้งตามค่าเริ่มต้นจะแสดงพอร์ต 6443 (API server), 10250 (kubelet), 10256 (kube-proxy health check) และ 8472/udp (flannel overlay ซึ่งใช้ VXLAN หรือ virtual extensible LAN) โดยค่าเริ่มต้น k3s จะผูกกับ 0.0.0.0 ดังนั้นพอร์ตทั้งหมดเหล่านี้จึงเปิดอยู่บนที่อยู่สาธารณะของคุณ ไม่ใช่แค่บน loopback เท่านั้น
เหตุผลที่พอร์ต 6443 คือหัวใจของทั้งคลัสเตอร์
ทุกสิ่งที่สามารถยืนยันตัวตนเข้าสู่พอร์ต 6443 ด้วยสิทธิ์ผู้ดูแลระบบสามารถสร้าง pod ได้ และ pod นั้นสามารถมีสิทธิ์เป็น root บนโฮสต์ได้ พอร์ต 6443 จึงเปรียบเสมือนประตูเข้าสู่เครื่อง
การเปิดพอร์ต 6443 ไม่ได้หมายความว่าระบบจะถูกเจาะทันที เพราะ Kubernetes ไม่ยอมรับรหัสผ่าน แต่ต้องการ client certificate หรือ bearer token อย่างไรก็ตาม มีสองสิ่งที่ยังคงเป็นความจริง
ประการแรก API server จะตอบสนองต่อบางคำขอโดยไม่ต้องใช้ข้อมูลยืนยันตัวตนเลย ค่าเริ่มต้นของ Kubernetes RBAC (role-based access control) จะผูกกลุ่ม system:unauthenticated เข้ากับ role ที่ชื่อ system:public-info-viewer ซึ่งอนุญาตให้เข้าถึง /version, /healthz, /livez และ /readyz ได้ จากเครื่องอื่น:
curl -sk https://YOUR_SERVER_IP:6443/versionคำสั่งนี้จะส่งกลับเวอร์ชันของ Kubernetes ที่คุณใช้อยู่ ซึ่งเป็นข้อมูลตั้งต้นสำหรับการค้นหา CVE (common vulnerabilities and exposures) และเป็นเหตุผลที่เครื่องมือสแกนตัดสินว่าเซิร์ฟเวอร์ของคุณน่าสนใจ ทุกสิ่งที่อยู่นอกเหนือจาก path เหล่านั้นจะถูกปฏิเสธ และการปฏิเสธนั้นจะระบุตัวตนของคุณ:
forbidden: User "system:anonymous" cannot get path "/api"ประการที่สอง บั๊กทุกตัวของ API server สามารถถูกโจมตีจากระยะไกลได้ตราบเท่าที่พอร์ตนี้ยังเปิดอยู่ การแพตช์ระบบจึงไม่ใช่ทางเลือกแต่เป็นสิ่งที่ต้องทำ
วิธีแก้ไขราคาประหยัดคือการใช้กฎ firewall โดย ufw ต้องการมากกว่าหนึ่งบรรทัดในกรณีนี้ เพราะ k3s จะส่ง traffic ของคลัสเตอร์ผ่าน kernel เดียวกัน
sudo ufw default deny incoming
sudo ufw allow OpenSSH
sudo ufw allow from 203.0.113.10 to any port 6443 proto tcp
sudo ufw allow from 10.42.0.0/16 to any
sudo ufw allow from 10.43.0.0/16 to any
sudo ufw enableกฎสองบรรทัดสุดท้ายมาจากเอกสารของ k3s โดยตรง 10.42.0.0/16 คือเครือข่ายเริ่มต้นของ pod และ 10.43.0.0/16 คือเครือข่ายเริ่มต้นของ service หากไม่มีกฎเหล่านี้ ufw จะดรอป traffic ภายในคลัสเตอร์ ทำให้ pod ไม่สามารถติดต่อ API server หรือติดต่อกันเองได้ ตัวอย่างของ k3s อนุญาตให้เข้าถึงพอร์ต 6443 ได้จากทุกที่ การเปลี่ยนเป็นระบุเฉพาะที่อยู่ IP ของคุณเองจึงเป็นการเปลี่ยนแปลงที่คุ้มค่า หากคุณยังไม่คุ้นเคยกับ ufw พื้นฐานการใช้งาน ufw firewall สำหรับ VPS จะครอบคลุมถึงนโยบายเริ่มต้นที่กฎเหล่านี้ต้องพึ่งพา
เอกสารของ k3s ระบุไว้อย่างชัดเจนเกี่ยวกับพอร์ต overlay ว่า "พอร์ต VXLAN บนโหนดไม่ควรเปิดเผยต่อสาธารณะ เพราะจะทำให้เครือข่ายคลัสเตอร์ของคุณถูกเข้าถึงโดยใครก็ได้" การตั้งค่านโยบาย default deny สำหรับ traffic ขาเข้าจะช่วยจัดการเรื่องนี้ได้โดยไม่ต้องระบุพอร์ตโดยตรง
วิธีแก้ไขที่แข็งแกร่งกว่าคือการหยุดเข้าถึง API ผ่านที่อยู่สาธารณะโดยสิ้นเชิง แล้วเปลี่ยนไปใช้ VPN หรือที่อยู่แบบ mesh แทน โดย certificate ของเซิร์ฟเวอร์จะต้องระบุที่อยู่ที่คุณใช้เชื่อมต่อ ดังนั้นให้เพิ่มที่อยู่นั้นเป็น SAN (subject alternative name) ใน /etc/rancher/k3s/config.yaml:
tls-san:
- 10.8.0.1
- k3s.example.com
secrets-encryption: truesudo systemctl restart k3s
sudo k3s secrets-encrypt statussecrets-encryption: true จะทำการเข้ารหัสออบเจกต์ประเภท Secret ใน datastore เอกสารของ k3s ระบุว่า "Secrets-encryption ไม่สามารถเปิดใช้งานบนเซิร์ฟเวอร์ที่มีอยู่เดิมได้โดยไม่รีสตาร์ท" และ Secret ที่ถูกเขียนลงไปก่อนการเปลี่ยนแปลงจะยังคงอยู่ในรูปแบบเดิมจนกว่าคุณจะรัน sudo k3s secrets-encrypt reencrypt ต้องเข้าใจให้ชัดเจนว่าสิ่งนี้ช่วยอะไรได้บ้าง มันช่วยป้องกันไฟล์ datastore ที่ถูกคัดลอกออกไปจากไฟล์สำรอง แต่ไม่ได้ช่วยป้องกันผู้ที่สามารถสื่อสารกับ API server ได้ เพราะ API server จะถอดรหัส Secret ให้กับใครก็ตามที่มีสิทธิ์อ่าน ความแตกต่างนี้เป็นหลักการเดียวกับที่ใช้ใน secret store แบบ self-hosted ทุกประเภท ซึ่งเป็นเหตุผลว่าทำไม การเพิ่มความปลอดภัยให้กับ Vaultwarden จึงเน้นไปที่ admin token และไฟล์สำรอง มากกว่าการเข้ารหัสตัวมันเอง
เหตุผลที่ kubelet บนพอร์ต 10250 มีความสำคัญ
kubelet คือเอเจนต์ที่ทำหน้าที่เริ่มการทำงานของคอนเทนเนอร์ API ของมันบนพอร์ต 10250 สามารถแสดงรายการ pod และรันคำสั่งภายใน pod เหล่านั้นได้ หาก kubelet ยอมรับคำขอแบบไม่ระบุตัวตน (anonymous requests) จะเท่ากับเป็นการเปิดช่องทาง remote shell เข้าสู่ทุกเวิร์กโหลดบนเครื่องนั้น
ตรวจสอบการตั้งค่าของคุณ:
curl -sk https://127.0.0.1:10250/pods | head -c 60k3s เวอร์ชันปัจจุบันจะตอบกลับด้วย Unauthorized เนื่องจาก kubelet จะร้องขอให้ API server ทำการยืนยันตัวตนและตรวจสอบสิทธิ์ของผู้เรียกใช้งานทุกคน หากผลลัพธ์ที่ได้กลับมาเป็นรายการ pod ในรูปแบบ JSON แสดงว่ามีการเปิดใช้งาน anonymous access ซึ่งหมายความว่าใครก็ตามที่สามารถเข้าถึงพอร์ต 10250 ได้ จะสามารถอ่านข้อมูลและสั่งรันคำสั่งภายในคอนเทนเนอร์ของคุณได้
ไม่ว่ากรณีใดก็ตาม คุณควรปิดกั้นการเข้าถึงจากภายนอก สำหรับโหนดเดี่ยว ไคลเอนต์เดียวของ kubelet คือ control plane ที่อยู่บนเครื่องเดียวกัน และทราฟฟิกดังกล่าวจะเข้ามาผ่าน loopback interface ซึ่ง ufw ยอมรับโดยค่าเริ่มต้นอยู่แล้ว การปฏิเสธการเชื่อมต่อพอร์ต 10250 จากอินเทอร์เน็ตจึงไม่มีผลกระทบใดๆ หากคุณมาที่นี่เนื่องจาก kubectl top ที่มีปัญหา หรือ metrics-server ที่ไม่สามารถทำงานได้ตามปกติ สาเหตุต่างๆ ได้ถูกรวบรวมไว้ที่ ข้อผิดพลาดของ kubelet พอร์ต 10250
kubeconfig ของคุณคือข้อมูลรับรองระดับ cluster admin
k3s เขียนไฟล์ /etc/rancher/k3s/k3s.yaml โดยกำหนดให้ root เป็นเจ้าของและใช้โหมด 600 เอกสารระบุผลลัพธ์ของการเปลี่ยนแปลงค่านี้ไว้ว่า: "ไฟล์ kubeconfig มี root เป็นเจ้าของ และถูกเขียนด้วยโหมดเริ่มต้น 600 การเปลี่ยนโหมดเป็น 644 จะทำให้ผู้ใช้ที่ไม่มีสิทธิ์ระดับสูงรายอื่นบนโฮสต์สามารถอ่านไฟล์นี้ได้"
ให้เข้าใจว่า: โหมด 644 จะทำให้บัญชีผู้ใช้ในเครื่องทุกคนกลายเป็นผู้ดูแลระบบ cluster คู่มือจำนวนมากแนะนำให้ทำเช่นนั้น โดยมักจะใช้ --write-kubeconfig-mode 644 เพื่อให้ kubectl ทำงานได้โดยไม่ต้องใช้ sudo วิธีนี้ทำงานโดยการมอบข้อมูลรับรองของผู้ดูแลระบบให้กับทุกคนที่มีสิทธิ์เข้าถึง shell
ให้คัดลอกไฟล์ไปยังผู้ใช้รายหนึ่งแทน
mkdir -p ~/.kube
sudo install -o "$USER" -g "$USER" -m 600 /etc/rancher/k3s/k3s.yaml ~/.kube/config
kubectl get nodesจากนั้นตรวจสอบว่าไฟล์ต้นฉบับยังคงถูกจำกัดสิทธิ์ไว้อย่างแน่นหนา:
stat -c '%a %U:%G' /etc/rancher/k3s/k3s.yaml600 root:root คือคำตอบที่คุณต้องการ ไฟล์นั้นเก็บ client certificate สำหรับสมาชิกของ system:masters ซึ่งเป็นกลุ่มที่ API server ถือว่าได้รับอนุญาตโดยไม่มีเงื่อนไข ดังนั้นกฎ RBAC จึงไม่ถูกนำมาตรวจสอบสำหรับกลุ่มนี้ Kubernetes ไม่มีรายการเพิกถอนใบรับรอง (certificate revocation list) ซึ่งหมายความว่าหากสำเนาไฟล์รั่วไหลออกไป มันจะยังคงใช้งานได้จนกว่าคุณจะหมุนเวียน (rotate) certificate authority ของ cluster ให้ปฏิบัติต่อไฟล์นี้เหมือนกับ SSH private key และจำกัดจำนวนบัญชีผู้ใช้ที่สามารถเข้าถึงไฟล์นี้ให้เหลือน้อยที่สุด ซึ่งเป็นหลักการเดียวกับ บัญชีผู้ใช้ที่มีสิทธิ์น้อยที่สุดบน VPS
ไฟร์วอลล์ของคุณไม่เห็นทราฟฟิกของ NodePort
บริการ type: NodePort จะเปิดพอร์ตในช่วง 30000 ถึง 32767 บนทุกแอดเดรสที่โหนดถือครองอยู่ รวมถึงแอดเดรสสาธารณะด้วย บริการ type: LoadBalancer จะทำงานเพิ่มเติมบน k3s โดย ServiceLB ซึ่งเป็นโหลดบาลานเซอร์ที่มาพร้อมกัน จะกำหนดตารางเวลาให้ pod ขนาดเล็กต่อหนึ่งบริการใน kube-system เพื่อจองพอร์ตของบริการนั้นโดยตรงบนโฮสต์
kubectl -n kube-system get pods | grep svclb
sudo ss -tulpn | grep -E ':3[0-2][0-9]{3}'ส่วนนี้คือสิ่งที่ทำให้ผู้คนประหลาดใจ หากคุณบล็อกพอร์ตนั้นด้วย ufw มันจะยังคงตอบสนองอยู่
sudo ufw deny 30080/tcp
curl http://YOUR_SERVER_IP:30080หน้าเว็บยังคงโหลดได้ เนื่องจากเส้นทางที่แพ็กเก็ตเดินทาง kube-proxy จะเขียนกฎ DNAT (destination network address translation) ลงในตาราง nat ของเชน PREROUTING และ PREROUTING จะทำงานก่อนการตัดสินใจกรองข้อมูลใดๆ ปลายทางจะกลายเป็นแอดเดรสของ pod ซึ่งไม่ใช่โฮสต์ ดังนั้นเคอร์เนลจึงส่งแพ็กเก็ตไปยังเชน FORWARD และไม่ผ่าน INPUT กฎของ ufw นั้นอยู่ใน INPUT แพ็กเก็ตจึงไม่เคยผ่านกฎเหล่านั้น ลำดับการกระโดด (jump order) สามารถดูได้ดังนี้:
sudo iptables -S PREROUTING -t nat | head
sudo iptables -S FORWARD | headKUBE-SERVICES จะอยู่ที่ด้านบนของ PREROUTING และการกระโดดของ Kubernetes ใน FORWARD จะอยู่เหนือเชนของ ufw เอง นี่เป็นกลไกเดียวกับที่ทำให้ Docker เผยแพร่พอร์ตโดยข้าม ufw และคำตอบก็เป็นแบบเดียวกัน
- กรองทราฟฟิกที่ไฟร์วอลล์เครือข่ายของผู้ให้บริการของคุณ มันทำงานอยู่หน้าเครื่องและไม่สนใจว่าเคอร์เนลของคุณกำหนดเส้นทางอย่างไร
- หลีกเลี่ยงการใช้ NodePort และ LoadBalancer ให้คงบริการไว้ที่
ClusterIPและเข้าถึงบริการเหล่านั้นด้วยkubectl port-forwardผ่านเซสชัน SSH ที่คุณมีอยู่แล้ว - เปิดเผย ingress เพียงรายการเดียวบนพอร์ต 80 และ 443 เท่านั้น และไม่ต้องเปิดพอร์ตอื่น
- จำกัดช่วงพอร์ตด้วย
service-node-port-rangeภายใต้kube-apiserver-argเพื่อให้กรณีที่มีการใช้ NodePort โดยไม่ตั้งใจ มันจะไปตกอยู่ในช่วงที่คุณเฝ้าระวังอยู่
ufw ยังคงคุ้มค่าที่จะใช้งาน มันควบคุมทราฟฟิกที่ส่งถึงตัวโฮสต์เอง ซึ่งได้แก่ SSH และ API server เพียงแต่ว่ามันไม่ได้ทำหน้าที่ตรวจสอบทราฟฟิกของ pod และการคาดหวังให้มันทำเช่นนั้นคือสาเหตุที่ทำให้ฐานข้อมูลถูกเข้าถึงได้จากภายนอก
Pod ที่ใช้ hostPath หรือ privileged จะมีสิทธิ์ root บน VPS ของคุณ
Container เป็นเพียง process ปกติบน kernel ของคุณที่มีมุมมองจำกัดต่อระบบ แต่มีฟิลด์บางอย่างใน pod ที่ทำให้ข้อจำกัดเหล่านั้นหมดไป
securityContext.privileged: trueมอบ Linux capability ทั้งหมดและสิทธิ์เข้าถึงอุปกรณ์บน host ให้กับ containerhostPathทำการ mount ไดเรกทอรีของ host เข้าไปใน pod หาก pod mount/แบบอ่านและเขียนได้ ก็สามารถเพิ่ม key เข้าไปใน/root/.ssh/authorized_keysได้hostPID: trueนำ container ไปอยู่ใน process namespace ของ host ซึ่งการทำnsenterกับ PID 1 จะเป็นการเปิด shell ของ hosthostNetwork: trueนำ container ไปอยู่ใน network stack ของ host ซึ่งทำให้สามารถ bind พอร์ตของ host และเข้าถึงบริการที่ผูกกับ loopback ได้
ดังนั้น คำถามที่ว่า "ใครสามารถสร้าง pod ที่นี่ได้บ้าง" จึงเท่ากับคำถามที่ว่า "ใครเป็น root บน VPS เครื่องนี้" ServiceAccount ใดก็ตามที่มีสิทธิ์ create บน pod ใน namespace ใดๆ จะมีสถานะเทียบเท่า root เว้นแต่จะมีกลไกปฏิเสธ pod นั้นเสียก่อน
กลไกดังกล่าวคือ Pod Security admission ซึ่งถูกรวมไว้ใน API server แล้ว วิธีที่รวดเร็วคือการกำหนด label ให้กับแต่ละ namespace โดยไม่ต้อง restart ระบบ
kubectl label namespace default \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/warn=restrictedbaseline จะปฏิเสธฟิลด์ทั้งสี่รายการข้างต้น ส่วน restricted จะเข้มงวดกว่าโดยกำหนดให้ต้องใช้ non-root user, ใช้ seccomp (secure computing mode) profile, ห้ามยกระดับสิทธิ์ และต้องตัด capability ออกเหลือเพียง ALL ซึ่งจะทำให้ chart ที่เผยแพร่ทั่วไปจำนวนมากใช้งานไม่ได้ การบังคับใช้ baseline ในขณะที่ตั้งค่าเตือนเฉพาะ restricted จะช่วยให้คุณทราบว่าจะมีส่วนใดที่ใช้งานไม่ได้ก่อนที่จะตัดสินใจบังคับใช้จริง
ทดสอบการทำงาน:
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: pstest
spec:
containers:
- name: app
image: busybox
command: ["sleep", "60"]
securityContext:
privileged: true
EOFAPI server จะปฏิเสธคำสั่งและระบุฟิลด์ที่ถูกปฏิเสธ:
Error from server (Forbidden): error when creating "STDIN": pods "pstest" is forbidden: violates PodSecurity "baseline:latest": privileged (container "app" must not set securityContext.privileged=true)สำหรับการตั้งค่าเริ่มต้นทั้ง cluster แทนการกำหนด label ทีละ namespace ทาง k3s ได้จัดทำเอกสารเกี่ยวกับไฟล์ admission configuration ไว้ที่ /var/lib/rancher/k3s/server/psa.yaml:
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1beta1
kind: PodSecurityConfiguration
defaults:
enforce: "baseline"
enforce-version: "latest"
warn: "restricted"
warn-version: "latest"
exemptions:
namespaces: [kube-system]ระบุไฟล์ดังกล่าวให้กับ API server ใน /etc/rancher/k3s/config.yaml จากนั้น restart k3s:
kube-apiserver-arg:
- 'admission-control-config-file=/var/lib/rancher/k3s/server/psa.yaml'ข้อยกเว้น kube-system เป็นสิ่งที่จำเป็นต้องมี เพราะ pod ประเภท ServiceLB ของ k3s เองจำเป็นต้องใช้พอร์ตของ host ซึ่ง baseline ไม่อนุญาต ดังนั้นหากละเว้น kube-system ออกจากรายการ pod เหล่านั้นจะถูกปฏิเสธในครั้งถัดไปที่มีการสร้างใหม่ ควรเปิด SSH session สำรองไว้เสมอเมื่อคุณ restart k3s หลังจากเปลี่ยนการตั้งค่า admission
แถมท้ายอีกหนึ่งเรื่อง: k3s มาพร้อมกับ network policy controller และเปิดใช้งานเป็นค่าเริ่มต้น ดังนั้น object ประเภท NetworkPolicy จะมีผลทันทีบน cluster นี้โดยไม่ต้องติดตั้งอะไรเพิ่มเติม ซึ่งไม่ใช่ทุก Kubernetes distribution ที่ทำได้ และนี่คือเครื่องมือสำคัญในการป้องกันไม่ให้ pod ที่ถูกบุกรุกเข้าถึงส่วนอื่นๆ ของระบบได้
ลบส่วนประกอบที่ติดตั้งมาพร้อมกันซึ่งคุณไม่ได้ใช้งาน
ตัวติดตั้งจะติดตั้งส่วนเสริมมาให้ชุดหนึ่ง แต่ละส่วนเสริมจะเพิ่ม listener และสิ่งที่ต้องคอยแพตช์เพิ่มขึ้นอีกหนึ่งอย่าง --disable ยอมรับค่าเหล่านี้: coredns, servicelb, traefik, local-storage, metrics-server, runtimes
ให้คง coredns ไว้ เนื่องจากไม่มีสิ่งใดในคลัสเตอร์ที่สามารถแก้ไขชื่อ (resolve name) ได้หากไม่มีส่วนประกอบนี้ ส่วนที่เหลือเป็นทางเลือก ใน /etc/rancher/k3s/config.yaml:
disable:
- traefik
- servicelb
disable-helm-controller: truesudo systemctl restart k3s
kubectl get pods -Ak3s จะลบส่วนประกอบที่คุณปิดการใช้งานออก ดังนั้น pod ของ traefik และ pod ของ svclb- จะหายไปเองโดยอัตโนมัติ โปรดทำความเข้าใจผลกระทบก่อนดำเนินการ เมื่อ servicelb ถูกลบออก บริการประเภท type: LoadBalancer ทุกรายการจะค้างอยู่ที่สถานะ <pending> ตลอดไป เนื่องจากไม่มีสิ่งใดกำหนดที่อยู่ให้ เมื่อ traefik ถูกลบออก จะไม่มี ingress controller ส่งผลให้วัตถุประเภท Ingress ไม่ทำงานใดๆ เลย ให้ปิดการใช้งานส่วนประกอบเหล่านี้เมื่อคุณให้บริการทราฟฟิกด้วยวิธีอื่น เช่น การใช้ reverse proxy บนโฮสต์ และปล่อยไว้ตามเดิมหากคุณยังคงใช้งานอยู่ disable-helm-controller: true จะลบ controller ที่คอยตรวจสอบทรัพยากรประเภท HelmChart ซึ่งเป็นส่วนประกอบที่มีสิทธิ์สูง (privileged component) ที่คุณไม่ได้ใช้งานหากคุณรัน helm ด้วยตนเอง
หยุดการทำ automount ของ ServiceAccount token เริ่มต้น
ทุก Pod จะได้รับ ServiceAccount token ที่ /var/run/secrets/kubernetes.io/serviceaccount/token เว้นแต่คุณจะกำหนดเป็นอย่างอื่น โดย ServiceAccount ที่ชื่อ default นั้นไม่มีสิทธิ์ RBAC ใดๆ ดังนั้นตัว token เพียงอย่างเดียวจึงไม่สามารถทำอะไรได้มากนัก สิ่งที่มันมอบให้กับผู้โจมตีภายในคอนเทนเนอร์ที่ถูกเจาะระบบคือ credential ที่ใช้งานได้จริงและ API server ที่เข้าถึงได้ ซึ่งเป็นขั้นตอนแรกของรายงานการยกระดับสิทธิ์ในคลัสเตอร์ส่วนใหญ่
คู่มือการเพิ่มความปลอดภัยของ k3s แนะนำให้ปิดการทำงานนี้ในระดับ namespace:
kubectl patch serviceaccount --namespace default default --patch '{"automountServiceAccountToken": false}'
kubectl patch serviceaccount --namespace kube-node-lease default --patch '{"automountServiceAccountToken": false}'
kubectl patch serviceaccount --namespace kube-public default --patch '{"automountServiceAccountToken": false}'ตรวจสอบว่าการตั้งค่ามีผลแล้ว โดยรอให้ Pod เริ่มทำงานสักครู่ก่อน
kubectl run t --image=busybox --restart=Never --command -- sleep 30
kubectl exec t -- ls /var/run/secrets/kubernetes.io/serviceaccountเมื่อ path ดังกล่าวถูกลบออกไปแล้ว คำสั่ง ls จะแสดงผลเป็น No such file or directory สำหรับ workload ที่จำเป็นต้องเข้าถึง API จริงๆ สามารถตั้งค่า automountServiceAccountToken: true ใน pod spec ของตนเองได้ ดังนั้นจึงไม่มีบริการใดถูกปิดกั้นอย่างถาวร นี่เป็นชัยชนะเล็กๆ ในตัวของมันเอง แต่ชัยชนะที่ใหญ่กว่าคือการไม่มอบ ServiceAccount ที่มีสิทธิ์จริงให้กับ workload และคุณสามารถตรวจสอบได้ว่าในปัจจุบัน token นั้นมีค่าเท่าใด:
kubectl auth can-i --list --as=system:serviceaccount:default:defaultการจำกัดทรัพยากรเพื่อป้องกันไม่ให้ Pod หนึ่งตัวทำให้คลัสเตอร์ล่ม
บนโหนดเดียว Control plane และ Workload ของคุณจะใช้ Kernel และหน่วยความจำร่วมกัน Pod ที่มีการรั่วไหลของหน่วยความจำ (memory leak) ไม่ได้หมายความว่าจะตายเพียงลำพังเสมอไป OOM (out of memory) killer ของ Kernel จะเลือกเหยื่อโดยพิจารณาจากคะแนนที่ให้น้ำหนักกับกระบวนการขนาดใหญ่ และ k3s เป็นกระบวนการขนาดใหญ่ที่ทำงานต่อเนื่องยาวนาน ดังนั้นสิ่งที่อาจหายไปอาจเป็นตัวคลัสเตอร์เองแทนที่จะเป็น Pod นั้น และจะไม่มีสิ่งใดรีสตาร์ท Workload ของคุณอีกต่อไป เพราะสิ่งที่ทำหน้าที่รีสตาร์ท Workload คือสิ่งที่ตายไปแล้ว
LimitRange จะช่วยกำหนดขีดจำกัดให้กับ Pod ที่ไม่ได้ตั้งค่าไว้:
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: default
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 50m
memory: 128MiResourceQuota จะจำกัดปริมาณทรัพยากรที่ Namespace ทั้งหมดสามารถเรียกใช้ได้:
apiVersion: v1
kind: ResourceQuota
metadata:
name: cap
namespace: default
spec:
hard:
limits.cpu: "3"
limits.memory: 3Gi
pods: "20"จากนั้นให้สำรองพื้นที่สำหรับ k3s ไว้ใน /etc/rancher/k3s/config.yaml:
kubelet-arg:
- 'system-reserved=cpu=250m,memory=512Mi'ความแตกต่างจะปรากฏให้เห็นในสองจุด คอนเทนเนอร์ที่ถูกสั่งยุติการทำงานเนื่องจากใช้ทรัพยากรเกินขีดจำกัดของตนเองจะรายงานสถานะ Reason: OOMKilled ภายใต้ Last State ใน kubectl describe pod และโหนดส่วนที่เหลือจะยังคงทำงานต่อไป ส่วนโหนดที่หน่วยความจำหมดลงโดยสิ้นเชิงจะทิ้งบรรทัด Killed process ไว้ใน dmesg และมักจะทำให้บริการข้างเคียงล่มตามไปด้วย กรณีแรกคือการที่ขีดจำกัดของคุณทำงานได้ตามหน้าที่ ส่วนกรณีที่สองคือสิ่งที่การกำหนดขีดจำกัดมีไว้เพื่อป้องกันไม่ให้เกิดขึ้น
สแกนไฟล์ manifest ใน CI และคลัสเตอร์ k3s ตามกำหนดเวลา
การสแกนควรทำในสองจุดเพื่อให้ครอบคลุมปัญหาที่ต่างกัน เริ่มต้นด้วยการติดตั้ง Trivy โครงการนี้มีการปล่อยแพ็กเกจ Debian ออกมาพร้อมกับทุก release โดยเวอร์ชัน 0.74.0 เป็นเวอร์ชันปัจจุบันในเดือนสิงหาคม 2026 ดังนั้นควรตรวจสอบหน้า releases เพื่อหาเวอร์ชันใหม่กว่าก่อนที่จะกำหนดเวอร์ชันลงในระบบอัตโนมัติ
sudo apt-get install -y wget
wget -q https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_Linux-64bit.deb
sudo dpkg -i trivy_0.74.0_Linux-64bit.deb
trivy --versionจุดแรกคือไฟล์ manifest ของคุณ ก่อนที่จะถูกนำไปใช้ในคลัสเตอร์
trivy fs --scanners misconfig --severity HIGH,CRITICAL --exit-code 1 ./deploy--exit-code 1 จะทำให้งาน CI (continuous integration) ล้มเหลวหากพบช่องโหว่ ผลลัพธ์แต่ละรายการจะระบุฟิลด์ที่ผิดพลาดและความรุนแรง ดังนั้น privileged: true ที่คุณไม่ได้ตั้งใจจะ commit จะทำให้ build ล้มเหลวแทนที่จะไปถึง API server หากคุณตัดสินใจยอมรับช่องโหว่ใด ให้ใส่ไว้ในไฟล์ .trivyignore ซึ่งจะเก็บการตัดสินใจนั้นไว้ใน git คู่กับไฟล์ manifest ที่เป็นต้นเหตุ
จุดที่สองคือคลัสเตอร์ที่กำลังทำงานอยู่ โดยทำตามกำหนดเวลา
trivy k8s --compliance=k8s-cis-1.23 --report summaryข้อควรทราบเกี่ยวกับคำสั่งดังกล่าว trivy k8s จะ deploy pod ประเภท node collector ซึ่งต้องการสิทธิ์เข้าถึง host เพื่อตรวจสอบการตั้งค่าระดับ node ในคลัสเตอร์ที่คุณเพิ่งเริ่มปฏิเสธการใช้งาน privileged pod สิ่งนี้เป็นจุดที่ควรพิจารณาแทนที่จะพยายามเลี่ยง trivy k8s --report summary --disable-node-collector จะข้ามการใช้ collector และทำให้สูญเสียการตรวจสอบในระดับ node ไปด้วย
kube-bench ครอบคลุมฝั่ง host เช่น สิทธิ์ของไฟล์และ flag ของ process ซึ่งเป็นส่วนที่เกณฑ์มาตรฐาน CIS (Center for Internet Security) ส่วนใหญ่ให้ความสำคัญ โดยมี profile สำหรับ k3s มาให้ ให้คุณนำ Job manifest จากต้นทางมาปรับแก้
curl -sfL -o kube-bench-job.yaml https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yamlเปลี่ยนคำสั่งของ container เป็น ["kube-bench", "--benchmark", "k3s-cis-1.7"] และสลับ mount /etc/kubernetes และ /var/lib/etcd เป็น /etc/rancher และ /var/lib/rancher เนื่องจากเป็นตำแหน่งที่ k3s เก็บไฟล์ไว้ จากนั้น:
kubectl apply -f kube-bench-job.yaml
kubectl logs -l app=kube-bench --tail=-1สังเกตว่า Job นั้นคืออะไร: hostPID: true รวมกับการ mount ไดเรกทอรีของ host ซึ่งเป็นรูปแบบเดียวกับ pod ที่คุณเริ่มปฏิเสธไปในหัวข้อก่อนหน้า ให้รันใน namespace kube-system ที่ได้รับการยกเว้น อ่านผลลัพธ์ แล้วจึง kubectl delete job kube-bench การที่เครื่องมือสแกนจำเป็นต้องมีสิทธิ์สูงไม่ใช่เหตุผลที่จะเลิกแบน workload ที่มีสิทธิ์สูง
เนื้อหาภายใน image เป็นอีกประเด็นหนึ่ง trivy image ghcr.io/example/app:1.4 จะอ่านฐานข้อมูลแพ็กเกจภายใน image และแสดงรายการสิ่งที่ทราบว่ามีช่องโหว่ ซึ่งเป็นงานเดียวกับที่คุณทำเมื่อ ตรวจสอบเซิร์ฟเวอร์ของคุณเพื่อหา CVE ที่ทราบ
ส่วนที่เป็นความจริงเกี่ยวกับผลลัพธ์ของเครื่องมือสแกนคือ คลัสเตอร์ขนาดโหนดเดียวสำหรับงานอดิเรกจะสอบตกเกณฑ์ CIS จำนวนมาก และความล้มเหลวส่วนใหญ่ถูกต้องแต่ไม่เกี่ยวข้องกับคุณ เกณฑ์มาตรฐานนี้เขียนขึ้นสำหรับคลัสเตอร์แบบหลายโหนดและหลายผู้เช่า เช่น etcd บนโฮสต์แยกต่างหาก, การส่ง audit log ออกจากเครื่อง, การมี certificate authority สำหรับ kubelet แยกต่างหาก, และ admission plugin สำหรับการปฏิบัติตามกฎระเบียบที่คุณไม่ได้อยู่ภายใต้บังคับ k3s ตั้งใจรัน control plane เป็น process เดียวด้วยไฟล์ config เดียว ดังนั้นการควบคุมที่ตรวจสอบสิทธิ์ของไฟล์ manifest kube-scheduler จึงไม่สามารถผ่านได้เพราะไม่มีไฟล์ดังกล่าวอยู่จริง
ให้อ่านรายการความล้มเหลวตามลำดับนี้และหยุดเมื่อไม่พบประโยชน์อีกต่อไป: โหมดและเจ้าของไฟล์ภายใต้ /etc/rancher และ /var/lib/rancher, สิ่งที่กล่าวถึงการเข้าถึงแบบ anonymous หรือ unauthenticated, สิ่งที่รายงานว่า component ผูกกับ 0.0.0.0, และ container ใดก็ตามที่รันด้วย UID 0 โดยไม่มีเหตุผล ส่วนที่เหลือสามารถรอจนกว่าจะมีโหนดที่สองหรือมีผู้ใช้งานคนที่สอง รายงานร้อยบรรทัดที่คุณเพิกเฉยมีค่าน้อยกว่ารายงานห้าบรรทัดที่คุณลงมือแก้ไขจริง
ลำดับการทำ hardening สำหรับ k3s บนโหนดเดียว
- ตั้งค่านโยบายขาเข้าเริ่มต้นของ ufw เป็น deny, อนุญาต SSH, อนุญาตพอร์ต 6443 จากที่อยู่ IP ของคุณ และอนุญาตเครือข่ายของ pod และ service
- คัดลอก kubeconfig ไปยังผู้ใช้ของคุณด้วยสิทธิ์ 600 และห้ามตั้งค่า
--write-kubeconfig-mode 644โดยเด็ดขาด - ยืนยันว่า kubelet บนพอร์ต 10250 ปฏิเสธคำขอแบบไม่ระบุตัวตน และห้ามเปิดพอร์ตนี้ออกสู่สาธารณะ
- ปิดการใช้งานคอมโพเนนต์ที่มาพร้อมกับ k3s ที่คุณไม่ได้ใช้งาน จากนั้นรีสตาร์ท k3s
- ติดป้ายกำกับ (label) namespace ของคุณสำหรับการยอมรับ Pod Security ด้วย
enforce=baselineและwarn=restricted - ปิดการตั้งค่า automounting ของ ServiceAccount token โดยค่าเริ่มต้น
- เพิ่ม LimitRange และ ResourceQuota พร้อมทั้งสำรอง CPU และหน่วยความจำสำหรับ k3s
- ใส่
trivy fs --scanners misconfigไว้ใน CI และทำการสแกน CIS เป็นประจำทุกเดือน
โฮสต์ที่อยู่เบื้องล่างยังคงต้องการการดูแลเช่นเดียวกับเซิร์ฟเวอร์อื่น ๆ และ k3s ก็เพิ่มความซับซ้อนในส่วนนี้ เนื่องจาก k3s ถูกติดตั้งผ่านสคริปต์แทนที่จะเป็น apt ดังนั้น apt upgrade จึงไม่เคยจัดการกับ k3s คุณต้องดูแลการอัปเดตแพตช์ของระบบปฏิบัติการตามกำหนดการแยกต่างหากด้วย unattended upgrades บน Ubuntu และอัปเกรด k3s ด้วยตนเองโดยการรันตัวติดตั้งซ้ำด้วย channel หรือเวอร์ชันที่ต้องการ เส้นทางการอัปเดตสองทางบนเครื่องเดียวอาจทำให้ลืมได้ง่าย ดังนั้นควรจดบันทึกไว้ว่าคอมโพเนนต์ใดใช้วิธีการอัปเดตแบบใด
FAQ
การเปิดเผย k3s API server ที่พอร์ต 6443 สู่สาธารณะปลอดภัยหรือไม่?
พอร์ตนี้ไม่ได้เปิดกว้างโดยตัวมันเอง เนื่องจาก API server ต้องการ client certificate หรือ token ในการเข้าถึง และจะปฏิเสธการเชื่อมต่ออื่นทั้งหมดด้วย forbidden: User "system:anonymous" อย่างไรก็ตามยังคงมีความเสี่ยงอยู่ 2 ประการ ประการแรก ผู้เรียกที่ไม่ระบุตัวตนยังสามารถอ่าน /version ได้ ซึ่งช่วยให้สแกนเนอร์ทราบเวอร์ชันของ Kubernetes ที่คุณใช้งานได้อย่างแม่นยำ ประการที่สอง ช่องโหว่ของ API server ในอนาคตจะสามารถถูกโจมตีจากระยะไกลได้หากพอร์ตนี้เปิดอยู่ สำหรับคลัสเตอร์แบบโหนดเดียว ไม่มีบริการภายนอกใดที่จำเป็นต้องใช้พอร์ต 6443 นอกจาก kubectl ของคุณเอง ดังนั้นควรอนุญาตเฉพาะ IP ของคุณด้วย sudo ufw allow from YOUR_IP to any port 6443 proto tcp และปล่อยให้เป็นหน้าที่ของนโยบาย default deny ในการจัดการส่วนที่เหลือ
ทำไมกฎ ufw ของฉันถึงไม่บล็อกบริการแบบ NodePort?
เพราะแพ็กเก็ตไม่เคยเดินทางไปถึง chain ที่กฎของคุณตั้งไว้ kube-proxy จะใส่กฎ DNAT ไว้ใน PREROUTING chain ของตาราง nat ซึ่งทำงานเป็นลำดับแรกและเขียนปลายทางใหม่ไปยังที่อยู่ของ pod แพ็กเก็ตจะถูกส่งต่อ (forward) แทนที่จะถูกส่งไปยังเครื่องโดยตรง จึงข้ามขั้นตอน INPUT ซึ่งเป็นที่ที่กฎของ ufw ทำงานอยู่ คุณสามารถตรวจสอบเรื่องนี้ได้ด้วย sudo iptables -S PREROUTING -t nat | head แนะนำให้กรอง NodePort ที่ network firewall ของผู้ให้บริการ หรือหลีกเลี่ยงการใช้ type: NodePort แล้วเข้าถึงบริการผ่าน kubectl port-forward แทน
ฉันควรเรียกใช้ k3s ด้วย --write-kubeconfig-mode 644 หรือไม่?
ไม่ควร เอกสารของ k3s ระบุชัดเจนว่า "การเปลี่ยนโหมดเป็น 644 จะทำให้ผู้ใช้คนอื่นที่ไม่มีสิทธิ์บนโฮสต์สามารถอ่านไฟล์ได้" ไฟล์ดังกล่าวเก็บ client certificate สำหรับ system:masters ไว้ ดังนั้นโหมด 644 จะทำให้ทุกบัญชีผู้ใช้ในเครื่องกลายเป็นผู้ดูแลระบบคลัสเตอร์ ให้ใช้วิธีคัดลอกไฟล์ไปยังผู้ใช้ที่ต้องการด้วย sudo install -o "$USER" -g "$USER" -m 600 /etc/rancher/k3s/k3s.yaml ~/.kube/config แทน และคงค่าเดิมของไฟล์ที่ 600 root:root ไว้ตามปกติ
ฉันควรใช้ kube-bench benchmark ตัวไหนสำหรับ k3s?
ให้ใช้ k3s-cis-1.7 เอกสารของ kube-bench ระบุว่า "kube-bench มี benchmark สำหรับแพลตฟอร์ม Rancher K3S โดยเฉพาะ ในการใช้งานคุณต้องระบุ --benchmark k3s-cis-1.7 เมื่อรันคำสั่ง kube-bench" คุณต้องระบุค่านี้อย่างชัดเจน เพราะระบบตรวจจับอัตโนมัติจะเข้าใจว่าเป็นโครงสร้างแบบ kubeadm ในขณะที่ k3s เก็บไฟล์ไว้ที่ /etc/rancher และ /var/lib/rancher คุณอาจพบข้อผิดพลาดที่ไม่เกี่ยวข้องกับโหนดเดียว ให้เน้นแก้ไขเรื่องสิทธิ์ของไฟล์และการเข้าถึงแบบไม่ระบุตัวตนก่อนเป็นอันดับแรก
Pod Security admission จะทำให้คอมโพเนนต์ที่มากับ k3s ทำงานไม่ได้หรือไม่?
จะเกิดปัญหาหากคุณบังคับใช้กับ kube-system เนื่องจาก ServiceLB pods ที่ k3s สร้างขึ้นสำหรับบริการประเภท type: LoadBalancer จะจองพอร์ตบนโฮสต์ และ baseline ไม่อนุญาตให้ใช้พอร์ตโฮสต์ ดังนั้น pod เหล่านั้นจะถูกปฏิเสธในครั้งถัดไปที่มีการสร้างใหม่ ให้ยกเว้น kube-system ในไฟล์กำหนดค่า admission หรือใช้ Pod Security เฉพาะกับ namespace ที่คุณสร้างขึ้นเองเท่านั้น เริ่มต้นด้วย enforce=baseline และ warn=restricted เพื่อให้คุณสามารถอ่านได้ว่า restricted จะส่งผลกระทบอย่างไรบ้างก่อนที่จะเปิดใช้งานจริง