الصور عديمة التوزيع والصور الخفيفة
الصور عديمة التوزيع والصور الخفيفة
كل ميغابايت تُضيفه إلى صورة الحاوية هو سطح هجوم إضافي، وزمن إقلاع أطول، وتكلفة نقل بيانات أعلى. على مقياس جوجل — حيث تُطلَق مئات الآلاف من حالات الحاوية كل دقيقة — يُعدّ اختيار الصورة الأساسية قراراً أمنياً واستقرارياً من الدرجة الأولى، لا مجرد تفصيلة هامشية. يتناول هذا الدرس الاستراتيجيات الثلاث السائدة للصور الخفيفة: scratch وAlpine Linux وDistroless، ومتى تُستخدم كل منها، وكيف تُمكِّن الملفات الثنائية الثابتة من تحقيق أقصى درجات التقليص.
تكلفة الصورة الأساسية الكاملة
تزن صورة ubuntu:22.04 القياسية نحو 77 ميغابايت مضغوطة، وتحمل معها صدفةً وأداة حزم وأدوات نظام ومئات المكتبات. لا يحتاج إليها أي خادم في وقت التشغيل. وُجدت هذه الأدوات لراحة كاتب الصورة أثناء التصحيح — وهي في الوقت ذاته تُيسّر مهمة المهاجم. كل ثغرة في bash أو curl أو openssl أو apt تصبح ثغرتك، حتى لو لم يستدعِ تطبيقك هذه الملفات الثنائية قط.
الاستراتيجية الأولى — FROM scratch
scratch هي الصورة الأساسية الفارغة في Docker: لا نظام ملفات، لا صدفة، لا مكتبة libc. حاوية مبنية FROM scratch لا تحتوي إلا على ما تنسخه أنت بأمر COPY. هذا هو الحد الأدنى المطلق.
تعمل هذه الاستراتيجية بإتقان مع الملفات الثنائية المُجمَّعة بشكل ثابت — وهي البرامج التي دُمجت كل تبعياتها في الملف الثنائي نفسه دون الحاجة إلى مكتبات مشتركة من نظام التشغيل. Go مثال نموذجي: أمر go build مع CGO_ENABLED=0 ينتج ملفاً ثنائياً ELF مكتفياً بذاته يعمل من scratch دون حاجة إلى أي ملفات أخرى سوى شهادات CA وبيانات المناطق الزمنية.
تتراوح الصورة الناتجة عادةً بين 5 و15 ميغابايت. لا صدفة، لا ps، لا wget — المهاجم الذي تمكّن من اختراق منطق التطبيق لن يجد له موطئ قدم. خيار -ldflags="-s -w" يُزيل جدول الرموز ومعلومات تصحيح DWARF مما يُصغّر الملف الثنائي أكثر.
/etc/passwd: كثير من التطبيقات تستدعي getpwnam() أو تعتمد على إمكانية تحليل معرّف مستخدم غير جذري. صورة scratch لا تحتوي على /etc/passwd أو /etc/group. إما أن تشغّل العملية بمعرّف رقمي (USER 65532 في Dockerfile) أو أن تنسخ ملفات passwd/group بسيطة من مرحلة البناء. فحص Kubernetes لـ runAsNonRoot: true يعتمد المعرّف الرقمي، فالأمان مضمون.
الاستراتيجية الثانية — Alpine Linux
Alpine توزيعة Linux خفيفة بحجم 5 ميغابايت تعتمد musl-libc. على عكس scratch، تمتلك صدفة (ash) ومدير حزم (apk) ونظام ملفات كافياً لجعل التصحيح ممكناً. هي الخيار العملي حين يتطلب وقت تشغيل لغتك ربطاً ديناميكياً — Python وNode.js وRuby وJava كلها تحتاج إلى مكتبات مشتركة لا تستطيع scratch توفيرها دون تدخل يدوي.
انضباط Alpine: استخدم دائماً --no-cache مع apk add لتجنب كتابة فهرس الحزم في الطبقة، حدّد الإصدارات بدقة في requirements.txt، ولا تُبقِ حزم البناء (gcc، musl-dev) في المرحلة النهائية. تظل صور Alpine تحمل صدفة وapk، مما يجعلها أضعف من Distroless في المسح الأمني رغم صغر حجمها.
الاستراتيجية الثالثة — Distroless
صور Distroless التي تُديرها جوجل هي النقطة المثلى لمعظم أعباء الإنتاج. تحتوي على وقت تشغيل اللغة وتبعياته فقط — لا صدفة، لا مدير حزم، لا أدوات نظام أساسية. متاحة لـ Go وPython وJava وNode.js و.NET، وهناك نسخة ثابتة. تُبنى من حزم Debian باستخدام rules_pkg من Bazel، فتستفيد من تصحيحات أمان Debian بنفس وتيرة فريق أمان Debian.
الوسم :nonroot يضمن تشغيل الصورة بالمعرّف 65532 افتراضياً — دون حاجة إلى تعليمة USER، وهو متوافق مع runAsNonRoot في Kubernetes. يوجد وسم :debug يتضمن BusyBox؛ استخدمه فقط في سيناريوهات التصحيح الطارئ، لا في بنيات الإنتاج المجدولة.
apk لتثبيت مكتبات نظام أصيلة أثناء البناء، لكن احصره في مرحلة البناء؛ واستخدمه كمرحلة نهائية فقط حين لا تُغطي Distroless حالتك.
الملفات الثنائية الثابتة بالتفصيل
الملف الثنائي المرتبط ثابتاً يُدمج كل كود المكتبات في وقت التجميع. لا محمّل ld-linux.so، لا libc.so، لا libssl.so — الملف الثنائي قائم بذاته. تُنتج لغة Rust ملفات ثنائية ثابتة عند استهداف x86_64-unknown-linux-musl. في C/C++ مرّر -static إلى GCC. في Go يكفي عادةً CGO_ENABLED=0؛ حين يكون CGO مطلوباً (مثل SQLite عبر mattn/go-sqlite3)، استخدم musl-cross لإنتاج ملف ثنائي musl ثابت.
تصحيح Distroless في الإنتاج
أكثر اعتراض شائع ضد Distroless وscratch: "كيف أُصحّح الأخطاء؟" الجواب هو حاويات تصحيح مؤقتة — ميزة في Kubernetes تُرفق حاوية مُجهّزة بالكامل بـ pod يعمل دون تعديل مواصفاته:
هذا النمط يعني أنك لن تحتاج إلى صدفة في صورة الإنتاج. حاوية التصحيح تشترك في فضاء العمليات مع الحاوية المستهدفة، مما يتيح الوصول إلى /proc/<pid>/fd ومقابس الشبكة والمتغيرات البيئية دون تغيير الصورة أو الوضع الأمني للـ pod.
نظافة الطبقات والفحوصات الأخيرة
أياً كانت قاعدتك المختارة، طبّق هذه القواعد في المرحلة النهائية: لا تُثبّت أدوات البناء في مرحلة التشغيل، عيّن دائماً USER بمعرّف غير جذري، احذف أي بيانات اعتماد أو أسرار نُسخت أثناء البناء، واستخدم --no-cache أو --mount=type=cache في مراحل البناء لتجنب تسرب فهارس الحزم إلى الطبقات. استخدم docker image inspect --format '{{json .RootFS.Layers}}' لمراجعة عدد الطبقات، وdocker history --no-trunc لاكتشاف الطبقات الكبيرة غير المقصودة قبل الدفع إلى السجل.