সার্ভারের প্রশাসন প্যানেল প্রায় সব সময় একই দেয়ালে ধাক্কা খায়: ওয়েব প্রসেসকে এমন কিছু করতে হয় যার অধিকার তার নেই। কোনো ঠিকানা আটকানো, কোনো পোর্ট খোলা, জার্নাল পড়া। যে উত্তরটি নিজেই সামনে আসে সেটি হলো sudoers-এ একটি সংকীর্ণ লাইন দিয়ে দেওয়া, আর তার বেশি কিছু নয়:

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

লাইনটি পড়া যায় «কেবল আটকানো» হিসেবে। আসলে এর মানে «root হিসেবে যা খুশি»। নিচে রইল কেন এমন হয়, নিজের সার্ভারে পাঁচ মিনিটে কী করে এটি যাচাই করবেন, আর এমন নিয়মের বদলে কী বসানো হয়।

* সত্যিই কী করে

নির্ধারক খুঁটিনাটি: 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

প্রথম ডাকটি সত্যিকারের একটি ক্রিয়ার লেখা বদলে দেয়, দ্বিতীয়টি jail-কে চালু হতে বাধ্য করে — আর fail2ban, যা root হিসেবে চলে, ঢুকিয়ে দেওয়া জিনিসটি চালিয়ে দেয়। আমরা এটি 4 জুলাই নিজেদের সার্ভারে করেছি: /root-এ একটি ফাইল root অধিকারে তৈরি হলো, অর্থাৎ ওয়েব প্রসেস এমন একটি নিয়ম থেকে মেশিনে পূর্ণ অধিকার পেয়ে গেল যা দেখতে সংকীর্ণ ছিল।

পথে তিনটি খুঁটিনাটি বেরিয়ে এল, যা অন্যদের আলোচনায় সাধারণত থাকে না:

  • set-এর আগে -c সুইচটি sudo কেটে দেয়: প্যাটার্নে set সরাসরি বাইনারির নামের সঙ্গে সাঁটা, আর তার আগে কিছু ঢোকানো যায় না;
  • banip শব্দ ছাড়া addaction এবং action-ও পার হয় না — কিন্তু যা দরকার তার সবটাই একটি কমান্ড set … banip …-এ এঁটে যায়, আর তাতেই প্যাটার্ন মেলে;
  • ক্রিয়ার নাম সত্যিকারের হতে হবে, নইলে বদলানোর কিছু থাকে না: সেটি দেখায় fail2ban-client get <jail> actions।

একই তালিকায় আর কে আছে

fail2ban এখানে দোষী নয়, আর একমাত্র উদাহরণও নয়। বিপজ্জনক হলো NOPASSWD আর তারা চিহ্নের যুগলবন্দিই। নিজেদের সার্ভারে এর পাশে আমরা কী পেয়েছি:

  • journalctl * — জার্নাল একটি পেজারের মধ্য দিয়ে খোলে, আর পেজার থেকে শেল চালু হয়ে যায়। নিয়মটি দেখতে লগ পড়ার; কাজে এটি root শেল। প্রতিকার সংকীর্ণ প্যাটার্ন নয়, বরং systemd-journal গ্রুপ: তখন journalctl sudo ছাড়াই চলে;
  • grep * /var/log/fail2ban.log — grep-এর প্রথম যুক্তি হলো প্যাটার্ন, কিন্তু তারা চিহ্ন দ্বিতীয় পথও দেওয়ার সুযোগ দেয়, আর grep চলে root হিসেবে। এই নিয়মে /etc/shadow পড়া একটিমাত্র কমান্ড। বিকল্পও একই: লগ পড়ার জন্য adm গ্রুপ;
  • ufw --force * আর খালি ufw allow/deny/delete * — ওয়েব প্রসেস ফায়ারওয়াল পুরোপুরি বন্ধ করে দিতে পারে। বিশেষভাবে অস্বস্তিকর: দুই ক্ষেত্রেই প্যানেল এই নিয়মগুলো ব্যবহারই করত না — ইনস্টলারের আগের সংস্করণ থেকে টিকে যাওয়া মৃত অনুমতি;
  • lynis-scan.sh * — যে আবরণ "$@"-কে root প্রসেসে পাঠিয়ে দেয়, সেটি কোনো সীমা ছাড়া নিয়মের সমান।

নিজের কাছে কী আছে, তা কী করে দেখবেন

দেখার জিনিস ফাইল নয়, বরং একটি নির্দিষ্ট ব্যবহারকারীর প্রকৃত অধিকার — সেই ব্যবহারকারী, যার অধীনে 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 উপেক্ষা করে। /etc/sudoers.d/-তে রাখা www-data.bak3 ফাইলটি বলবৎ নিয়মের মতো দেখায় এবং বলবৎ নিয়মের মতোই পড়া যায়, কিন্তু কাজ করে না। এটি দুই দিকেই খাটে: «নিয়ম আছে, অধিকার নেই» আর কনফিগের একটি অনুলিপি «পাশেই পড়ে আছে» বলে মিথ্যা নিশ্চিন্তি।

কী দিয়ে বদলাবেন

প্যাটার্ন সংকুচিত করা অর্থহীন — লাইনের যেকোনো জায়গায় তারা চিহ্ন কাজটিকে শুরুতে ফিরিয়ে দেয়। টিকে থাকে একটিই পথ: একটি root আবরণ, যা নির্দিষ্ট অবস্থানের যুক্তি নেয়, নিজেই সেগুলো যাচাই করে এবং প্রোগ্রামটিকে এমনভাবে ডাকে যাতে কিছু জোড়ার সামান্যতম সুযোগও না থাকে।

#!/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

আবরণটি root-এর মালিকানায়, অনুমতি 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-র নিয়ম কমই বদলায়, কিন্তু নিঃশব্দে বদলায়: ইনস্টলার তাতে যোগ করে, হোস্টিংয়ের প্যানেল নিজের অংশ বসায়, সরিয়ে দেওয়া প্যাকেজ নিজের লাইন রেখে যায়। একবারের পরিদর্শন আজ যা আছে তা বন্ধ করে, আর পরের হালনাগাদ কী আনবে সে বিষয়ে কিছুই বলে না। ব্যবহারিক সিদ্ধান্ত সরল: কে root হতে পারে তার তালিকা সার্ভারের বাকি অবস্থার পাশে চোখের সামনে থাকা ভালো, কোনো ঘটনার পরে মনে পড়ার চেয়ে। সবটা একসঙ্গে কেমন দেখায়, তা নিচের ডেমো পাতাগুলো দেখায়।