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

www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client set * banip *

يُقرأ السطر على أنه «الحظر فقط». وهو في الحقيقة يعني «أي شيء، بصلاحية الجذر». وفي ما يلي: لماذا يحدث ذلك، وكيف تتحقق منه على خادمك في خمس دقائق، وبماذا تُستبدل مثل هذه القواعد.

ما تفعله النجمة * في الواقع

التفصيل الحاسم: لا يقارن sudo الوسائط واحدًا واحدًا بل سطر الأمر بأكمله، والنجمة في النمط تعبر المسافات بلا عائق — أي تعبر حدود الوسائط. ولذلك فإن النص الحرفي banip الواقع في وسط القاعدة لا يقيّد شيئًا: يكفي أن تظهر كلمة banip في مكان ما من الأمر، وقبلها وبعدها يمكن تمرير أي شيء.

ثم يُبحث عن برنامج يمكن أن يُسلَّم إليه أمر للتنفيذ. وفي حالتنا كان موجودًا في القاعدة نفسها. في fail2ban يُحدَّد إجراء الحظر كنص، وهذا النص يمكن إعادة تعريفه أثناء التشغيل:

fail2ban-client get portscan actions
sudo fail2ban-client set portscan action nftables-multiport actionban "<أمر>" banip 192.0.2.10
sudo fail2ban-client set portscan banip 192.0.2.11

النداء الأول يستبدل نص الإجراء في إجراء حقيقي، والثاني يجعل السجن يعمل — وينفّذ fail2ban، العامل بصلاحية الجذر، ما أُدخل. فعلنا ذلك في 4 يوليو على خادمنا: أُنشئ ملف في /root بصلاحية الجذر، أي أن عملية الويب حصلت على صلاحيات كاملة على الآلة من قاعدة كانت تبدو ضيّقة.

وقد ظهرت في الطريق ثلاث تفاصيل تغيب عادة عن شروح الآخرين:

  • الخيار -c قبل set يقطعه sudo: في النمط يلتصق set مباشرة باسم الملف التنفيذي، ولا يمكن إدخال شيء قبله؛
  • addaction وaction بدون كلمة banip لا تمران أيضًا — لكن كل ما يلزم يتّسع في أمر واحد set … banip …، وبذلك يتحقق النمط؛
  • اسم الإجراء يجب أن يكون حقيقيًا، وإلا فلا شيء يُستبدل: يعرضه fail2ban-client get <jail> actions.

من يقف أيضًا في القائمة نفسها

ليس fail2ban مذنبًا هنا ولا حالة فريدة. الخطر في اقتران NOPASSWD بالنجمة نفسه. وما وجدناه إلى جانبه على خوادمنا:

  • journalctl * — يُفتح السجل عبر مُصفِّح، ومن المُصفِّح تُشغَّل الصدفة. تبدو القاعدة متعلقة بقراءة السجلات، وهي في الواقع صدفة جذر. والعلاج ليس نمطًا أضيق بل المجموعة systemd-journal: حينها يعمل journalctl دون sudo إطلاقًا؛
  • grep * /var/log/fail2ban.log — الوسيط الأول لـgrep هو النمط، لكن النجمة تتيح تمرير مسار ثانٍ أيضًا، وgrep ينفَّذ بصلاحية الجذر. قراءة /etc/shadow بهذه القاعدة أمر واحد. والبديل هو نفسه: المجموعة adm لقراءة السجلات؛
  • ufw --force * وufw allow/deny/delete * المجردة — تستطيع عملية الويب إيقاف الجدار الناري بالكامل. والمزعج على نحو خاص أن اللوحة لم تستخدم هذه القواعد في الحالتين على الإطلاق: صلاحيات ميتة بقيت من إصدارات مبكرة للمُثبِّت؛
  • lynis-scan.sh * — غلاف يمرّر "$@" إلى عملية جذر يعادل قاعدة بلا أي قيود.

كيف ترى ما لديك

ما يجب النظر فيه ليس الملف بل الصلاحيات الفعلية لمستخدم محدد — ذاك الذي يعمل به فعليًا مجمّع PHP-FPM (ليس www-data دائمًا؛ على إحدى آلاتنا كانت اللوحة تعمل باسم admin):

ps -o user= -C php-fpm | sort -u
sudo -l -U www-data
sudo -l -U admin

ثم قائمة الملفات نفسها، وهنا فخّان كلّفانا وقتًا:

sudo grep -rn 'NOPASSWD' /etc/sudoers /etc/sudoers.d/
ls -la /etc/sudoers.d/

الأول: القواعد تعيش في موضعين في الوقت نفسه. عندنا كان السطر الخطير في /etc/sudoers.d/monitor وفي /etc/sudoers الرئيسي معًا — وفي الرئيسي في نحو ثماني نسخ تراكمت من إصدارات مختلفة للمُثبِّت. وتنظيف موضع واحد والاطمئنان هو الطريقة المعتادة لترك الثغرة مفتوحة.

