Docker المتقدم وأمن الحاويات

تشغيل الحاويات بأمان

18 دقيقة الدرس 6 من 28

تشغيل الحاويات بأمان

الحاوية ليست آلة افتراضية. فهي تشارك نواة المضيف، وبالتالي فإن عملية مخترقة داخل حاوية بإعدادات تشغيل خاطئة قادرة على الهروب إلى المضيف، وقراءة الأسرار من حاويات أخرى، أو التحرك عبر المجموعة بأكملها. يتناول هذا الدرس ضوابط التشغيل الأربعة التي يطبقها كل فريق إنتاج بمعيار شركات التقنية الكبرى كخط أساسي: المستخدمون غير الجذريون، وأنظمة الملفات للقراءة فقط، وصلاحيات Linux، وملفات seccomp / AppArmor. كل طبقة تضيّق نطاق الضرر في حال الاختراق بشكل مستقل؛ ومجتمعةً تُنشئ دفاعاً متعمّق الطبقات يصعب اختراقه.

لماذا يُعدّ root داخل الحاوية خطيراً؟

افتراضياً، تعمل العمليات داخل حاوية Docker بصلاحيات UID 0 (root). توفر مساحات أسماء المستخدمين (User Namespaces) بعض العزل، لكن إن وُجدت ثغرة في الهروب من الحاوية — وقد حدث ذلك مراراً — فإن عملية حاوية بصلاحيات root تتطابق مع root على المضيف مباشرةً. وهذا يعني الوصول الكامل لكل ملف على المضيف، وكل حجم مُركّب، وكل عملية تعمل.

الحل هو تعريف مستخدم غير جذري في Dockerfile وإنشاء المستخدم بمعرف UID مرتفع وثابت. تستخدم صور شركات التقنية الكبرى باستمرار معرفات UID في النطاق 10000–65534 لتجنب التعارض مع حسابات نظام المضيف:

# ---- بناء متعدد المراحل: البناء بصلاحيات root، والتشغيل بصلاحيات غير جذرية ---- FROM golang:1.22-alpine AS builder WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -o /app ./cmd/server FROM scratch # نسخ الملف التنفيذي فقط COPY --from=builder /app /app # في صور scratch لا يوجد adduser، لذا نحدد USER مباشرة. # نستخدم UID رقمياً لا معنى له الخاص على أي مضيف. USER 65532:65532 ENTRYPOINT ["/app"]

للصور التي تحتوي على shell (Alpine، Debian-slim)، أنشئ المستخدم صراحةً لضمان ثبات مجلده الرئيسي ومعرفه:

FROM node:20-alpine # إنشاء مجموعة ومستخدم مخصصَين بـ UID/GID ثابتَين RUN addgroup -S appgroup && adduser -S -G appgroup -u 10001 appuser WORKDIR /app COPY --chown=appuser:appgroup package*.json ./ RUN npm ci --omit=dev COPY --chown=appuser:appgroup . . # التبديل إلى مستخدم غير جذري قبل نقطة الدخول USER 10001 EXPOSE 3000 CMD ["node", "server.js"]
تجنّب استخدام USER nobody. يُشارَك مستخدم nobody (UID 65534) من قِبَل كثير من خدمات النظام وقد يملك صلاحيات غير متوقعة على بعض إعدادات المضيف. استخدم بدلاً من ذلك مستخدماً مخصصاً بـ UID ثابت وموثّق.

أنظمة الملفات للقراءة فقط

حتى مع وجود مستخدم غير جذري، يمكن لعملية مخترقة كتابة ملفات ثنائية خبيثة في /tmp، أو الكتابة فوق ملفات التطبيق، أو تثبيت أدوات استمرارية، إذا كان نظام الملفات قابلاً للكتابة. يمنع تركيب نظام الملفات بالكامل للقراءة فقط باستخدام --read-only هذا على مستوى نظام التشغيل — لن ينجح أي chmod أو mv أو سكريبت shell مُسقَط بعد العملية الحالية.

معظم التطبيقات تحتاج إلى بعض المساحة القابلة للكتابة — لـ /tmp وملفات PID والذاكرة التخزينية. النمط الصحيح هو منح الكتابة فقط لمجلدات محددة ومعروفة باستخدام tmpfs، مع إبقاء كل شيء آخر ثابتاً:

# التشغيل مع نظام ملفات جذري للقراءة فقط. # منح tmpfs قابل للكتابة فقط حيث يحتاجه التطبيق فعلياً. docker run -d \ --name api \ --read-only \ --tmpfs /tmp:rw,size=32m,noexec,nosuid \ --tmpfs /run:rw,size=4m,noexec,nosuid \ --user 10001:10001 \ myorg/api:v2.4.1 # للتحقق: حاول الكتابة إلى / — يجب أن تفشل docker exec api touch /pwned # يجب أن يُرجع: Read-only file system

في Kubernetes يُضبط المكافئ في سياق أمان الـ pod:

