OpenVPN ב-Docker: חוזה הריצה שלקוח מינימלי זקוק לו
תמונה שנבדקה, פרופיל מוגן אחד, גישה ל-TUN ו-NET_ADMIN. מה המכולה מקבלת, מה היא אסורה לקבל ואיך מוכיחים שהמנהרה עובדת.
שימו לקוח VPN במכולה ותקבלו מרחב שמות אחד עם מנהרה בתוכו, לא מארח שמנתב את כל התעבורה שלו דרך VPN. אלה פרויקטים נפרדים, והתייחסות אליהם כאל אחד היא הדרך שבה מכולות הופכות לבעלות הרשאות בטעות. ספר זה מכסה רק את הראשון: תמונה אחת שנבדקה, פרופיל מוגן אחד ותהליך לקוח המוגבל למרחב השמות של הרשת שלו בלבד.
התחילו מפריט שכבר אושר בידי מישהו. השלבים שלהלן מניחים תמונה שמכילה את הקובץ ההרצה openvpn ופרופיל לקוח מלא שהתעודה וחומר המפתח שלו כבר נפתרו בתוך ההרכבה. הם אינם בונים תוכנה, אינם מנפיקים אישורים ואינם מכסים סביבות הרצה ללא root או מכולות Windows מקוריות, שכל אחת מהן משנה את התשובה.
מה המכולה מקבלת ומה לא
שמרו את הפרופיל וכל מה שהוא מפנה אליו בספרייה מקומית עם הרשאות מוגבלות, בהרכבה לקריאה בלבד. נתיבים יחסיים בתוך הפרופיל חייבים להיפתר בתוך אותה הרכבה; זהו פרט שאנשים מפספסים כאשר תעודה נמצאת ספרייה אחת מעל הפרופיל והמכולה לעולם אינה רואה אותה.
services:
vpn:
image: ${VPN_CLIENT_IMAGE:?set the reviewed image reference}
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ./vpn:/vpn:ro
working_dir: /vpn
entrypoint: ["openvpn"]
command: ["--config", "/vpn/client.ovpn"]
הקובץ הזה מעניק שלושה דברים ואינו מבקש דבר אחר. יש לנעוץ את ההפניה לתמונה ב-digest אם מדיניות הפריסה שלכם אומרת כך, והפרופיל צריך להיות זה שנבדק, לא עותק שמישהו ערך במקום על המארח.
קריאה בלבד כאן פירושה שהמכולה אינה יכולה לשנות את הקבצים שלכם במארח; זה לא אומר שהמכולה אינה יכולה לקרוא את המפתח הפרטי שהרכבתם. זה כל הרעיון של ההרכבה, ולכן הרשאות קבצים, בעלות והאופן שבו הסוד הגיע למארח עדיין חשובים. לעולם אל תעבירו סיסמה בשורת הפקודה ואל תאפו אותה בשכבת תמונה, כי שתיהן שורדות במקומות שלא תוכלו לנקות אחר כך.
יכולות, entrypoint והמלכודת שביניהן
נדרשים שני הגדרות והם עושים עבודות לא קשורות. NET_ADMIN מתיר מניפולציה של ממשקים ונתיבים בתוך מרחב השמות של המכולה. רשומת ההתקן /dev/net/tun מתירה לתהליך לפתוח את התקן המנהרה בכלל. מתן האחד בלי השני מייצר כשל ברור, וזה נוח: התקן TUN חסר נכשל בעת הפתיחה, ואילו יכולת חסרה נכשלת כאשר הלקוח מנסה להגדיר את הממשק.
אף אחד מהשניים אינו מצדיק privileged: true. מכולה בעלת הרשאות מקבלת כל יכולת ועוד גישה להתקנים, וזו הענקה גדולה בהרבה ממה שעומס העבודה הזה צריך. ההפניה להרשאות וליכולות של סביבת ההרצה מסבירה עד כמה ההענקה הזו מגיעה. אם chart או פוסט בבלוג מבקש אותה, בדקו קודם אם הדרישה הייתה בעצם נתיב התקן חסר או יכולת חסרה.
עקיפת entrypoint היא החלק שנושך. קריאה ישירה ל-openvpn נכונה לתמונה רגילה ושגויה לכל תמונת מוצר שסקריפט האתחול שלה מכין כללי חומת אש, קובצי פותר או אישורים לפני הפעלת הדמון. עקיפת הסקריפט הזה מדלגת על ההכנה ומשאירה אתכם לנפות מנהרה שעלתה במרחב שמות מוגדר למחצה.
הריצו את הלקוח בחזית ותנו לו להיות התהליך הראשי של המכולה. תהליך שהופך לדמון יוצא מיד, המכולה מופעלת מחדש בלולאה, והלוגים שאתם צריכים מפוזרים על פני הפעלות מחדש שנראות כולן זהות.
אמתו מתוך מרחב השמות, לא מהמארח
העלו את הסטאק, אשרו בלוג שהלקוח הגיע למצב מחובר, ואז בדקו את מרחב השמות שמחזיק את המנהרה בפועל.
docker compose up -d
docker compose logs --tail 40 vpn
docker compose exec vpn ip -br addr
docker compose exec vpn ip route
רשימת הכתובות אמורה להראות ממשק מנהרה עם כתובת מהמאגר, וטבלת הנתיבים אמורה להראות את היעדים שנדחפו מפנים אליו. אם הממשק חסר, הלקוח מעולם לא הגיע ליצירתו. אם הוא קיים אבל אף נתיב לא משתמש בו, המקום לחפש בו הוא ה-push או אפשרויות הנתיב.
אחר כך בדקו את היעד שחשוב לכם, ממרחב השמות שאמור להשתמש במנהרה, עם היישום ולא רק עם ping. פתרון שמות הוא כשל נפרד שראוי לבדיקה מפורשת: כתובת DNS שנדחפה אינה מעדכנת את הפותר של המכולה מעצמה, ולקוח עצמאי שמתעלם מה-push יפתור שמות דרך מה שהתמונה הגדירה.
המצב המנותק הוא חלק מהחוזה
התנהגות ההתחברות מחדש צריכה בדיקה משלה, כי מדיניות הפעלה מחדש מוכיחה שהתהליך מופעל מחדש ואינה אומרת דבר על מה שהתעבורה עושה בינתיים. עצרו את המנהרה, נסו את היעד המיועד וצפו אם החיבור נדחה, נתקע או מצליח בשקט דרך הנתיב הרגיל של המכולה. התוצאה האחרונה היא זו שחשובה, והיא בלתי נראית בלוג בריא.
אם עומס העבודה אסור לעולם להשתמש בנתיב הרגיל, תכננו זאת במפורש עם כללי חומת אש ואות מוכנות בתוך מרחב השמות. Kill switch הוא מדיניות שאתם כותבים ובודקים, לא תכונה שאתם יורשים מהרצת לקוח במכולה.
לבסוף, זכרו את הגבול: הנתיבים האלה חיים במרחב השמות של המכולה ואינם מחליפים את טבלת הניתוב של המארח. כללי העברה, תרגום וחומת אש של Docker נשארים חלק מהנתיב החיצוני, ופורט שפורסם אינו ממשק VPN.