الثاني: يتجاهل sudo الملفات التي في أسمائها نقطة. الملف www-data.bak3 في /etc/sudoers.d/ يبدو قاعدة سارية ويُقرأ كقاعدة سارية، لكنه لا يعمل. وهذا يسري في الاتجاهين: «القاعدة موجودة والصلاحية غائبة»، والطمأنينة الزائفة بأن نسخة من الإعداد «موضوعة إلى جانبه».

بماذا تُستبدل

تضييق النمط بلا جدوى — فوجود نجمة في أي موضع من السطر يعيد المهمة إلى نقطة البداية. يصمد أسلوب واحد: غلاف بصلاحية الجذر يستقبل وسائط موضعية ثابتة، ويتحقق منها بنفسه، وينادي البرنامج دون أي إمكانية لإضافة شيء.

#!/bin/sh
# /usr/local/sbin/arciveo-f2b — ban|unban <jail> <ip>، ولا شيء غير ذلك
set -eu
action="$1"; jail="$2"; ip="$3"
case "$action" in ban|unban) ;; *) echo "invalid action"; exit 1 ;; esac
case "$jail" in *[!a-zA-Z0-9_-]*|'') echo "invalid jail"; exit 1 ;; esac
printf '%s' "$ip" | grep -Eq '^[0-9a-fA-F:.]+$' || { echo "invalid ip"; exit 1; }

case "$action" in
  ban)   exec /usr/bin/fail2ban-client set "$jail" banip   "$ip" ;;
  unban) exec /usr/bin/fail2ban-client set "$jail" unbanip "$ip" ;;
esac

الغلاف ملك للجذر، بصلاحيات 755، و— وهذا ليس اختياريًا — يجب أن يكون غير قابل للكتابة من الويب، وإلا فقد البناء كله معناه. ولا يبقى في sudoers سواه:

www-data ALL=(root) NOPASSWD: /usr/local/sbin/arciveo-f2b

لا تُسرد الوسائط في القاعدة: يتحقق منها السكربت نفسه، ومواضعها فيه ثابتة، فلا مكان لتمرير نص إجراء. أما القواعد التي تقرأ فقط — الحالات وss وipset list — فتوضع في أسطر منفصلة، وكذلك بلا نجمة حيث يكون ذلك ممكنًا.

وبشكل منفصل عن استبدال sudo بالمجموعات. الأسلوب صحيح: systemd-journal للسجل وadm للملفات السجلية أرخص وأكثر أمانًا من أي قاعدة sudo. لكن لا يجوز إخراج مستخدم الويب من المجموعات على غير هدى. أخرجنا www-data من مجموعة واحدة على مضيف فيه لوحة، فحصلنا على 403 في كل المواقع دفعة واحدة: فهناك يعمل Apache باسم www-data، وملفات المواقع ملك لمستخدم آخر، وصلاحية القراءة كانت تأتي من تلك العضوية في المجموعة بعينها. واستلزم الإصلاح إعادة تشغيل كاملة — لا إعادة تحميل — لـApache وPHP-FPM، لأن العمليات العاملة القديمة تحتفظ بمجموعاتها السابقة وتنتج حالة «يعمل، ثم 403».

التحقق بعد التعديل

قبل استبدال ملف sudoers يلزم التحقق من صحته النحوية — وإلا فقد يبقى المرء بلا sudo على الإطلاق:

sudo visudo -cf /etc/sudoers.d/monitor

ثم يلزم تأكيد أن المسار القديم قد مات، وباسم ذلك المستخدم بعينه:

sudo -k
sudo -u www-data sudo -n fail2ban-client set portscan banip 192.0.2.10

يجب أن يكون الجواب a password is required، بينما نداء الغلاف بنفاية بدلًا من عنوان يجب أن يعطي invalid ip. وهنا فخ خاص: sudo -v يخزّن التأكيد خمس عشرة دقيقة، وبعده يمرّ أي تحقق بـsudo -n «بنجاح». وهكذا بالضبط أكّدنا مرة قاعدة لم تكن على الخادم أصلًا. ولذلك فإن sudo -k قبل التحقق إلزامي.

الغاية من المراقبة

قواعد sudo تتغير نادرًا، لكنها تتغير بصمت: المُثبِّت يضيف إليها، ولوحة الاستضافة تكمل بما لديها، والحزمة المحذوفة تترك أسطرها خلفها. المراجعة لمرة واحدة تُغلق ما هو موجود اليوم ولا تقول شيئًا عما سيأتي به التحديث القادم. والاستنتاج العملي بسيط: قائمة من يستطيع أن يصبح جذرًا مكانها أمام العين، إلى جانب بقية حالة الخادم، لا في الذاكرة بعد وقوع حادثة. وكيف يبدو ذلك مجتمعًا تعرضه صفحات العرض التوضيحي أدناه.