בלוג
אילו לקוחות באמת צריכים VM משלהם? תוכנית בידוד מדורגת ל-Docker
VM עבור כל לקוח זה מוגזם. הנה איך להחליט כמה בידוד כל דייר צריך—ולהפוך את ההחלטה לאוטומטית.
סיכום
סוכנויות ניכנסות פעמים רבות לפאניקה כאשר לקוח שואל עד כמה הנתונים שלו באמת מבודדים מדיירים אחרים. מרחבי השמות (namespaces) וה-cgroups של Docker נותנים לך בידוד אמיתי, אבל הם לא אותו דבר כמו גבול חומרה. במקום להריץ כל לקוח על VM—או גרוע מכך, להתייחס לכל לקוח באותו אופן—בנה קבוצה קטנה של דרגות בידוד והתאם כל לקוח לאחת מהן לפי רגישות הנתונים, אמון וציות. קונטיינר נעול (לא-root, הסרת יכולות, seccomp, מערכת קבצים לקריאה בלבד) מכסה את רוב האתרים; עומסי עבודה רגולטוריים או עוינים מקבלים VM או היברידית של קונטיינר בתוך VM. הפוסט הזה נותן זרימת החלטות ניתנת לחזרה, טבלת השוואה, ומבט כן על מתי בידוד נוסף הוא מוגזם.
האם אתה נמצא בנקודה בשיחת מכירות שבה הלקוח החדש אומר “אנחנו בתחום הבריאות, תראה לנו שהנתונים שלנו מבודדים משאר הלקוחות שלך” ואתה מעדיף לדבר על כל דבר אחר?
זו בעיית הסוכנות: לא פריסה מושלמת אחת, אלא אותה פריסה אמינה שחוזרת על עצמה על פני תריסר לקוחות עם תקציבים שונים, פרופילי סיכון ודרישות ציות. הנה הגרסה הכנה. בידוד של Docker הוא אמיתי, אבל הוא ספציפי. מרחבי שמות נותנים לכל קונטיינר תצוגה משלו של תהליכים, רשת ומערכת קבצים; cgroups מגבילים CPU, זיכרון וקלט/פלט דיסק כך שדיירים לא יכולים להרעיב זה את זה. מה שזה לא קונה לך הוא קיר חומרה בין הקונטיינר לליבת המארח. אם תוקף נמלט מהקונטיינר, הוא בתוך הליבה היחידה שיש לך. שאר המאמר הזה הופך את העובדה הלא נוחה הזו להחלטה ניתנת לחזרה: סווג כל לקוח לפי רגישות נתונים ואמון, החל פרופיל הקשחה בסיסי, ופנה ל-VM רק כאשר עלות הפריצה גבוהה מעלות ה-VM.
רגע, האם קונטיינרים כבר מבודדים?
Docker רץ על מרחבי שמות (namespaces) ו-cgroups של לינוקס, והמילים האלה עושות עבודה אמיתית. מרחבי שמות מפרידים מזהי תהליך, מחסניות רשת, נקודות עיגון ומשתמשים, כך שתהליך בקונטיינר אחד לא יכול לראות את טבלת התהליכים של אחר. Cgroups קובעים מגבלות: תן לקונטיינר 0.5 CPU, 512 MB של זיכרון, ומשקל קבוע של קלט/פלט בלוקים, וזה בדיוק מה שהוא מקבל. לולאה בורחת בדייר אחד מקבלת הגבלה במקום להפיל את השכן. אם לא הגדרת מגבלות, דילגת על הדבר הבסיסי ביותר ש-cgroups נועדו עבורו.
קח אפליקציית PHP פשוטה בקונטיינר A. היא רואה את מערכת הקבצים של עצמה, את ממשק הרשת של עצמה, את ה-PID 1 של עצמה. לקונטיינר B יש את אותו הדבר, אבל תצוגה שונה. זה מרחבי שמות. עכשיו התרחק ודלג על מגבלת הזיכרון: קונטיינר A יכול למלא את ה-RAM של המארח ולגרום לקונטיינר B לזחול. זה מה ש-cgroups קיימים כדי למנוע. אבל שני קונטיינרים יכולים להיות מבודדים זה מזה על ידי מרחבי שמות ועדיין לחלוק את ליבת המארח, וזה החלק שכל סיפור בריחת קונטיינר עוסק בו. ניצול שמגיע לליבה יכול להגיע פוטנציאלית לכל דייר על המארח.
“Docker מבודד” הוא משפט חצי-נכון. הגרסה המדויקת היא “Docker מבודד עם מרחבי שמות ו-cgroups, ופגיעות ליבה היא רדיוס הפיצוץ.” לפני שאתה סומך על דייר שיריץ קוד לא מהימן, שב עם המחשבה הזו לרגע. התשובה היא לא “לעולם אל תשתמש בקונטיינרים”—זו הפאניקה הקלה. התשובה היא מערכת דרגות.
אז למה חלק מהלקוחות צריכים יותר ממרחבי שמות?
התשובה הכנה היא שבידוד הוא לא מתג, אלא ספקטרום. בקצה האחד יש לך קונטיינר משותף לחלוטין שבו כולם למעשה באפליקציה אחת. בקצה השני יש לך VM נפרד לכל דייר עם ליבה משלו. רוב עבודת הסוכנויות חיה באמצע הלא נוח, והאמצע הוא לא בחירה בינארית בין “Docker בסדר” ל“הרץ VM לכולם.”
מה שדוחף לקוח ימינה זה לא הגודל שלו. אלה ארבע שאלות:
- האם הם מאחסנים נתונים רגולטוריים? רשומות בריאות, פרטי כרטיסי תשלום, כל מה שרגולטור יכנה רגיש.
- האם לפריצה אצל הדייר שלהם יש נתיב מציאותי לדייר אחר? אם הם יכולים להריץ קוד שרירותי, כן.
- האם אתה סומך על הקוד ועל האנשים שפורסים אותו? לקוח ששוכר את הפרילנסר הזול ביותר הוא לא באותה רמת אמון כמו לקוח שצוות הפיתוח שלו אתה מכיר.
- האם החוזה שלהם אומר “ייעודי,” “מבודד,” או “פרטי”? אם כן, כבר הבטחת דרגה; עכשיו כל המשימה היא לבחור את הנכונה.
אם אתה עדיין לא יכול לענות על השאלות האלה, שים את הלקוח בדרגת בסיס ורשום את ההנחות. זה לא ביקורת אבטחה; זה בדיקת שפיות שאתה חוזר עליה בכל קליטת לקוח.
איך אני מחליט לכל לקוח בלי להריץ ביקורת אבטחה בכל פעם?
עשה טבלה קטנה והתחייב אליה. אתה לא צריך מטריצה עם ארבעים תאים. ארבע דרגות יכסו כמעט כל לקוח שסוכנות רואה.
| עמדת לקוח | מה באמת מבדיל אותם | השתמש כאשר |
|---|---|---|
| דרגה 1: אפליקציה/קונטיינר משותפים | רק לוגיקת אפליקציה | כלים פנימיים, נתונים בסיכון נמוך, פרויקטים שבהם כולם נמצאים במפורש במערכת התחברות אחת |
| דרגה 2: אותו מארח, קונטיינרים נפרדים | מרחבי שמות ו-cgroups | רוב אתרי השיווק, טפסי יצירת קשר, אין נתונים רגישים |
| דרגה 3: קונטיינר נעול | דרגה 2 + לא-root, יכולות שהוסרו, seccomp, מערכת קבצים לקריאה בלבד, פילוח רשת | מסחר אלקטרוני, PII, קוד מותאם אישית שאינך סומך עליו לחלוטין |
| דרגה 4: VM לכל דייר | היפרוויזור וליבה נפרדת | בריאות, פיננסים, ניירת ציות, קוד לא מהימן, שכנים רועשים |
כך זה עובד בפועל. לקוח מאפייה עם טופס יצירת קשר וקישור לאינסטגרם עובר לדרגה 2: קונטיינר אחד על מארח משותף, רשת Docker ברירת מחדל, מגבלות משאבים, עבודה גמורה. חנות מקוונת שאוגרת שמות לקוחות, כתובות והפניות תשלום עוברת לדרגה 3: אותו מארח משותף, אבל הקונטיינר רץ כמשתמש לא-root, אין לו יכולות ליבה נוספות, משתמש בפרופיל seccomp, וחושף רק פורט 443. פורטל קליטה רפואי שאוגר מידע בריאותי מוגן עובר לדרגה 4: VM לכל דייר, כי עלות הפריצה היא לא “ננקה את זה” אלא “אנחנו לא יכולים להראות ללקוח שלקחנו אותו ברצינות.”
כל הטריק הוא שאתה לא חושב מחדש על הארכיטקטורה עבור כל לקוח. אתה בוחר שורה מטבלה שכבר הסכמת עליה. כך סוכנות של חמישה אנשים יכולה להריץ מאה אתרים ללא מאה אובססיות אבטחה נפרדות. זה גם אומר שהלקוח הבא לא מקבל תשובה שתלויה באיזה חבר צוות ענה לטלפון. לוויכוח הארכיטקטוני העמוק יותר מאחורי הבחירות האלה, המדריך הזה לתכנון רמות בידוד מרובות-דיירים מכסה את הפשרות בפירוט רב יותר.
איך נראה קונטיינר נעול בפועל?
בוא נפסיק לומר “נעול” ונהיה קונקרטיים. כך נראית דרגה 3 עבור לקוח WordPress או PHP טיפוסי.
ראשית, החלף את המשתמש. רוב התמונות הרשמיות עדיין רצות כ-root כברירת מחדל; ב-Dockerfile שלך, צור משתמש שאינו root והרץ את האפליקציה כמשתמש זה. זה מיד מסיר את הדרך הנפוצה ביותר שבה פגיעה בקונטיינר הופכת לפגיעה במארח. שנית, הסר את היכולות שאינך צריך. הרץ עם --cap-drop ALL והוסף בחזרה רק אחת, בדרך כלל NET_BIND_SERVICE כדי שהאפליקציה תוכל להאזין בפורט 80. זה לבדו שינוי גדול יותר ממה שרוב האנשים מצפים. שלישית, הפוך את מערכת הקבצים של השורש לקריאה בלבד עם --read-only, והרכב ספריות כתיבה (העלאות, ספריית נתוני מסד הנתונים) ככרכים או tmpfs. רביעית, החל פרופיל seccomp ואם המארח שלך תומך בכך, AppArmor או SELinux. לבסוף, שים את הקונטיינר על רשת Docker ייעודית וחשוף רק את הפורטים שבאמת צריכים להיות נגישים.
בוא נעבור על דוגמה של WordPress. תמונת הבסיס כנראה רצה כ-root, אז אתה מוסיף שלב useradd והנחיית USER. אתה מריץ את הקונטיינר עם מגבלת זיכרון ומגבלת CPU, כך שפרץ תעבורת תוספים לא פוגע בשכן. אתה מרכיב את /var/www/html/wp-content/uploads ככרך ניתן לכתיבה. אתה מגדיר --read-only. אתה מחבר אותו לרשת שאין בה דגל --privileged בשום מקום בקרבתה. התוצאה היא קונטיינר שהיה פעם “אתר WordPress” והוא עכשיו “אתר WordPress שבמקרה נעול יותר מרוב השרתים הווירטואליים הפרטיים.”
אם הרכבה ידנית של כל זה מרגישה שברירית, יש דרך ביניים קלה יותר: Enhanced Container Isolation של Docker, שמשתמשת בבידוד מרחב משתמשים ובזמן ריצה מאובטח של קונטיינרים. זה קיצור דרך לגיטימי, אבל זה לא כרטיס חינם לדלג על אי-הרצה כ-root או על הסרת יכולות. הדייר עדיין צריך תמונה הגיונית. ההבדל הוא שמשטח התקיפה הפונה לליבה נעשה קטן יותר בלי שאתה הופך למומחה seccomp בין לילה. אם אתה רוצה את הרצף המדויק עבור דייר יחיד, מדריך ההקשחה שלב-אחר-שלב לבידוד הופך את הסעיף הזה לפקודות העתק-הדבק.
מתי אני מפסיק להוסיף שכבות ופשוט נותן להם VM?
הנה החלק המנוגד לאינטואיציה: בידוד נוסף הוא לא אוטומטית טוב יותר. VMs נותנים לך בידוד ברמת חומרה, ליבה נפרדת, ומשטח תקיפה קטן בהרבה אם הליבה האורחת נופלת. זה בדיוק מה שלקוחות בריאות ופיננסים מצפים כשהם אומרים “אנחנו רוצים להיות מבודדים.” אבל כל VM מוסיף עלות של תיקונים, גיבויים ומחשוב, ומכפיל את העבודה של שמירת צי מעודכן. אם אתה נותן VM לכל לקוח כי לקוח אחד אמר לך פעם ש-Docker מפחיד אותו, קנית תיאטרון אבטחה בכסף אמיתי.
VM הוא התשובה הנכונה כאשר הסיכון לכל דייר גבוה יותר מהעלות התפעולית של VM לכל דייר. זה אומר נתונים רגולטוריים, דרישות ציות כתובות, קוד צד שלישי לא מהימן, או לקוח שצריך להרחיק שכן רועש. זו גם התשובה הנכונה כאשר החוזה של הלקוח מבטיח בפועל סביבה ייעודית, כי “קונטיינר” זה לא מה שהם מדמיינים כשהם חותמים על “ייעודי.”
אבל VM אינו תירוץ לקונטיינר מרושל. מלכודת נפוצה היא לשים את הלקוח ב-VM ואז לדלג על ההקשחה כי “ה-VM מגן עליהם.” ה-VM מגן על המארח מפני הדייר, לא על הדייר מפני התמונה הרעה שלו עצמו. אתה עדיין רוצה לא-root, יכולות שהוסרו ו-seccomp בתוך ה-VM. הגישה ההיברידית—קונטיינרים בתוך VM—היא לעתים קרובות נקודת האיזון: ה-VM מספק את הגבול לשיחות ציות, והקונטיינר נותן לך את זרימת העבודה לפריסה שאתה כבר מכיר. יש גרסה ארוכה יותר של הוויכוח הזה בהאם כל דייר צריך VM משלו?, אבל התשובה הקצרה היא שה-VM הוא בשביל החוזה, לא בשביל הפחד.
איך אני הופך את זה לחזרתי אצל כל לקוח?
אתה הופך את זה לחזרתי על ידי הפיכת מערכת הדרגות לתבנית, לא לזיכרון. שמור תיקייה של קבצי Compose, אחד לכל דרגה: tier2-baseline, tier3-locked, tier4-vm-hybrid. כאשר לקוח חדש מופיע, העתק את התבנית, שנה את משתני הסביבה, ואתה כבר יודע את צורת הבידוד לפני שכתבת שורת תשתית חדשה.
אז רשום את ההחלטה. לא דוח אבטחה של 400 עמודים, אלא פסקה קצרה במאגר של הלקוח: איזה נתונים הם מאחסנים, באיזו דרגה הם נמצאים, למה, ומה יעלה אותם מדרגה. הפסקה הזו שווה יותר ממאה חוקי חומת אש, כי זה הדבר שאתה יכול להראות למבקר הבא או ללקוח המודאג הבא. זה גם מונע ממך לזכור למה המאפייה קיבלה דרגה 2 וחנות המסחר האלקטרוני קיבלה דרגה 3 אחרי ששיחת המכירות המקורית דעכה.
הפוך את הבדיקות המשעממות לאוטומטיות. תן ל-CI שלך לסרוק כל תמונת לקוח ולהיכשל בבנייה אם היא רצה כ-root, אם יש לה את כל היכולות, או אם היא מנסה לפרסם פורט אחר מאלה שדרגת הבידוד מתירה. שום דבר מזה אינו אקזוטי; זה רק לוודא שהתבנית לא נשברת בטעות על ידי מפתח בעל כוונות טובות. אם אתה בונה את זרימת העבודה של האירוח מסביב בכל מקרה, המאמר על אסטרטגיות אירוח Docker מוכן לייצור מכסה את החלק שמגיע לאחר שהקונטיינרים מוגדרים.
שום דבר מזה אינו זוהר. שום פוסט בבלוג לא הולך לגרום ל“בידוד דיירים” להישמע מרגש כמו דיאגרמת ארכיטקטורה של פרויקט גרינפילד. אבל זה ההבדל בין סוכנות שעונה על “עד כמה אנחנו מבודדים?” עם “בטח” באצבעות משוכלות לבין אחת שיכולה להראות דרגה, קונפיגורציה וסיבה. קונטיינרים הם לא קיר קסם. VMs הם לא כדור כסף. מערכת דרגות היא רק החלטה שאתה רושם ומשתמש בה שוב—ובשביל סוכנות, חזרתיות היא כל המשחק.