# kubernetes/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: api spec: template: spec: securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 containers: - name: api image: myorg/api:v2.4.1 securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false volumeMounts: - name: tmp mountPath: /tmp - name: run mountPath: /run volumes: - name: tmp emptyDir: medium: Memory sizeLimit: 32Mi - name: run emptyDir: medium: Memory sizeLimit: 4Mi
يحجب ضبط allowPrivilegeEscalation: false في Kubernetes ملفات setuid ثنائية وأوامر sudo داخل الحاوية، حتى لو كانت موجودة في الصورة. هذا مستقل عن نظام الملفات للقراءة فقط ويجب دائماً ضبطه جنباً إلى جنب.

صلاحيات Linux — تقليص الامتيازات بدقة

Root ليس مفتاحاً واحداً. تُقسّم نواة Linux امتياز root إلى نحو 40 صلاحية مستقلة — CAP_NET_BIND_SERVICE (ربط المنافذ أقل من 1024)، وCAP_SYS_PTRACE (إلحاق المحليات)، وCAP_SYS_ADMIN (كل شيء تقريباً)، وغيرها. يمنح Docker حاوية مجموعة افتراضية من نحو 14 صلاحية. هذه المجموعة أضيق من الجذر الكامل لكنها أوسع بكثير مما تحتاجه معظم عمليات التطبيق.

النهج الإنتاجي هو إسقاط جميع الصلاحيات، ثم إضافة ما تحتاجه العملية فعلاً فقط:

# إسقاط جميع الصلاحيات، ثم إضافة NET_BIND_SERVICE فقط # (مطلوبة إذا ربطت العملية المنفذ 443 مباشرة) docker run -d \ --name nginx \ --user 65532 \ --read-only \ --cap-drop ALL \ --cap-add NET_BIND_SERVICE \ myorg/nginx:1.27 # معظم خدمات API عديمة الحالة لا تحتاج أي صلاحيات: docker run -d \ --name grpc-service \ --user 10001 \ --read-only \ --cap-drop ALL \ myorg/grpc-svc:latest
Linux Capabilities: Default Set vs Hardened Set Docker Default (~14 caps) CAP_SYS_ADMIN CAP_NET_RAW CAP_SYS_PTRACE CAP_CHOWN CAP_KILL CAP_SETUID CAP_NET_BIND_SERVICE ... 7 more سطح الهجوم: واسع CAP_NET_RAW وحدها تُتيح انتحال ARP CAP_SYS_ADMIN تُتيح تركيب أنظمة الملفات تصليب مُصلَّب (--cap-drop ALL) لا صلاحيات (API نموذجية) + CAP_NET_BIND_SERVICE (إن لزم) سطح الهجوم: ضئيل لا انتحال ARP ممكن لا تركيب أنظمة ملفات ممكن لا ptrace / حقن في العمليات
إسقاط جميع صلاحيات Linux وإعادة إضافة ما تحتاجه العملية فقط يُقلّص سطح هجوم النواة من نحو 14 صلاحية إلى صفر في الغالب.
نفّذ docker run --rm -it --cap-drop ALL ubuntu:24.04 capsh --print لفحص مجموعة الصلاحيات بشكل تفاعلي. استخدم pscap (من حزمة libcap-ng-utils) على المضيف للتحقق من الصلاحيات التي تمتلكها عمليات الحاوية فعلاً.

Seccomp — تصفية استدعاءات النظام

حتى مع إسقاط جميع الصلاحيات، يمكن لعملية داخل الحاوية استدعاء مئات استدعاءات نظام Linux. يمكن لعملية مخترقة استخدام استدعاءات مثل ptrace وkeyctl وclone بأعلام محددة لمحاولة رفع الامتيازات. Seccomp (Secure Computing Mode) هو ميزة في نواة Linux تُقيّد استدعاءات النظام التي يجوز للعملية استخدامها. يُوفّر Docker ملف seccomp افتراضي يحجب نحو 44 استدعاءاً خطيراً مع السماح بكل ما تحتاجه التطبيقات الطبيعية.

لضمان أعلى مستوى، أنشئ ملف شخصي ضيّقاً مخصصاً للتطبيق. سير العمل: شغّل تطبيقك في وضع SCMP_ACT_LOG لتسجيل كل استدعاءات النظام التي يُجريها، ثم أنشئ قائمة سماح من ذلك السجل. أدوات مثل oci-seccomp-bpf-hook أو Falco يمكنها أتمتة خطوة التسجيل:

# تطبيق ملف seccomp مخصص عند التشغيل docker run -d \ --name api \ --security-opt seccomp=/etc/docker/seccomp/api-profile.json \ myorg/api:v2.4.1 # استخدام ملف Docker الافتراضي صراحةً (نفس نتيجة حذف العلامة في معظم الحالات) docker run -d \ --security-opt seccomp=default \ myorg/api:v2.4.1 # تعطيل seccomp كلياً — للتشخيص فقط، ليس في الإنتاج أبداً docker run --security-opt seccomp=unconfined myorg/api:v2.4.1

