EduFlow

الوحدة 2 · Images جاهزة للإنتاج

Base images صغيرة ومستخدمون غير root

20 دقراءة 4 داكتشف الخطأ

الدرس في لمحة

اختر base image لـ notes-api بالموازنة بين slim وAlpine وdistroless، وشغّل التطبيق بمستخدم غير root، وأحكِم إغلاق الـ container أكثر وقت التشغيل.

  1. 1اختيار الـ base
  2. 2توقّف عن التشغيل بصلاحيات root
  3. 3نسخة distroless
  4. 4أحكِم الإغلاق وقت التشغيل

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

في 2024 فشلت image لـ Skylane في مراجعة أمنية أجراها أحد العملاء، مع 140 نتيجة. أقل من خمس منها كانت في كود كتبناه أو استدعيناه. والباقي في حزم صادف أن الـ base image تتضمّنها: مكتبة XML، ومحوّل صور، ووكيل لنقل البريد. لا شيء منها كان يعمل، وكلها كانت مشكلتنا. الـ base image اعتمادية ترثها بالجملة، فاخترها عن قصد.

اختيار الـ base

الـ Base ما بداخلها مناسبة لـ انتبه إلى
node:24 Debian كاملة، ومترجِمات، ومكتبات كثيرة مراحل البناء التي تترجم إضافات أصلية كبيرة؛ وفيها الكثير ليُفحص
node:24-slim Debian مصغّرة + Node معظم images التشغيل ما زال فيها shell وapt
node:24-alpine Alpine Linux ‏(musl) + Node images صغيرة جدًا الفروق بين musl وglibc
gcr.io/distroless/nodejs24-debian13 Node ومكتبات تشغيله فقط images إنتاج محصّنة لا shell ولا مدير حزم

خياري الافتراضي لفريق جديد على الـ containers هو slim: صغيرة، ومبنية على glibc فتعمل الوحدات الأصلية الجاهزة، وما زلت تستطيع الدخول إليها بـ docker exec وفتح shell فيها حين تقع أزمة في الإنتاج.

تعطيك Alpine images أصغر، لكنها تستخدم مكتبة C ‏musl بدلًا من glibc. الإضافات الأصلية الجاهزة المترجَمة لـ glibc تفشل في التحميل، أما الفروق الدقيقة في تحليل أسماء DNS وفي حجز الذاكرة فقد سبّبت لفرق كثيرة متاعب في الإنتاج. اخترها حين تكون قد اختبرت شجرة اعتمادياتك كاملة عليها، لا لأن الرقم في صفحة الـ tags أصغر.

الـ images من نوع Distroless تحتوي على Node.js والمكتبات التي يحتاج إليها، ولا شيء غير ذلك: لا shell، ولا apt، ولا curl. المهاجم الذي يحصل على تنفيذ كود يجد القليل جدًا ليعمل به، والماسح يجد القليل جدًا ليُبلغ عنه. الثمن هو صعوبة التشخيص. لا تستطيع فتح shell داخلها بـ exec، وستتعلّم طريقة Kubernetes للالتفاف على ذلك، kubectl debug، في الدرس الأخير من هذه الدورة.

توقّف عن التشغيل بصلاحيات root

افتراضيًا، تعمل العملية داخل الـ container كـ root، أي UID 0. الـ namespaces تحدّ ما يستطيع هذا الـ root رؤيته، لكنه يبقى root في نظر النواة. وإذا اجتمع ذلك مع خلل في النواة أو mount مضبوط خطأً، فهكذا يتحوّل اختراق الـ container إلى اختراق للمضيف. التشغيل كمستخدم بلا صلاحيات يحوّل كثيرًا من تلك الثغرات إلى طرق مسدودة.

تأتي الـ images الرسمية لـ Node.js بمستخدم اسمه node رقمه UID 1000. انتقل إليه في مرحلة التشغيل، بعد كل ما يحتاج إلى root:

Dockerfile
# …build and deps stages from the previous lesson

FROM node:24-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY package.json ./
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

لاحظ ما ليس موجودًا: لا --chown=node:node على عمليات النسخ. تبقى الملفات مملوكة لـ root وقابلة للقراءة من الجميع، فيستطيع التطبيق قراءة كوده لكنه لا يستطيع تغييره. إذا وجد مهاجم ثغرة تنفيذ كود عن بُعد، فلن يستطيع إعادة كتابة dist/server.js ليبقى داخل النظام. امنح التطبيق صلاحية الكتابة فقط حيث يحتاج إليها حقًا، وفضّل /tmp أو volume مُركّبًا لذلك.

