chroot נכשל ב-OpenVPN: כלל AppArmor או הרשאה?
הוספת נתיב לפרופיל AppArmor לא מעניקה לעולם CAP_SYS_CHROOT, וכשל של chroot יכול לנבוע משלוש שכבות נפרדות. גלו איזו מהן לשנות.
הדמון רושם כשל של chroot והפרופיל כבר נראה נדיב, ולכן התיקון המפתה הוא להרחיב כלל נתיב ולהפעיל מחדש. זה בדרך כלל לא משנה דבר, מפני ש-chroot נשלט על ידי הרשאה שהפרופיל מעולם לא יכול היה להעביר. ההחלטה באיזו שכבה לגעת דורשת שלוש בדיקות זולות: מה התהליך רשאי להחזיק, מה המדיניות מתירה, ומה המפעיל הסיר לפני ששניים מאלה בכלל נחשבו.
שני שערים נפרדים, ורק אחד שייך ל-AppArmor
chroot זקוק ל-CAP_SYS_CHROOT במרחב השמות של המשתמש שבו התהליך פועל. זו הרשאה מקובלת של לינוקס, שהקרנל בודק לפני שהקריאה עושה דבר עם נתיבים. בנפרד, פרופיל AppArmor אוכף יכול לשלול את היכולת גם כשהתהליך מחזיק בה, מפני שהמדיניות המחייבת נבדקת בנוסף להרשאות ולא במקומן. ההבחנה חשובה בפועל: שתי השכבות חיות בקבצים שונים ובדרך כלל אנשים שונים מתחזקים אותן.
שני השערים נכשלים עם אותו errno. שניהם מפיקים EPERM, ושניהם מופיעים כתלונת הרשאה כללית ביומן הדמון, ולכן ניחוש תוכן הפרופיל מבזבז כל כך הרבה זמן. כלל הנתיב שעמדתם להוסיף שולט בגישה לקבצים אחרי ש-chroot מצליח; אין לו שום קשר לשאלה אם הפעולה מותרת בכלל.
שער שלישי נמצא במעלה שניהם. אם מנהל השירות או עטיפה הפעילו את התהליך בלי היכולת בקבוצת הגבול שלו, שום שינוי ב-AppArmor לא יכול להחזיר אותה, והוספת יכולות לפרופיל רק מתעדת כוונה שהקרנל לא יכבד.
קראו את מצב ההרשאות לפני עריכת המדיניות
התחילו מהתהליך שרץ ולא מהתצורה שאתם חושבים שפעילה. קובץ המצב חושף חמש מסכות יכולת ואת דגל no-new-privileges, והם עונים על שאלות שונות:
grep -E '^(CapInh|CapPrm|CapEff|CapBnd|CapAmb|NoNewPrivs):' /proc/$(pidof openvpn)/status
sudo aa-status | grep -i openvpnCapEff היא הקבוצה שחשובה לקריאה: יכולת שנמצאת ב-CapBnd וחסרה ב-CapEff מותרת על ידי התקרה ואינה מוחזקת כעת. CapAmb חשובה כשהבינארי אינו מורשה במובן setuid הרגיל, מפני שיכולות סביבה הן הדרך שבה שירות שומר הרשאה לאורך exec בלי יכולות קובץ. CapInh מחזיקה סיביות שעלולות לעבור בירושה לאורך exec, אבל רק כשקבוצת הירושה של הקובץ המופעל מאפשרת זאת, ולכן סיבית שם אינה נותנת דבר בעצמה.
אחר כך ודאו איזה פרופיל מחובר באמת. תהליך יכול להיות כלוא בשם פרופיל שלא ציפיתם לו, במיוחד כשהפצה שולחת יחד את קובץ היחידה והפרופיל שלה, וקריאת /proc/PID/attr/current מכריעה זאת בשורה אחת. השוואת הקובץ שערכתם לפרופיל שהקרנל מדווח עליו היא ההבדל בין לתקן את הסיבה לערוך קובץ שאיש אינו טוען.
הוסיפו את כלל היכולת רק כשמסלול הביקורת אומר זאת
רישומי הקרנל והביקורת נוקבים בשכבה שסירבה. חפשו סירוב שנושא את הפעולה capable ואת השם sys_chroot, יחד עם הפרופיל שהפיק אותו. אם רישום כזה קיים, התהליך כבר החזיק ביכולת ו-AppArmor סירב, והשינוי הממוקד הוא כלל יכולת בתוך הפרופיל האמיתי:
capability sys_chroot,השאירו אותו כקטע בפרופיל שמחובר באמת, ובדקו את מילת המפתח מול עיון הפרופילים של apparmor.d במקום להעתיק כלל מדריך לא קשור. הכנסתו ל-include משותף מעניקה אותה לכל תוכנית כלואה שמושכת את הקובץ הזה, ומעבר של הפרופיל למצב complain מסתיר את הסירוב הבא במקום לפתור את זה. כלל הפרופיל מסיר סירוב מדיניות; הוא לא מייצר הרשאה, ולכן תהליך שמעולם לא החזיק CAP_SYS_CHROOT בקבוצה האפקטיבית שלו ייכשל גם עם הכלל.
אם רישום הביקורת לא מראה סירוב AppArmor כלל, הפסיקו לערוך את הפרופיל. מדובר בבעיית הרשאות, והעבודה שנותרה היא בקובץ היחידה או בתהליך האב, לא בשפת המדיניות.
מנהל השירות שומר על תקרה משלו
systemd מגדיר קבוצת גבול של יכולות לכל שירות, וברירת המחדל שלה צרה. כל דבר שנשלל שם אינו זמין לתהליך, יהיה אשר יהיה הפרופיל, ולכן יחידות מוקשחות עלולות להיכשל בפעולה שעבדה כשהדמון הופעל ביד. את ההוראות הרלוונטיות קל לקרוא ביחידה וקל להחמיץ כשהאפליקציה מדווחת על הכשל. קובצי drop-in שמגיעים מחבילת ההפצה הם המקור הרגיל לתקרה שאיש אינו זוכר שהגדיר.
העלאת התקרה והענקת היכולת הן שני שינויים, לא אחד. רשומה בקבוצת הגבול אומרת שהתהליך רשאי להחזיק ביכולת; היא אינה מוסיפה אותה לקבוצה האפקטיבית, ו-NoNewPrivs יכול למנוע הצלחה של העלאה מאוחרת גם כשכל השאר מסתדר. הוסיפו רק את מה שהפעולה צריכה, שמרו על שאר ההקשחה, והימנעו מקיצור הדרך של הענקת כל היכולות כדי שקריאה אחת תעבור.
גם הסדר חשוב כאן. שנו קודם את תקרת היחידה, ודאו ש-CapEff מכילה כעת את הסיבית, ורק אז החליטו אם AppArmor עדיין מסרב. עשיית שני השינויים בבת אחת משאירה אתכם שוב מנחשים אם הכשל נמשך.
אחרי שהסירוב מתפוגג, בדקו את הכלא עצמו
chroot מוצלח מזיז את השורש, ולכן כל נתיב שהדמון נוגע בו לאחר מכן נפתר בתוך העץ החדש. תצורה חסרה, צומת התקן נעדר או תיקיית תעודות שלא הועתקה יופיעו כעת כשגיאת קובץ, וזו בעיית פריסה ולא בעיית הרשאה. קראו את האירוע הבא במקום להוסיף עוד הרשאה רחבה. התזוזה משנה גם את משמעותו של נתיב יחסי, ולכן מדריכי הקשחה מתעקשים על נתיבים מוחלטים בתצורת הדמון.
כדאי גם לזכור מה chroot אינו עושה. הוא גבול נתיב, לא ארגז חול: תהליך ששומר על ההרשאה יכול להימלט ממנו, ולכן היכולת בדרך כלל נזרקת אחרי הקריאה ולכן הכלאה צריכה להישאר סביב הדמון. החזקת פרופיל AppArmor, קבוצת גבול היכולות והגבלות מערכת הקבצים יחד היא מטרת התרגיל, לא שארית שיש להסיר כשהשירות עולה.
כלל ההחלטה קצר. אם רישום הביקורת נוקב ב-AppArmor, הוסיפו כלל יכולת אחד לפרופיל המחובר. אם הקבוצה האפקטיבית ריקה, תקנו את תקרת היחידה ואת סדר זריקת ההרשאה. אם הפרופיל אינו זה שערכתם, הפסיקו לערוך ומצאו את האמיתי.