تأمين عنقود k3s أحادي العقدة على VPS
اكتشف ما يتركه تثبيت k3s مكشوفاً على VPS: المنفذ 6443، وkubelet على 10250، وصلاحيات kubeconfig، وNodePort، والحاويات ذات الامتيازات.
ما الذي يعرّضه عنقود k3s أحادي العقدة في اليوم الأول
يكون عنقود k3s أحادي العقدة على VPS عام مكشوفاً في خمسة مواضع محددة في اليوم التالي لاكتمال تشغيل برنامج التثبيت ذي السطر الواحد: خادم Kubernetes API على TCP 6443، وkubelet على TCP 10250، وملف kubeconfig المخزّن على القرص، ونطاق NodePort الذي لا يستطيع جدارك الناري رؤيته، وأي pod مسموح له بطلب privileged أو hostPath. لكل موضع منها إجراء إصلاح يستغرق دقائق. يفترض هذا الدليل أن k3s يعمل بالفعل. إذا لم يكن كذلك، فابدأ بـتثبيت k3s أحادي العقدة على VPS ثم عد إلى هنا.
تحقق مما يستمع قبل أن تغيّر أي شيء.
sudo ss -tulpn | grep -E '6443|10250|10256|8472'يعرض التثبيت الافتراضي المنافذ 6443 (خادم API)، و10250 (kubelet)، و10256 (فحص صحة kube-proxy)، و8472/udp (طبقة flannel overlay التي تستخدم VXLAN، أي virtual extensible LAN). يرتبط k3s افتراضياً بـ0.0.0.0، لذلك تكون جميع هذه المنافذ على عنوانك العام، لا على loopback فقط.
لماذا يُعد المنفذ 6443 هو كامل العنقود
يمكن لأي جهة تصادق على المنفذ 6443 بصلاحيات admin إنشاء pod، ويمكن أن يصبح الـpod بحساب root على المضيف. المنفذ 6443 هو بوابة الجهاز.
لا يعني فتح المنفذ 6443 اختراقاً فورياً، لأن Kubernetes لا يقبل كلمات المرور. بل يتطلب شهادة عميل أو bearer token. ومع ذلك، تبقى حقيقتان.
أولاً، يستجيب API server لبعض الطلبات من دون أي بيانات اعتماد. يربط Kubernetes RBAC الافتراضي (التحكم بالوصول القائم على الأدوار) المجموعة system:unauthenticated بدور اسمه system:public-info-viewer، ويسمح ذلك بـ/version و/healthz و/livez و/readyz. من جهاز آخر:
curl -sk https://YOUR_SERVER_IP:6443/versionيعرض ذلك إصدار Kubernetes الدقيق لديك. ويُستخدم هذا الإصدار في البحث عن CVE (الثغرات والتعرّضات الشائعة)، وهو سبب اعتبار أداة الفحص جهازك مثيراً للاهتمام. تُرفض كل المسارات الأخرى، وتذكر رسالة الرفض هويتك:
forbidden: User "system:anonymous" cannot get path "/api"ثانياً، يمكن الوصول عن بُعد إلى كل خطأ في API server ما دام هذا المنفذ مفتوحاً. عندها تصبح التحديثات الأمنية أمراً إلزامياً.
الحل الرخيص هو إضافة قاعدة إلى الجدار الناري. يحتاج ufw هنا إلى أكثر من سطر واحد، لأن k3s يمرر حركة العنقود عبر نواة النظام نفسها.
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 شبكة الخدمة الافتراضية. ومن دونهما، يسقط ufw حركة المرور الداخلية للعنقود، فتفقد الـpods الاتصال بـAPI server وببعضها بعضاً. يسمح مثال k3s بالوصول إلى 6443 من كل مكان؛ واستبدال ذلك بعنوانك أنت هو التغيير المهم. إذا لم تستخدم ufw من قبل، يشرح دليل أساسيات جدار ufw الناري لخادم VPS السياسات الافتراضية التي يعتمد عليها هذا الإعداد.
توضح وثائق k3s مخاطر منفذ overlay بصراحة: "يجب ألا يكون منفذ VXLAN على العقد مكشوفاً للعالم، لأنه يتيح لأي شخص الوصول إلى شبكة عنقودك." وتتعامل سياسة الرفض الافتراضي لحركة المرور الواردة مع ذلك من دون تحديد المنفذ.
الحل الأقوى هو التوقف تماماً عن الوصول إلى API عبر العنوان العام، واستخدام عنوان VPN أو mesh بدلاً منه. يجب أن تتضمن شهادة الخادم العنوان الذي تتصل به، لذلك أضفه كـSAN (اسم بديل للموضوع) في /etc/rancher/k3s/config.yaml:
tls-san:
- 10.8.0.1
- k3s.example.com
secrets-encryption: truesudo systemctl restart k3s
sudo k3s secrets-encrypt statusيشفّر secrets-encryption: true كائنات Secret في مخزن البيانات. تذكر وثائق k3s أن "تشفير Secrets لا يمكن تفعيله على خادم موجود من دون إعادة تشغيله"، وتظل Secrets المكتوبة قبل التغيير بصيغتها القديمة إلى أن تشغّل sudo k3s secrets-encrypt reencrypt. يجب أن تكون الفائدة واضحة. فهذا يحمي ملف مخزن بيانات نُسخ من نسخة احتياطية. لكنه لا يحمي من يستطيع التحدث إلى API server، لأن API server يفك تشفير Secrets لأي جهة مخوّلة بقراءتها. وينطبق الفرق نفسه على أي مخزن أسرار مستضاف ذاتياً، ولهذا يركّز دليل تقوية إعداد Vaultwarden على admin token وملف النسخة الاحتياطية، لا على التشفير نفسه.
أهمية kubelet على المنفذ 10250
kubelet هو الوكيل الذي يبدأ الحاويات. تعرض واجهته البرمجية API على المنفذ 10250 قائمة pods وتنفّذ الأوامر داخلها. إذا كان kubelet يقبل الطلبات المجهولة، فإنه يوفّر shell عن بُعد إلى كل workload على الجهاز.
تحقق من إعدادك:
curl -sk https://127.0.0.1:10250/pods | head -c 60يستجيب k3s الحالي بالرمز Unauthorized، لأن kubelet يطلب من خادم API مصادقة كل مستدعٍ وتفويضه. إذا ظهرت قائمة pods بتنسيق JSON بدلاً من ذلك، فهذا يعني أن الوصول المجهول مفعّل، وأن أي شخص يستطيع الوصول إلى المنفذ 10250 يمكنه القراءة من الحاويات وتنفيذ الأوامر فيها.
أغلق هذا المنفذ أمام الاتصالات الخارجية في كلتا الحالتين. في العقدة المنفردة، يكون العميل الوحيد لـkubelet هو مستوى التحكم الموجود على الجهاز نفسه، وتمر هذه الاتصالات عبر واجهة loopback التي يقبلها ufw افتراضياً. لا يسبب حظر المنفذ 10250 من الإنترنت أي ضرر. إذا وصلت إلى هنا بسبب kubectl top معطّل أو metrics-server لا يستقر، فالأسباب موضحة في أخطاء منفذ kubelet 10250.
ملف kubeconfig لديك هو بيانات اعتماد مسؤول المجموعة
يكتب k3s الملف /etc/rancher/k3s/k3s.yaml بملكية root وبالوضع 600. توضّح الوثائق نتيجة تغيير ذلك: "ملف kubeconfig مملوك لـ root ويُكتب افتراضياً بالوضع 600. يؤدي تغيير الوضع إلى 644 إلى السماح للمستخدمين غير المميزين الآخرين على المضيف بقراءة الملف."
افهم ذلك على النحو التالي: يجعل الوضع 644 كل حساب محلي مسؤولاً عن المجموعة. تقترح أدلة إرشادية كثيرة إجراء ذلك تحديداً، عادةً باستخدام --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.yamlإن 600 root:root هي النتيجة المطلوبة. يحتوي هذا الملف على شهادة عميل لعضو في system:masters، وهي المجموعة التي يتعامل معها خادم API على أنها مسموح لها بلا شروط، لذلك لا تُطبَّق قواعد RBAC عليها. لا يوفّر Kubernetes قائمة لإبطال الشهادات، ما يعني أن النسخة المسربة تظل صالحة إلى أن تدوّر سلطة شهادات المجموعة. تعامل مع الملف كما تتعامل مع مفتاح SSH الخاص، وقلّل عدد الحسابات التي يمكنها الوصول إليه، وهذا هو المبدأ نفسه وراء حسابات المستخدمين وفق مبدأ أقل صلاحية على VPS.
لا يرى جدارك الناري حركة NodePort
تفتح خدمة type: NodePort منفذاً بين 30000 و32767 على كل عنوان يملكه العقدة، بما في ذلك العنوان العام. وتتجاوز خدمة type: LoadBalancer ذلك في k3s: إذ يوزّع ServiceLB، وهو موازن التحميل المضمّن، حاوية صغيرة لكل خدمة في 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 (ترجمة عنوان الشبكة الوجهة) في سلسلة PREROUTING ضمن جدول nat، وتُنفَّذ PREROUTING قبل اتخاذ أي قرار بالتصفية. تصبح الوجهة عنوان حاوية، وليس المضيف، لذلك يرسل kernel الحزمة عبر سلسلة FORWARD ولا تمر أبداً عبر INPUT. توجد قواعد ufw في INPUT، ولذلك لا تصل الحزمة إليها. يظهر ترتيب القفزات هنا:
sudo iptables -S PREROUTING -t nat | head
sudo iptables -S FORWARD | headتوجد KUBE-SERVICES في أعلى PREROUTING، كما توجد قفزات Kubernetes في FORWARD قبل سلاسل ufw نفسها. هذه هي الآلية نفسها التي تتيح لـDocker نشر المنافذ متجاوزاً ufw، والحلول هي نفسها.
- صفِّ الحركة باستخدام جدار الشبكة لدى مزود الخدمة. يعمل هذا الجدار قبل وصول الحركة إلى الجهاز، ولا يتأثر بطريقة توجيه kernel لها.
- تجاوز NodePort وLoadBalancer. اترك الخدمات في
ClusterIP، ووصل إليها باستخدامkubectl port-forwardعبر جلسة SSH الموجودة لديك. - اكشف نقطة دخول واحدة على المنفذين 80 و443، ولا تكشف أي شيء آخر.
- ضيّق النطاق باستخدام
service-node-port-rangeضمنkube-apiserver-arg، حتى يقع أي NodePort غير مقصود في نطاق تراقبه.
يبقى من المفيد تشغيل ufw. فهو يتحكم في الحركة الموجّهة إلى المضيف نفسه، مثل SSH وAPI server. لكنه لا يفرض سياسات على حركة الحاويات، وتوقع قيامه بذلك هو ما يجعل قاعدة البيانات متاحة للوصول.
الحاوية التي تستخدم hostPath أو privileged تملك صلاحيات root على VPS الخاص بك
الحاويات عمليات عادية تعمل على نواتك، لكن مع عرض مقيّد لها. تعيد عدة حقول في pod هذه القيود إلى الخلف.
securityContext.privileged: trueيمنح الحاوية جميع إمكانات Linux وإمكانية الوصول إلى أجهزة المضيف.hostPathيحمّل دليلاً من المضيف داخل pod. ويمكن لـpod الذي يحمّل/بصلاحية القراءة والكتابة أن يضيف مفتاحاً إلى/root/.ssh/authorized_keys.hostPID: trueيضع الحاوية في مساحة أسماء العمليات الخاصة بالمضيف، حيث يؤدي تنفيذnsenterمقابل PID 1 إلى فتح shell على المضيف.hostNetwork: trueيضع الحاوية على مكدس شبكة المضيف، حيث يمكنها ربط منافذ المضيف والوصول إلى الخدمات المرتبطة بواجهة loopback.
لذلك فإن السؤال: «من يستطيع إنشاء pods هنا؟» هو السؤال نفسه: «من يملك صلاحيات root على VPS هذا؟». وأي ServiceAccount يملك create على pods في أي namespace يعادل root، ما لم يرفض شيء pod أولاً.
ذلك الشيء هو Pod Security admission، وهو مدمج في API server. الطريقة السريعة هي استخدام label لكل namespace، ولا تتطلب إعادة التشغيل.
kubectl label namespace default \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/warn=restrictedيرفض baseline الحقول الأربعة السابقة. ويذهب restricted إلى أبعد من ذلك، إذ يفرض استخدام مستخدم غير root، وملف تعريف seccomp (وضع الحوسبة الآمنة)، ومنع تصعيد الصلاحيات، وإسقاط الإمكانات إلى ALL، ما يؤدي إلى تعطيل العديد من charts المنشورة. يتيح لك تفعيل 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
EOFيرفض API 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)لضبط قيمة افتراضية على مستوى المجموعة بدلاً من إضافة label إلى كل namespace، يوضح k3s استخدام ملف إعداد admission في /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، ثم أعد تشغيل k3s:
kube-apiserver-arg:
- 'admission-control-config-file=/var/lib/rancher/k3s/server/psa.yaml'إن استثناء kube-system إلزامي. إذ إن pods الخاصة بـServiceLB في k3s تستخدم منافذ المضيف، وهو ما يمنعه baseline. لذلك يؤدي حذف kube-system من القائمة إلى رفض هذه pods في المرة التالية التي يعيد فيها شيء ما إنشاءها. أبقِ جلسة SSH ثانية مفتوحة عند إعادة تشغيل k3s بعد تغيير إعداد admission.
وهناك فائدة إضافية: يأتي k3s مزوداً بوحدة تحكم لسياسات الشبكة ويفعّلها افتراضياً، لذلك تصبح كائنات NetworkPolicy سارية على هذه المجموعة من دون تثبيت أي مكونات إضافية. لا ينطبق ذلك على كل توزيعات Kubernetes، وهذه هي الأداة التي تمنع pod مخترقاً من الوصول إلى بقية النظام.
أزل المكونات المضمّنة التي لا تستخدمها
ينشر برنامج التثبيت مجموعة من الإضافات. يضيف كل مكوّن منها مستمعاً وعنصراً إضافياً يجب تصحيحه. يقبل --disable القيم التالية: coredns، servicelb، traefik، local-storage، metrics-server، runtimes.
أبقِ على coredns. لا يمكن لأي شيء في الـcluster حل اسم من دونه. أما المكونات الأخرى فهي اختيارية. في /etc/rancher/k3s/config.yaml:
disable:
- traefik
- servicelb
disable-helm-controller: truesudo systemctl restart k3s
kubectl get pods -Aيحذف k3s المكونات التي تعطلها، لذلك تختفي pods الخاصة بـtraefik وpods الخاصة بـsvclb- تلقائياً. افهم العواقب أولاً. عند إزالة servicelb، تبقى كل خدمة type: LoadBalancer عند <pending> إلى الأبد، لأن لا شيء يعيّن لها عنواناً. وعند إزالة traefik، لا يعود هناك ingress controller، لذلك لا تنفذ كائنات Ingress أي إجراء. عطّل هذه المكونات عندما تقدّم حركة الشبكة بطريقة أخرى، مثل استخدام reverse proxy على المضيف، واتركها مفعّلة عندما تستخدمها. يزيل disable-helm-controller: true وحدة التحكم التي تراقب موارد HelmChart، وهي مكوّن ذو صلاحيات مرتفعة لا تحتاج إليه إذا كنت تشغّل helm بنفسك.
إيقاف التحميل التلقائي للرمز المميز الافتراضي لـServiceAccount
يحصل كل pod على رمز مميز لـServiceAccount في /var/run/secrets/kubernetes.io/serviceaccount/token ما لم تحدد خلاف ذلك. لا يملك default ServiceAccount أي أذونات RBAC، لذلك لا يحقق الرمز المميز وحده فائدة كبيرة. لكنه يمنح المهاجم داخل حاوية مخترقة بيانات اعتماد صالحة وخادم API يمكن الوصول إليه، وهذه هي الخطوة الأولى في معظم تقارير تصعيد الصلاحيات داخل العناقيد.
يعطّل دليل تقوية 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اختفى المسار، لذلك يطبع ls القيمة No such file or directory. يحدد workload الذي يحتاج فعلاً إلى الوصول إلى API القيمة automountServiceAccountToken: true في مواصفات pod الخاصة به، لذلك لا يُمنع أي workload بشكل دائم. هذا مكسب صغير بحد ذاته. أما المكسب الأكبر فهو عدم منح workload حساب ServiceAccount يملك أذونات فعلية. ويمكنك معرفة قيمة الرمز المميز حالياً:
kubectl auth can-i --list --as=system:serviceaccount:default:defaultحدود الموارد، حتى لا تتمكن حجرة واحدة من إسقاط العنقود
على عقدة واحدة، تشترك طبقة التحكم وأحمالك في نواة واحدة ومجموعة ذاكرة واحدة. لا تموت الحجرة التي تسرّب الذاكرة وحدها دائماً. يختار قاتل OOM (نفاد الذاكرة) في النواة العملية المستهدفة وفق درجة تميل إلى العمليات الكبيرة، ويُعد k3s عملية كبيرة طويلة التشغيل، لذلك قد يختفي العنقود بدلاً من الحجرة. عندها لا يعيد أي مكوّن تشغيل أحمالك، لأن المكوّن المسؤول عن إعادة تشغيل الأحمال هو نفسه الذي توقف.
يملأ LimitRange الحدود للحجرات التي لم تحدد أي حدود:
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: default
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 50m
memory: 128Miيضع ResourceQuota حداً أقصى لما يمكن لمساحة الأسماء بأكملها حجزه:
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، وغالباً ما تؤثر في العقد المجاورة معها. الحالة الأولى تعني أن حدك يؤدي وظيفته. أما الثانية فهي الحالة التي وُجدت الحدود لمنعها.
فحص ملفات manifests في CI وعنقود k3s وفق جدول زمني
يجب إجراء الفحص في موضعين، لأن كل موضع يكتشف مشكلات مختلفة. ثبّت Trivy أولاً. يوفّر المشروع حزمة Debian مع كل إصدار، وكان الإصدار 0.74.0 هو الأحدث في August 2026، لذلك تحقّق من صفحة الإصدارات بحثاً عن إصدار أحدث قبل تثبيته في إعدادات التشغيل الآلي.
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الموضع الأول هو ملفات manifests، قبل وصولها إلى العنقود.
trivy fs --scanners misconfig --severity HIGH,CRITICAL --exit-code 1 ./deployتُفشل --exit-code 1 مهمة CI (التكامل المستمر) عند العثور على مشكلة. تسمي كل نتيجة الحقل الذي تسبب في الفشل وشدته، لذلك تفشل عملية البناء بدلاً من وصولها إلى خادم API إذا احتوى privileged: true على شيء لم تقصد إيداعه. ضع المشكلة التي قررت قبولها في ملف .trivyignore، وبذلك يبقى هذا القرار في git بجوار ملف manifest الذي تسبب فيها.
الموضع الثاني هو العنقود قيد التشغيل، وفق جدول زمني.
trivy k8s --compliance=k8s-cis-1.23 --report summaryتوجد ملاحظة مهمة حول هذا الأمر. تنشر trivy k8s حاوية pod لجمع معلومات العقدة، وتحتاج إلى وصول إلى المضيف لفحص الإعدادات على مستوى العقدة. إذا كنت قد بدأت للتو في رفض حاويات pod ذات الامتيازات في عنقودك، فانتبه إلى ذلك بدلاً من التحايل عليه. تتجاوز trivy k8s --report summary --disable-node-collector حاوية الجمع، وتفقد معها الفحوصات على مستوى العقدة.
يغطي kube-bench جانب المضيف: أذونات الملفات ورايات العمليات، وهو الجانب الذي يوجد فيه معظم معيار CIS (مركز أمن الإنترنت) فعلياً. ويتضمن ملف تعريف خاصاً بـk3s. خذ ملف Job من المشروع الأصلي وعدّله.
curl -sfL -o kube-bench-job.yaml https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yamlغيّر أمر الحاوية إلى ["kube-bench", "--benchmark", "k3s-cis-1.7"]، واستبدل نقطتي التحميل /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 بالإضافة إلى عمليات تحميل لأدلة من المضيف، وهو شكل pod نفسه الذي بدأت برفضه قبل قسم واحد. شغّله في مساحة الأسماء المستثناة kube-system، واقرأ المخرجات، ثم kubectl delete job kube-bench. لا يُعدّ احتياج أداة الفحص إلى الامتيازات سبباً لإيقاف حظر أحمال العمل ذات الامتيازات.
محتويات الصور مسألة منفصلة. يقرأ trivy image ghcr.io/example/app:1.4 قاعدة حزم البرامج داخل الصورة، ويسرد الحزم المعروفة بوجود ثغرات فيها. وهذه هي المهمة نفسها التي تنفذها عندما تفحص خادمك بحثاً عن CVEs معروفة.
والآن إلى الجزء الواقعي بشأن مخرجات أدوات الفحص. سيفشل عنقود هواية مكوّن من عقدة واحدة في قائمة طويلة من عناصر التحكم في CIS، ومعظم هذه الإخفاقات صحيحة وغير مهمة لك في الوقت نفسه. كُتب المعيار لعنقود متعدد العقد ومتعدد المستأجرين: etcd على مضيفين منفصلين، وسجلات تدقيق تُرسل خارج الجهاز، وسلطة شهادات منفصلة لـkubelet، وإضافات قبول مخصصة لنظام امتثال لا تخضع له. يشغّل k3s عمداً مستوى التحكم كعملية واحدة بملف إعداد واحد، لذلك لا يمكن أن تنجح عناصر التحكم التي تتحقق من أذونات ملف manifest الخاص بـkube-scheduler، لأنه لا يوجد مثل هذا الملف.
اقرأ الإخفاقات بالترتيب التالي، وتوقف عندما تنفد الفائدة: أوضاع الملفات وملكيتها ضمن /etc/rancher و/var/lib/rancher، وكل ما يذكر الوصول المجهول أو غير الموثّق، وكل ما يفيد بأن مكوّناً يستمع على 0.0.0.0، وأي حاوية تعمل بالمعرّف UID 0 دون سبب واضح. أما الباقي فيمكن أن ينتظر عقدة ثانية أو شخصاً ثانياً لديه صلاحية الوصول. تقرير من مئة سطر تتجاهله أقل قيمة من تقرير من خمسة أسطر تتصرف بناءً عليه.
ترتيب تعزيز أمان k3s لعقدة واحدة
- اضبط السياسة الافتراضية للاتصالات الواردة في ufw على الرفض، واسمح بـSSH، واسمح بالاتصالات إلى 6443 من عنوانك فقط، واسمح بشبكتي الـpod والـservice.
- انسخ kubeconfig إلى حساب المستخدم لديك بالوضع 600، ولا تضبط
--write-kubeconfig-mode 644مطلقاً. - تأكد من أن kubelet على 10250 يرفض الطلبات المجهولة، وأبقِ المنفذ غير متاح عبر الإنترنت.
- عطّل المكونات المضمّنة التي لا تستخدمها، ثم أعد تشغيل k3s.
- أضف تسميات إلى namespaces لاستخدام Pod Security admission عبر
enforce=baselineوwarn=restricted. - عطّل الإلحاق التلقائي لرموز ServiceAccount الافتراضية.
- أضف LimitRange وResourceQuota، واحجز موارد CPU والذاكرة لـk3s.
- أضف
trivy fs --scanners misconfigإلى CI، ونفّذ فحص CIS شهرياً.
يحتاج المضيف الأساسي إلى العناية نفسها التي يحتاج إليها أي خادم آخر، ويضيف k3s تعقيداً في هذا الجانب. فقد ثُبّت بواسطة script بدلاً من apt، لذلك لا يتعامل apt upgrade معه مطلقاً. أبقِ نظام التشغيل محدّثاً وفق جدول تحديث مستقل باستخدام التحديثات غير المراقبة على Ubuntu، وحدّث k3s عمداً عبر إعادة تشغيل installer الخاص به مع channel أو version المطلوب. من السهل نسيان وجود مساري تحديث على جهاز واحد، لذلك دوّن المكوّن الذي يتبع كل مسار.
FAQ
هل من الآمن إتاحة خادم k3s API على المنفذ 6443 عبر الإنترنت؟
ليس ذلك باباً مفتوحاً بحد ذاته، لأن خادم API يتطلب شهادة عميل أو رمزاً مميزاً، ويرفض كل ما عدا ذلك بالخطأ forbidden: User "system:anonymous". لكن يبقى خطران. لا يزال بإمكان المتصلين المجهولين قراءة /version، ما يخبر أداة الفحص بالإصدار المحدد من Kubernetes الذي ينبغي البحث عنه. كما أن أي ثغرة مستقبلية في خادم API تصبح قابلة للوصول عن بُعد ما دام المنفذ مفتوحاً. في عنقود أحادي العقدة، لا يحتاج أي طرف خارجي إلى 6443 باستثناء kubectl الخاص بك، لذلك اسمح بالوصول من عنوانك باستخدام sudo ufw allow from YOUR_IP to any port 6443 proto tcp، واترك سياسة الرفض الافتراضية تتولى الباقي.
لماذا لا تحظر قاعدة ufw لدي خدمة NodePort؟
لأن الحزمة لا تصل إلى السلسلة التي توجد فيها قاعدتك. يضع kube-proxy قواعد DNAT في جدول nat، في سلسلة PREROUTING التي تعمل أولاً وتعيد كتابة الوجهة إلى عنوان pod. بعد ذلك تُمرَّر الحزمة بدلاً من تسليمها محلياً، ولذلك تعبر FORWARD وتتجاوز INPUT، حيث توجد قواعد ufw. أكّد ذلك باستخدام sudo iptables -S PREROUTING -t nat | head. طبّق تصفية NodePort في جدار الشبكة لدى مزود الخدمة، أو تجنّب type: NodePort، واستخدم kubectl port-forward للوصول إلى الخدمات بدلاً منه.
هل ينبغي أن أشغّل k3s باستخدام --write-kubeconfig-mode 644؟
لا. توضّح وثائق k3s ما يفعله ذلك: "سيؤدي تغيير الوضع إلى 644 إلى السماح للمستخدمين الآخرين غير ذوي الامتيازات على المضيف بقراءته." يحتوي ذلك الملف على شهادة عميل لـ system:masters، ولذلك يجعل الوضع 644 كل حساب محلي مسؤولاً عن العنقود. انسخه إلى مستخدم واحد بدلاً من ذلك باستخدام sudo install -o "$USER" -g "$USER" -m 600 /etc/rancher/k3s/k3s.yaml ~/.kube/config، واترك الملف الأصلي بالوضع 600 root:root.
ما الاختبار المعياري في kube-bench الذي ينبغي أن أستخدمه مع k3s؟
استخدم k3s-cis-1.7. تنص وثائق kube-bench على ما يلي: "يتضمن kube-bench اختبارات معيارية لمنصة Rancher K3S. لتشغيل ذلك، يجب تحديد --benchmark k3s-cis-1.7 عند تشغيل أمر kube-bench." مرّره صراحةً، لأن الاكتشاف التلقائي يفترض بنية kubeadm، بينما يحتفظ k3s بملفاته ضمن /etc/rancher و/var/lib/rancher. توقّع ظهور إخفاقات لا تنطبق على عقدة واحدة، وابدأ بمعالجة صلاحيات الملفات والوصول المجهول.
هل سيؤدي تفعيل Pod Security admission إلى تعطيل مكونات k3s المضمّنة؟
سيحدث ذلك إذا فرضته على kube-system. تطالب pods الخاصة بـServiceLB التي ينشئها k3s بالمنافذ الخاصة بالمضيف لخدمات type: LoadBalancer، بينما يمنع baseline استخدام منافذ المضيف، ولذلك تُرفض تلك pods عند إعادة إنشائها في المرة التالية. استثنِ kube-system في ملف إعدادات admission، أو طبّق Pod Security باعتبارها labels على namespaces التي تملكها. ابدأ باستخدام enforce=baseline وwarn=restricted حتى تتمكن من قراءة ما الذي سيتسبب restricted في تعطيله قبل تفعيله.