نسخة distroless

هذه مرحلة التشغيل نفسها على distroless. الـ tag ‏:nonroot يعمل بـ UID 65532 دون أي إعداد إضافي، ونقطة الدخول في الـ image هي node أصلًا، لذا لا تذكر CMD إلا السكربت:

Dockerfile
FROM gcr.io/distroless/nodejs24-debian13:nonroot AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY package.json ./
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["dist/server.js"]

ما زالت مرحلتا build وdeps تستخدمان node:24-slim؛ وحدها المرحلة المشحونة تتغيّر. أبقِ الإصدار الرئيسي لـ Node متطابقًا عبر المراحل؛ وإلا فإن الوحدات الأصلية (native modules) المترجَمة وفق الـ ABI الخاص بإصدار معيّن من Node ستُحمَّل في إصدار آخر وتفشل عند بدء التشغيل.

تستطيع التأكّد من المستخدم الذي سيعمل به الـ container دون تشغيله:

Terminal
docker image inspect notes-api:1.0.0 --format '{{.Config.User}}'

أحكِم الإغلاق وقت التشغيل

الـ image تضبط القيم الافتراضية؛ والـ runtime يستطيع تشديدها أكثر:

Terminal
docker run --rm -p 8080:3000 \
  --read-only --tmpfs /tmp \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  notes-api:1.0.0

الخيار --read-only يجعل نظام الملفات الجذري للـ container غير قابل للتعديل، ولا يبقى قابلًا للكتابة إلا /tmp بوصفه mount صغيرًا في الذاكرة. والخيار --cap-drop ALL يزيل صلاحيات Linux ‏(capabilities) التي تحتاج إليها العمليات المميّزة الخاصة بالـ root. وno-new-privileges يمنع العملية من اكتساب صلاحيات عبر ملفات setuid التنفيذية. في القسم الخامس ستضبط الأشياء نفسها في securityContext ضمن Kubernetes، حيث تصبح سياسة لكل Pod.

الـ image لديك الآن صغيرة وبلا صلاحيات. التالي: كيف تسمّيها بحيث يكون ما تنشره هو بالضبط ما اختبرته.

أهم النقاط

  • كل حزمة في الـ base image شيء عليك ترقيعه وفحصه والدفاع عنه، لذا ابدأ من أصغر base يعمل عليه تطبيقك فعلًا.
  • الـ image ‏node:24-slim خيار افتراضي متين؛ وAlpine تستخدم musl بدلًا من glibc، وdistroless تزيل الـ shell ومدير الحزم كليًا.
  • الـ images الرسمية لـ Node.js تتضمّن مستخدمًا اسمه node ‏(UID 1000)؛ انتقل إليه بـ USER node بعد الخطوات التي تحتاج إلى root.
  • اترك ملفات التطبيق مملوكة لـ root وللقراءة فقط بالنسبة إلى مستخدم التطبيق، حتى لا تستطيع عملية مخترَقة إعادة كتابة كودها.
  • أضف تحصينات وقت التشغيل مثل --read-only و--cap-drop ALL وno-new-privileges؛ ولـ Kubernetes الضوابط نفسها في securityContext.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

يعتمد notes-api على حزمة فيها إضافة أصلية (native addon). بعد تغيير الـ base من node:24-slim إلى node:24-alpine، ينهار الـ container عند البدء بخطأ في تحميل ملف .node. ما السبب المرجّح؟

السؤال 2 من 4

أي تغيير يجعل notes-api يعمل بمستخدم غير root في الـ image الرسمية لـ Node.js؟

السؤال 3 من 4

تنقل notes-api إلى gcr.io/distroless/nodejs24-debian13:nonroot. أي CMD صحيحة؟

السؤال 4 من 4

لماذا نترك ملفات dist/ في notes-api مملوكة لـ root مع أن العملية تعمل كمستخدم node؟

تدريب

اكتشف الخطأ

الإصلاح المؤقت الذي وصل إلى الإنتاج

رصدت مراجعة أمنية في هذه الـ image الملاحظة التالية: “container process runs as UID 0”. كاتب الملف يصرّ على أنه انتقل إلى المستخدم node. اختر السطر الذي يجعل الـ container يعمل بصلاحيات root، وفكّر في الإصلاح الصحيح للخطأ الأصلي.

حدّد السطر أو الأسطر التي تظنها خاطئة، ثم تحقّق.