ה-hook עובד כשאתה מריץ אותו ביד כ-root ומחזיר Operation not permitted ברגע שהשירות מפעיל אותו. הקובץ לא השתנה בין ההרצות האלה; הסביבה כן. systemd נותן לתהליך עולם קטן יותר: פחות יכולות, /dev שונה, נתיבי מערכת קבצים לקריאה בלבד ותקרה למשימות. קרא את הכשל כגבול שדחה פעולה מסוימת אחת, לא כתסריט שנשבר.

התחל מהפעולה שנדחתה

שורת לוג שאומרת שה-unit נכשל לא אומרת לך כמעט כלום. השורה המועילה נוקבת בקריאה שנדחתה: צומת התקן שלא ניתן היה לפתוח, תיקייה שלא ניתן היה לכתוב בה, מרחב שמות שלא ניתן היה ליצור. מצא את הקריאה הזו לפני שאתה נוגע באף הנחיה אחת, מפני שכל אחת מהן מתאימה להגבלה אחרת. אחר כך קרא את ה-unit שבאמת רץ, כולל קובצי drop-in, במקום את הקובץ שהחבילה סיפקה.

השאילתה שלמטה מדפיסה את נתיב השבר, את תיקיות ה-drop-in ואת המאפיינים שחשובים לדחייה של התקן או של מערכת קבצים. קרא את כולם לפני שתחליט איזה גבול רלוונטי:

systemctl show "[email protected]" -p FragmentPath -p DropInPaths -p PrivateDevices -p DevicePolicy -p DeviceAllow -p ProtectHome -p ProtectSystem -p TasksMax
journalctl -u "[email protected]" -b --no-pager | grep -iE 'operation not permitted|permission denied|cannot open|failed'

שום דבר בבלוק הזה לא כותב. אם הקריאה שנדחתה חסרה ביומן, העלה את רמת הפירוט של הלקוח להרצה אחת במקום לנחש הנחיות. תהליך שרץ מוכיח שה-unit עלה, לא שה-hook רץ עם ההרשאות שהיה צריך.

מרחב שמות זקוק להרשאות mount שהתקן TUN אף פעם לא משתמש בהן

לשתי פעולות שנראות זהות בתוך hook אחד יש פרופילי הרשאה שונים. פתיחת /dev/net/tun ויצירת הממשק דורשת ניהול רשת ועוד גישה לצומת הזה. יצירת מרחב שמות רשת בעל שם דורשת גם עבודת mount, מפני שכלי מרחב השמות עושה mount לתצוגה /sys חדשה ומקשר תיקיית מצב למקומה. ה-unit של הלקוח מה-upstream משאיר בכוונה את ניהול ה-mount מחוץ לקבוצת הגבולות שלו, ולכן שלב מרחב השמות נדחה בעוד שהמנהרה עצמה הייתה עובדת.

לכן מתן צומת ההתקן לא מציל את התסריט. הרשאת התקן עונה על שאלה אחת, אם התהליך הזה רשאי לפתוח את הצומת הזה, ושאלת מרחב השמות היא אחרת. התאם את הקריאה שנכשלת ליכולת שלה לפני שאתה מרחיב משהו.

בדיקה ב-shell שלך יורשת כל יכולת שיש ל-root ללא הגבלה, וה-unit יורש את מה שקבוצת הגבולות שלו שמרה. שתי ההרצות הן ניסויים שונים גם כשהקובץ הבינארי זהה.

התקנים, נתיבים מוגנים ותקרות משימות

לאחר שהפעולה שנדחתה ידועה, שלוש קבוצות של הנחיות מסבירות את רוב הכשלים. את הגישה להתקנים מסדיר PrivateDevices, שמחליף את /dev בקבוצה מינימלית של התקנים מדומים, ואת DevicePolicy עם DeviceAllow, שקובעים מה ה-cgroup מתיר. /dev/net/tun אינו חלק מהקבוצה המינימלית הזו, ולכן unit עם PrivateDevices=yes לא יכול לפתוח אותו, יהא אשר יאמר מדיניות ההתקנים.

