सर्वर पर प्रबंधन पैनल लगभग हमेशा उसी दीवार से टकराता है: वेब प्रक्रिया को कुछ ऐसा करना होता है जिसका उसे अधिकार नहीं। कोई पता ब्लॉक करना, कोई पोर्ट खोलना, जर्नल पढ़ना। जो उत्तर सामने आता है वह यही है कि 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 बन सकता है, इसकी सूची सर्वर की बाक़ी स्थिति के साथ नज़र के सामने रहनी चाहिए, न कि किसी घटना के बाद याद आने वाली चीज़ हो। यह सब एक साथ कैसा दिखता है, नीचे दिए डेमो पेज दिखाते हैं।