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

الصور عديمة التوزيع والصور الخفيفة

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

الصور عديمة التوزيع والصور الخفيفة

كل ميغابايت تُضيفه إلى صورة الحاوية هو سطح هجوم إضافي، وزمن إقلاع أطول، وتكلفة نقل بيانات أعلى. على مقياس جوجل — حيث تُطلَق مئات الآلاف من حالات الحاوية كل دقيقة — يُعدّ اختيار الصورة الأساسية قراراً أمنياً واستقرارياً من الدرجة الأولى، لا مجرد تفصيلة هامشية. يتناول هذا الدرس الاستراتيجيات الثلاث السائدة للصور الخفيفة: 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 وبيانات المناطق الزمنية.

# بناء ملف ثنائي Go ثابت بالكامل ثم وضعه في scratch FROM golang:1.22-alpine AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ go build -trimpath -ldflags="-s -w" -o /app/server ./cmd/server # الصورة النهائية: الملف الثنائي + شهادات TLS فقط FROM scratch COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=builder /app/server /server ENTRYPOINT ["/server"]

تتراوح الصورة الناتجة عادةً بين 5 و15 ميغابايت. لا صدفة، لا ps، لا wget — المهاجم الذي تمكّن من اختراق منطق التطبيق لن يجد له موطئ قدم. خيار -ldflags="-s -w" يُزيل جدول الرموز ومعلومات تصحيح DWARF مما يُصغّر الملف الثنائي أكثر.

فخ scratch — غياب /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 توفيرها دون تدخل يدوي.

FROM python:3.12-alpine AS builder WORKDIR /app RUN apk add --no-cache gcc musl-dev libffi-dev COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt FROM python:3.12-alpine WORKDIR /app COPY --from=builder /install /usr/local COPY src/ ./src/ RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser ENTRYPOINT ["python", "-m", "src.main"]

انضباط 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.

Base Image Comparison: Attack Surface vs Usability Attack Surface / Image Size Runtime Support & Debugging Ease scratch ~5–15 MB Static binaries only Distroless ~20–60 MB No shell, runtime only Alpine ~50–120 MB Shell + apk ubuntu:22.04 ~77 MB+ Full toolchain
مقارنة المقايضة: الصور الأصغر تُقلّص سطح الهجوم لكنها تستلزم انضباطاً أكبر في البناء.
# خدمة Java مع Distroless JRE FROM eclipse-temurin:21-jdk-alpine AS builder WORKDIR /app COPY . . RUN ./gradlew bootJar --no-daemon FROM gcr.io/distroless/java21-debian12:nonroot WORKDIR /app COPY --from=builder /app/build/libs/myservice.jar app.jar EXPOSE 8080 CMD ["/app/app.jar"]

الوسم :nonroot يضمن تشغيل الصورة بالمعرّف 65532 افتراضياً — دون حاجة إلى تعليمة USER، وهو متوافق مع runAsNonRoot في Kubernetes. يوجد وسم :debug يتضمن BusyBox؛ استخدمه فقط في سيناريوهات التصحيح الطارئ، لا في بنيات الإنتاج المجدولة.

مصفوفة القرار في الإنتاج: استخدم scratch لـ Go/Rust/C مع تعطيل CGO. استخدم Distroless لـ Python وJava وNode.js أو أي لغة ذات ربط ديناميكي — فهي النقطة المثلى بين الأمان والعملية. استخدم Alpine حين تحتاج إلى 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 ثابت.

# خدمة Rust: ربط ثابت بـ musl FROM rust:1.79-alpine AS builder RUN apk add --no-cache musl-dev WORKDIR /src COPY . . RUN cargo build --release --target x86_64-unknown-linux-musl FROM scratch COPY --from=builder /src/target/x86_64-unknown-linux-musl/release/myservice /myservice COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ USER 65532:65532 ENTRYPOINT ["/myservice"]

تصحيح Distroless في الإنتاج

أكثر اعتراض شائع ضد Distroless وscratch: "كيف أُصحّح الأخطاء؟" الجواب هو حاويات تصحيح مؤقتة — ميزة في Kubernetes تُرفق حاوية مُجهّزة بالكامل بـ pod يعمل دون تعديل مواصفاته:

# إرفاق حاوية تصحيح بـ pod يعمل بـ Distroless kubectl debug -it <pod-name> \ --image=gcr.io/distroless/base:debug \ --target=<container-name> \ --copy-to=debug-pod # أو باستخدام صورة أدوات كاملة kubectl debug -it <pod-name> \ --image=ubuntu:22.04 \ --target=app \ -- bash

هذا النمط يعني أنك لن تحتاج إلى صدفة في صورة الإنتاج. حاوية التصحيح تشترك في فضاء العمليات مع الحاوية المستهدفة، مما يتيح الوصول إلى /proc/<pid>/fd ومقابس الشبكة والمتغيرات البيئية دون تغيير الصورة أو الوضع الأمني للـ pod.

حجم الصورة ليس المقياس الوحيد. قد تكون صور Distroless أكبر من Alpine لنفس وقت التشغيل لأنها تسحب حزم مكتبات Debian الكاملة. الميزة الأساسية هي عدد الثغرات CVE في الصورة لا حجمها الخام. قِس الاثنين دائماً بماسحك الأمني (Trivy أو Grype أو Snyk) قبل اتخاذ القرار النهائي.

نظافة الطبقات والفحوصات الأخيرة

أياً كانت قاعدتك المختارة، طبّق هذه القواعد في المرحلة النهائية: لا تُثبّت أدوات البناء في مرحلة التشغيل، عيّن دائماً USER بمعرّف غير جذري، احذف أي بيانات اعتماد أو أسرار نُسخت أثناء البناء، واستخدم --no-cache أو --mount=type=cache في مراحل البناء لتجنب تسرب فهارس الحزم إلى الطبقات. استخدم docker image inspect --format '{{json .RootFS.Layers}}' لمراجعة عدد الطبقات، وdocker history --no-trunc لاكتشاف الطبقات الكبيرة غير المقصودة قبل الدفع إلى السجل.