ملف JSON لـ seccomp بنية الحد الأدنى تبدو كالتالي — يرفض افتراضياً ويسمح باستدعاءات محددة بالاسم (نهج قائمة السماح):

{ "defaultAction": "SCMP_ACT_ERRNO", "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"], "syscalls": [ { "names": [ "accept4", "bind", "brk", "clone", "close", "connect", "epoll_create1", "epoll_ctl", "epoll_wait", "execve", "exit", "exit_group", "fcntl", "fstat", "futex", "getpid", "getrandom", "getsockname", "getsockopt", "listen", "mmap", "mprotect", "munmap", "nanosleep", "open", "openat", "poll", "prctl", "read", "recvfrom", "recvmsg", "rt_sigaction", "rt_sigprocmask", "sendmsg", "sendto", "setitimer", "setsockopt", "sigaltstack", "socket", "stat", "tgkill", "uname", "write" ], "action": "SCMP_ACT_ALLOW" } ] }

AppArmor — التحكم الإلزامي في الوصول للمسارات والشبكات

AppArmor (Application Armor) هو وحدة أمان Linux تفرض سياسة تحكم إلزامي في الوصول فوق صلاحيات Unix التقديرية. بينما يعمل seccomp على مستوى استدعاء النظام، يعمل AppArmor على مستوى المورد — يتحكم في الملفات والمجلدات والمقابس والعمليات الشبكية التي يُسمح لملف تنفيذي بعينه بالوصول إليها، بغض النظر عن مستخدم Unix الذي يُشغّله.

يُطبّق Docker ملف AppArmor افتراضي يُسمّى docker-default على كل حاوية في الأنظمة التي يعمل فيها AppArmor (Ubuntu، Debian، openSUSE). يمكنك تحميل ملف مخصص وتطبيقه لكل حاوية:

# تحميل ملف AppArmor مخصص (يُنفَّذ مرة واحدة على المضيف) apparmor_parser -r -W /etc/apparmor.d/docker-api-profile # تطبيق الملف عند تشغيل الحاوية docker run -d \ --name api \ --security-opt apparmor=docker-api-profile \ myorg/api:v2.4.1 # التحقق من الملف النشط docker inspect api | grep -i apparmor

ملف AppArmor إنتاجي لخدمة Go HTTP يبدو كالتالي — يُدرج المسارات التي تمسّها العملية فعلاً فقط:

#include <tunables/global> profile docker-api-profile flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base> network inet tcp, network inet udp, # الملف التنفيذي والمكتبات المشتركة /app r ix, /lib/** mr, /usr/lib/** mr, # الكتابة فقط إلى مجلد tmp المخصص /tmp/** rw, /run/** rw, # حجب كل شيء آخر deny /proc/** w, deny /sys/** w, deny /etc/shadow r, }
AppArmor وseccomp متكاملان وليسا بديلَين. يُصفّي seccomp على مستوى رقم استدعاء النظام؛ يُصفّي AppArmor على مستوى مسار المورد وعملية الشبكة. تأخذ gVisor من Google وFirecracker من Amazon العزل أبعد من ذلك، بتشغيل الحاويات داخل نواة معزولة — سياق مهم حين تنتقل إلى بيئات متعددة المستأجرين عالية الضمان.

الجمع بين الضوابط — أمر تشغيل مُصلَّب بالكامل

تتكامل هذه الضوابط الأربعة بسلاسة. إليك أمر إطلاق حاوية إنتاجية يُطبّق جميعها في آنٍ واحد:

docker run -d \ --name api \ --user 10001:10001 \ --read-only \ --tmpfs /tmp:rw,size=32m,noexec,nosuid,uid=10001 \ --cap-drop ALL \ --cap-add NET_BIND_SERVICE \ --security-opt no-new-privileges:true \ --security-opt seccomp=/etc/docker/seccomp/api-tight.json \ --security-opt apparmor=docker-api-profile \ --restart unless-stopped \ myorg/api:v2.4.1

يستحق العلم --security-opt no-new-privileges:true إشارة خاصة: يضبط بتة PR_SET_NO_NEW_PRIVS في prctl، مما يمنع أي عملية في الحاوية — بما في ذلك العمليات الفرعية — من اكتساب امتيازات إضافية عبر execve أو الملفات الثنائية setuid أو صلاحيات نظام الملفات على الملفات التنفيذية. إنها شبكة الأمان الأخيرة بعد كل الضوابط السابقة.

أتمت الامتثال لهذه الضوابط باستخدام Open Policy Agent / Gatekeeper في Kubernetes. أنشئ ConstraintTemplate يرفض الـ pods التي تفتقر إلى readOnlyRootFilesystem: true وrunAsNonRoot: true وallowPrivilegeEscalation: false. أجرِ التحقق من السياسات في CI باستخدام conftest قبل أن تصل أي بيان إلى المجموعة.