ההגנה על מערכת הקבצים עונה על שאלה שנייה. ProtectHome=yes הופך את /home, /root ואת /run/user לבלתי נגישים, ואת ProtectSystem=full הופך את /usr, /boot ואת /etc לקריאה בלבד. לכן hook שכותב לצד קובץ התצורה של עצמו עובד ב-shell שלך ונכשל תחת ה-unit, באותה צורת הרשאה כמו יכולת חסרה. העבר את הקלט הנכתב לנתיב שהמדיניות מתירה, או הוסף רשומת ReadWritePaths= צרה.

מגבלות משאבים הן הקבוצה השלישית, והפחות מובנת מאליה. TasksMax= מגביל כמה משימות ה-unit רשאי ליצור, ותקרה של מתארי קבצים מתנהגת אותו דבר עבור sockets וידיות. פריסה שצמחה מעבר לתקרה נכשלת בשגיאת משאב ולא בשגיאת הרשאה, ולכן ההודעה המדויקת עדיין חשובה.

הוצא את שלב מרחב השמות מה-hook

הכנה שצריכה הרשאה שהלקוח לא אמור להחזיק שייכת ל-unit משל עצמה. oneshot שרץ לפני הלקוח, שומר את תוצאתו וממוין לפני החיבור נותן לשלב מרחב השמות את מה שהוא צריך בלי למסור את הזכויות האלה לתהליך המנהרה. אחר כך הלקוח מפנה ל-unit הזה במקום לקרוא לתסריט מה-hook up שלו.

התבנית שלמטה היא צורה התחלתית, לא משהו שמיישמים בלי לקרוא. היא שומרת את ההכנה במקום שבו ההרשאה חיה ומשאירה את unit המנהרה ללא שינוי:

# /etc/systemd/system/[email protected]
[Unit]
Description=Namespace preparation for %i
Before=openvpn-client@%i.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/netns-up.sh %i
CapabilityBoundingSet=CAP_NET_ADMIN CAP_SYS_ADMIN
[Install]
WantedBy=multi-user.target

הוסף את הסדר עם drop-in במקום לערוך את ה-unit הארוז, כדי שעדכון לא יוכל להפיל את השינוי שלך בשקט. התיקייה צמודה לשם המופע, ולכן פרופיל שני צריך קובץ משל עצמו:

sudo mkdir -p /etc/systemd/system/[email protected]
sudo tee /etc/systemd/system/[email protected]/10-namespace.conf >/dev/null <<'CONF'
[Unit]
[email protected]
[email protected]
CONF
sudo systemctl daemon-reload

חזור על אותה קריאה אחרי השינוי

תיקון מאושר רק כאשר אותה קריאה מפיקה תוצאה שונה. אתחל את שתי ה-units, הרץ את הפעולה שנכשלה, והשווה את תוצאתה לשורת היומן ששמרת לפני השינוי. unit שעולה בעוד ה-hook לא רץ שוב אף פעם אינו תיקון.

הרצף שלמטה שומר על השוואה הוגנת, מפני שהוא בודק את הקריאה שנדחתה ולא את צבע ה-unit. שמור את הפלט שלו לצד שורת היומן שתפסת לפני השינוי:

sudo systemctl restart "[email protected]" "[email protected]"
systemctl status "[email protected]" --no-pager
journalctl -u "[email protected]" -b --no-pager | grep -c 'Operation not permitted'
ip -brief addr show tun0

ספור את הדחיות במקום לקרוא את השורה האחרונה, מפני ש-hook יכול להיכשל, לנסות שוב ולהצליח, ורק הספירה מראה אם הגבול זז. השאר הערה ב-drop-in שמנקבה בהנחיה שהקלת ובה סיבה, מפני שהעדכון הבא משווה את התצורה שלך לברירת המחדל של החבילה. unit שעולה רק אחרי שכל הגנה כבויה לא תוקן.

מאמרים קשורים