{"id":"l-dk-2-2","courseId":"docker-kubernetes","lang":"ar","kind":"lesson","summary":"اختر base image لـ notes-api بالموازنة بين slim وAlpine وdistroless، وشغّل التطبيق بمستخدم غير root، وأحكِم إغلاق الـ container أكثر وقت التشغيل.","html":"<p>في 2024 فشلت image لـ Skylane في مراجعة أمنية أجراها أحد العملاء، مع 140 نتيجة. أقل من خمس منها كانت في كود كتبناه أو استدعيناه. والباقي في حزم صادف أن الـ base image تتضمّنها: مكتبة XML، ومحوّل صور، ووكيل لنقل البريد. لا شيء منها كان يعمل، وكلها كانت مشكلتنا. الـ base image اعتمادية ترثها بالجملة، فاخترها عن قصد.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">اختيار الـ base</h2>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"جدول قابل للتمرير\" tabindex=\"0\"><table><thead>\n<tr>\n<th>الـ Base</th>\n<th>ما بداخلها</th>\n<th>مناسبة لـ</th>\n<th>انتبه إلى</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">node:24</code></td>\n<td>Debian كاملة، ومترجِمات، ومكتبات كثيرة</td>\n<td>مراحل البناء التي تترجم إضافات أصلية</td>\n<td>كبيرة؛ وفيها الكثير ليُفحص</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">node:24-slim</code></td>\n<td>Debian مصغّرة + Node</td>\n<td>معظم images التشغيل</td>\n<td>ما زال فيها shell و<code dir=\"ltr\">apt</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">node:24-alpine</code></td>\n<td>Alpine Linux ‏(musl) + Node</td>\n<td>images صغيرة جدًا</td>\n<td>الفروق بين musl وglibc</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">gcr.io/distroless/nodejs24-debian13</code></td>\n<td>Node ومكتبات تشغيله فقط</td>\n<td>images إنتاج محصّنة</td>\n<td>لا shell ولا مدير حزم</td>\n</tr>\n</tbody>\n</table></div><p>خياري الافتراضي لفريق جديد على الـ containers هو <strong>slim</strong>: صغيرة، ومبنية على glibc فتعمل الوحدات الأصلية الجاهزة، وما زلت تستطيع الدخول إليها بـ <code dir=\"ltr\">docker exec</code> وفتح shell فيها حين تقع أزمة في الإنتاج.</p>\n<p>تعطيك <strong>Alpine</strong> images أصغر، لكنها تستخدم مكتبة C ‏musl بدلًا من glibc. الإضافات الأصلية الجاهزة المترجَمة لـ glibc تفشل في التحميل، أما الفروق الدقيقة في تحليل أسماء DNS وفي حجز الذاكرة فقد سبّبت لفرق كثيرة متاعب في الإنتاج. اخترها حين تكون قد اختبرت شجرة اعتمادياتك كاملة عليها، لا لأن الرقم في صفحة الـ tags أصغر.</p>\n<p>الـ images من نوع <strong>Distroless</strong> تحتوي على Node.js والمكتبات التي يحتاج إليها، ولا شيء غير ذلك: لا shell، ولا <code dir=\"ltr\">apt</code>، ولا <code dir=\"ltr\">curl</code>. المهاجم الذي يحصل على تنفيذ كود يجد القليل جدًا ليعمل به، والماسح يجد القليل جدًا ليُبلغ عنه. الثمن هو صعوبة التشخيص. لا تستطيع فتح shell داخلها بـ <code dir=\"ltr\">exec</code>، وستتعلّم طريقة Kubernetes للالتفاف على ذلك، <code dir=\"ltr\">kubectl debug</code>، في الدرس الأخير من هذه الدورة.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">توقّف عن التشغيل بصلاحيات root</h2>\n<p>افتراضيًا، تعمل العملية داخل الـ container كـ root، أي UID 0. الـ namespaces تحدّ ما يستطيع هذا الـ root رؤيته، لكنه يبقى root في نظر النواة. وإذا اجتمع ذلك مع خلل في النواة أو mount مضبوط خطأً، فهكذا يتحوّل اختراق الـ container إلى اختراق للمضيف. التشغيل كمستخدم بلا صلاحيات يحوّل كثيرًا من تلك الثغرات إلى طرق مسدودة.</p>\n<p>تأتي الـ images الرسمية لـ Node.js بمستخدم اسمه <code dir=\"ltr\">node</code> رقمه UID 1000. انتقل إليه في مرحلة التشغيل، بعد كل ما يحتاج إلى root:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># …build and deps stages from the previous lesson</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> node:24-slim </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> runtime</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ENV</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> NODE_ENV=production</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WORKDIR</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /app</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> package.json ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=deps /app/node_modules ./node_modules</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=build /app/dist ./dist</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">USER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> node</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">EXPOSE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> 3000</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">CMD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"node\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dist/server.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span></code></pre></div>\n<p>لاحظ ما <em>ليس</em> موجودًا: لا <code dir=\"ltr\">--chown=node:node</code> على عمليات النسخ. تبقى الملفات مملوكة لـ root وقابلة للقراءة من الجميع، فيستطيع التطبيق قراءة كوده لكنه لا يستطيع تغييره. إذا وجد مهاجم ثغرة تنفيذ كود عن بُعد، فلن يستطيع إعادة كتابة <code dir=\"ltr\">dist/server.js</code> ليبقى داخل النظام. امنح التطبيق صلاحية الكتابة فقط حيث يحتاج إليها حقًا، وفضّل <code dir=\"ltr\">/tmp</code> أو volume مُركّبًا لذلك.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>إصلاح خطأ صلاحيات بـ USER root</span></p>\n<p>يحتاج التطبيق إلى كتابة ملف مؤقت، فيحصل على <code dir=\"ltr\">EACCES</code>، فيضيف أحدهم <code dir=\"ltr\">USER root</code> في نهاية الـ Dockerfile «مؤقتًا». يُشحن هكذا، ويبقى. أصلح الحاجة الفعلية بدلًا من ذلك: أنشئ المجلد الوحيد الذي يكتب فيه التطبيق وسلّمه لمستخدم التطبيق، أو وجّه التطبيق إلى <code dir=\"ltr\">/tmp</code>.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> mkdir -p /app/tmp &#x26;&#x26; chown node:node /app/tmp</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">USER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> node</span></span></code></pre></div>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">نسخة distroless</h2>\n<p>هذه مرحلة التشغيل نفسها على distroless. الـ tag ‏<code dir=\"ltr\">:nonroot</code> يعمل بـ UID 65532 دون أي إعداد إضافي، ونقطة الدخول في الـ image هي <code dir=\"ltr\">node</code> أصلًا، لذا لا تذكر <code dir=\"ltr\">CMD</code> إلا السكربت:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> gcr.io/distroless/nodejs24-debian13:nonroot </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> runtime</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ENV</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> NODE_ENV=production</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WORKDIR</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /app</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> package.json ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=deps /app/node_modules ./node_modules</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=build /app/dist ./dist</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">EXPOSE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> 3000</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">CMD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dist/server.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span></code></pre></div>\n<p>ما زالت مرحلتا build وdeps تستخدمان <code dir=\"ltr\">node:24-slim</code>؛ وحدها المرحلة المشحونة تتغيّر. أبقِ الإصدار الرئيسي لـ Node متطابقًا عبر المراحل؛ وإلا فإن الوحدات الأصلية (native modules) المترجَمة وفق الـ ABI الخاص بإصدار معيّن من Node ستُحمَّل في إصدار آخر وتفشل عند بدء التشغيل.</p>\n<p>تستطيع التأكّد من المستخدم الذي سيعمل به الـ container دون تشغيله:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> image</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> inspect</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:1.0.0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --format</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '{{.Config.User}}'</span></span></code></pre></div>\n<h2 id=\"sec-4\" tabindex=\"-1\">أحكِم الإغلاق وقت التشغيل</h2>\n<p>الـ image تضبط القيم الافتراضية؛ والـ runtime يستطيع تشديدها أكثر:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --rm</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -p</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 8080:3000</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --read-only</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --tmpfs</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> /tmp</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --cap-drop</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ALL</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --security-opt</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> no-new-privileges</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  notes-api:1.0.0</span></span></code></pre></div>\n<p>الخيار <code dir=\"ltr\">--read-only</code> يجعل نظام الملفات الجذري للـ container غير قابل للتعديل، ولا يبقى قابلًا للكتابة إلا <code dir=\"ltr\">/tmp</code> بوصفه mount صغيرًا في الذاكرة. والخيار <code dir=\"ltr\">--cap-drop ALL</code> يزيل صلاحيات Linux ‏(capabilities) التي تحتاج إليها العمليات المميّزة الخاصة بالـ root. و<code dir=\"ltr\">no-new-privileges</code> يمنع العملية من اكتساب صلاحيات عبر ملفات setuid التنفيذية. في القسم الخامس ستضبط الأشياء نفسها في <code dir=\"ltr\">securityContext</code> ضمن Kubernetes، حيث تصبح سياسة لكل Pod.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>المنافذ تحت 1024</span></p>\n<p>أبقِ التطبيقات على منافذ عالية مثل 3000 أو 8080 داخل الـ container. ما زلت تستطيع نشرها على 80 أو 443 عند الحافة؛ لأن الربط يحدث خارج العملية، فلا يحتاج التطبيق أبدًا إلى صلاحيات من أجله.</p>\n</aside>\n<p>الـ image لديك الآن صغيرة وبلا صلاحيات. التالي: كيف تسمّيها بحيث يكون ما تنشره هو بالضبط ما اختبرته.</p>\n","toc":[{"id":"sec-1","text":"اختيار الـ base","level":2},{"id":"sec-2","text":"توقّف عن التشغيل بصلاحيات root","level":2},{"id":"sec-3","text":"نسخة distroless","level":2},{"id":"sec-4","text":"أحكِم الإغلاق وقت التشغيل","level":2}],"takeaways":["كل حزمة في الـ base image شيء عليك ترقيعه وفحصه والدفاع عنه، لذا ابدأ من أصغر base يعمل عليه تطبيقك فعلًا.","الـ image ‏<code dir=\"ltr\">node:24-slim</code> خيار افتراضي متين؛ وAlpine تستخدم musl بدلًا من glibc، وdistroless تزيل الـ shell ومدير الحزم كليًا.","الـ images الرسمية لـ Node.js تتضمّن مستخدمًا اسمه <code dir=\"ltr\">node</code> ‏(UID 1000)؛ انتقل إليه بـ <code dir=\"ltr\">USER node</code> بعد الخطوات التي تحتاج إلى root.","اترك ملفات التطبيق مملوكة لـ root وللقراءة فقط بالنسبة إلى مستخدم التطبيق، حتى لا تستطيع عملية مخترَقة إعادة كتابة كودها.","أضف تحصينات وقت التشغيل مثل <code dir=\"ltr\">--read-only</code> و<code dir=\"ltr\">--cap-drop ALL</code> و<code dir=\"ltr\">no-new-privileges</code>؛ ولـ Kubernetes الضوابط نفسها في <code dir=\"ltr\">securityContext</code>."],"further":[{"title":"Dockerfile reference: USER","url":"https://docs.docker.com/reference/dockerfile/#user"},{"title":"Docker Engine security","url":"https://docs.docker.com/engine/security/"},{"title":"Building best practices","url":"https://docs.docker.com/build/building/best-practices/"}],"quiz":[{"q":"<p>يعتمد notes-api على حزمة فيها إضافة أصلية (native addon). بعد تغيير الـ base من <code dir=\"ltr\">node:24-slim</code> إلى <code dir=\"ltr\">node:24-alpine</code>، ينهار الـ container عند البدء بخطأ في تحميل ملف <code dir=\"ltr\">.node</code>. ما السبب المرجّح؟</p>","options":[{"text":"الـ images المبنية على Alpine لا تدعم Node.js 24.","why":"الـ images الرسمية <code dir=\"ltr\">node:24-alpine</code> موجودة وتشغّل Node 24 دون مشاكل. المشكلة في مكتبة C، لا في إصدار Node."},{"text":"الـ image تحتاج إلى <code dir=\"ltr\">EXPOSE</code> حتى تُحمَّل الوحدات الأصلية.","why":"التعليمة <code dir=\"ltr\">EXPOSE</code> توثيق للمنافذ. لا علاقة لها بتحميل المكتبات المشتركة."},{"text":"Alpine تعمل افتراضيًا كمستخدم غير root، وهذا يمنع الوحدات الأصلية.","why":"images ‏Node المبنية على Alpine تعمل كـ root افتراضيًا، مثل images ‏Debian. الصلاحيات ليست المشكلة هنا."},{"text":"الإضافة مترجَمة لتعمل مع glibc، وAlpine تستخدم musl، فلا يمكن تحميل الملف التنفيذي.","why":"صحيح. الملفات التنفيذية الجاهزة تستهدف glibc غالبًا. أعِد بناء الإضافة داخل مرحلة بناء على Alpine، أو ابقَ على base يستخدم glibc مثل slim."}],"answer":3},{"q":"<p>أي تغيير يجعل notes-api يعمل بمستخدم غير root في الـ image الرسمية لـ Node.js؟</p>","options":[{"text":"أضف <code dir=\"ltr\">USER node</code> في مرحلة التشغيل بعد الخطوات التي تحتاج إلى root.","why":"صحيح. الـ image الرسمية تنشئ المستخدم <code dir=\"ltr\">node</code> ‏(UID 1000). والتعليمات وعملية الـ container بعد <code dir=\"ltr\">USER</code> تعمل بهذا المستخدم."},{"text":"أضف <code dir=\"ltr\">RUN su node</code> قبل <code dir=\"ltr\">CMD</code>.","why":"الأمر <code dir=\"ltr\">su</code> داخل خطوة <code dir=\"ltr\">RUN</code> لا يؤثّر إلا في shell تلك الخطوة. الـ container ما زال يبدأ كـ root."},{"text":"أضف <code dir=\"ltr\">ENV USER=node</code>.","why":"هذا يضبط متغيّر بيئة اسمه <code dir=\"ltr\">USER</code>. لا يغيّر المستخدم الذي تعمل به العملية."},{"text":"انشر الـ container على منفذ أعلى من 1024.","why":"رقم المنفذ لا يغيّر المستخدم. سيبقى الـ container يعمل كـ root."}],"answer":0},{"q":"<p>تنقل notes-api إلى <code dir=\"ltr\">gcr.io/distroless/nodejs24-debian13:nonroot</code>. أي <code dir=\"ltr\">CMD</code> صحيحة؟</p>","options":[{"text":"<code dir=\"ltr\">CMD [&quot;node&quot;, &quot;dist/server.js&quot;]</code>","why":"نقطة الدخول في image ‏Node من distroless هي <code dir=\"ltr\">node</code> أصلًا، فهذا سيشغّل <code dir=\"ltr\">node node dist/server.js</code> ويفشل في العثور على وحدة اسمها <code dir=\"ltr\">node</code>."},{"text":"<code dir=\"ltr\">CMD node dist/server.js</code>","why":"صيغة shell تحتاج إلى <code dir=\"ltr\">/bin/sh</code>، والـ images من نوع distroless لا تحتوي على shell، فلا يستطيع الـ container البدء."},{"text":"<code dir=\"ltr\">CMD [&quot;dist/server.js&quot;]</code>","why":"صحيح. نقطة الدخول هي <code dir=\"ltr\">node</code>، فتقدّم <code dir=\"ltr\">CMD</code> مسار السكربت وسيطًا (argument) لها."},{"text":"<code dir=\"ltr\">CMD [&quot;npm&quot;, &quot;start&quot;]</code>","why":"الـ images الخاصة بـ Node من distroless تشحن الـ runtime الخاص بـ Node، لا npm. وأنت تريد تجنّب npm في موقع PID 1 على أي حال."}],"answer":2},{"q":"<p>لماذا نترك ملفات <code dir=\"ltr\">dist/</code> في notes-api مملوكة لـ root مع أن العملية تعمل كمستخدم <code dir=\"ltr\">node</code>؟</p>","options":[{"text":"لأن Node.js يرفض تنفيذ الملفات المملوكة للمستخدم الحالي.","why":"يشغّل Node الملفات التي تملكها دون أي مشكلة. الأمر يتعلّق بالحدّ من الضرر، لا بقاعدة يفرضها الـ runtime."},{"text":"لأن المهاجم إن حصل على تنفيذ كود كمستخدم <code dir=\"ltr\">node</code>، يستطيع قراءة الكود لكنه لا يستطيع تعديله أو زرع ملفات فيه.","why":"صحيح. جعل الملفات للقراءة فقط بالنسبة إلى مستخدم التطبيق يقلّص ما يستطيع الاستغلال تغييره. امنح صلاحية الكتابة فقط للمجلدات التي يجب على التطبيق الكتابة فيها."},{"text":"لأن الملفات المملوكة لـ root أصغر حجمًا في الـ image.","why":"ملكية الملف بضعة بايتات من البيانات الوصفية في الحالتين."},{"text":"لأن Kubernetes يرفض الـ images التي ملفاتها مملوكة لمستخدم غير root.","why":"لا يوجد في Kubernetes قاعدة كهذه. يستطيع أن يشترط أن تكون <em>العملية</em> غير root، وهذا ما تفعله أصلًا."}],"answer":1}],"exercise":{"kind":"spot-bug","language":"dockerfile","code":"# syntax=docker/dockerfile:1\n# (build and deps stages as in the previous lesson)\nFROM node:24-slim AS runtime\nENV NODE_ENV=production\nWORKDIR /app\nCOPY package.json ./\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY --from=build /app/dist ./dist\nUSER node\n# temporary: fixes EACCES writing /app/tmp\nUSER root\nEXPOSE 3000\nCMD [\"node\", \"dist/server.js\"]","bugLines":[11],"title":"الإصلاح المؤقت الذي وصل إلى الإنتاج","prompt":"<p>رصدت مراجعة أمنية في هذه الـ image الملاحظة التالية: “container process runs as UID 0”. كاتب الملف يصرّ على أنه انتقل إلى المستخدم <code dir=\"ltr\">node</code>. اختر السطر الذي يجعل الـ container يعمل بصلاحيات root، وفكّر في الإصلاح الصحيح للخطأ الأصلي.</p>\n","hints":["آخر تعليمة <code dir=\"ltr\">USER</code> قبل <code dir=\"ltr\">CMD</code> هي التي تحدّد المستخدم الذي يعمل به الـ container.","الحاجة الحقيقية كانت مجلدًا واحدًا قابلًا للكتابة، لا root للعملية كلها."],"explanation":"<p>السطر 11 يعود إلى root بعد <code dir=\"ltr\">USER node</code>، فتبدأ العملية بـ UID 0. أما خطأ <code dir=\"ltr\">EACCES</code> الأصلي فسببه أن <code dir=\"ltr\">/app</code> مملوك لـ root. أنشئ المجلد الوحيد الذي يكتب فيه التطبيق، وسلّمه إلى مستخدم التطبيق، وابقَ غير root:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> mkdir -p /app/tmp &#x26;&#x26; chown node:node /app/tmp</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">USER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> node</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">EXPOSE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> 3000</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">CMD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"node\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dist/server.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span></code></pre></div>\n<p>والأفضل من ذلك أن تجعل التطبيق يكتب في <code dir=\"ltr\">/tmp</code>، القابل للكتابة من كل المستخدمين، والذي يمكن أن يكون mount من نوع <code dir=\"ltr\">tmpfs</code> حين يكون نظام الملفات الجذري للقراءة فقط.</p>\n","checkLabels":{}},"words":697,"readMinutes":4}