מחקר חולשות בעולמות ה-Cloud הוא אחד מהתחומים המתפתחים ביותר בשנים האחרונות, בהלימה טבעית לתאוצה של כל עולם ה-Cloud שכבר מזמן הפך להיות לחלק אינטגרלי ומשמעותי בכל ארגון, הן מבחינת אחסון מידע והן מבחינת אחסון שרתים.
פוסט זה הוא הראשון מתוך סדרה שתעסוק בחולשות אבטחה בסביבות Cloud. אמנם כמובן שחולשות האבטחה הקלאסיות בעולמות ה-Web וה-Infrastructure רלוונטיות גם לכל שרת שאנו מאתרים בסביבת Cloud, אך פוסט זה לא יתמקד בהם, אלא בחולשות ואתגרים שרלוונטיים רק לסביבות ה-Cloud, בדגש על פערי האבטחה ב-Docker ו-Kubernetes, אחד מהנדבכים המרכזיים בארכיטקטורות Cloud‑Native.
Cloud-Native היא גישה לפיתוח והפעלה של יישומים באופן שתואם את הסביבה הדינמית של הענן: קונטיינרים, שרתים זמניים, תשתיות גמישות, ואוטומציה מלאה.
הרכיבים המרכזיים שנראה לרוב בתשתית Cloud-Native הם:
| רכיב | תפקיד | כלים נפוצים |
|---|---|---|
| Docker | אריזת אפליקציות כקונטיינרים ניידים וקלילים | Docker, Podman, Buildah |
| Kubernetes | תזמון וניהול קונטיינרים בצורה מבוזרת ואוטונומית | Kubernetes, OpenShift, Amazon EKS, Azure AKS, Google GKE |
| CI/CD Pipelines | זרימת פיתוח ופריסה אוטומטית | Jenkins, GitHub Actions, GitLab CI/CD, CircleCI, ArgoCD, Tekton |
| Infrastructure as Code (IaC) | ניהול תשתית בצורה דקלרטיבית, כמו קוד | Terraform, Pulumi, AWS CloudFormation, Azure Bicep, Crossplane |
| Service Mesh | שליטה בתקשורת בין שירותים, כולל ניטור, TLS ואבטחה | Istio, Linkerd, Consul Connect, Kuma, AWS App Mesh |
| Secrets Management | ניהול מאובטח של Secrets, API Keys וסיסמאות | HashiCorp Vault, AWS Secrets Manager, AWS KMS, Azure Key Vault, Doppler |
כמו להרבה קטגוריות אחרות, גם ל-Cloud-Native Application קיים OWASP TOP 10 (דירוג של עשרת הפגיעויות הנפוצות כזכור) וגם אליהם אתייחס בהרחבה בסדרת מאמרים זו, בעיקר בחלקים ב' וג'.
Docker & Kubernetes
טכנולוגיית ה-Docker היא אחת מהטכנולוגיות הנפוצות והפופולריות ביותר בשנים האחרונות. סביר לומר שאם אתם חלק מהתעשייה, ה-Docker הוא אחד מהחברים הכי טובים שלכם ובצדק. בפוסט זה אתייחס לסיכוני האבטחה הרבים ש-Docker ו-Kubernetes מביאים עמם, אך לפני הכל, אדבר קצת על הבסיס בשביל ליישר קו בכל-זאת.
Docker היא פלטפורמה שמאפשרת להריץ, לפתח ולפרוס אפליקציות בתוך קונטיינרים (containers) שהן יחידות תוכנה קלות משקל, מבודדות, שמכילות את כל מה שהאפליקציה צריכה כדי לפעול: קוד, תלויות, ספריות בגרסאות ספציפיות, קבצי קונפיגורציה ועוד.
בעולם שבו סביבת הפיתוח לא תמיד זהה לסביבת ההרצה, Docker נוצר כדי לפתור את בעיית ה-"It works on my machine". קונטיינר מספק סביבה עקבית שמריצה את האפליקציה בדיוק באותה צורה בכל מקום, על הלפטופ של המפתח, על שרת או בענן.
בואו נדבר קצת על מושגים:
- Images: תבנית הפרויקט להרצה. הוא כולל את מערכת הקבצים, התלויות והקוד וממנו יוצרים מופעים של הכלי. בתהליך הבנייה, ניתן להשתמש בכמה Base Images שונים, כלומר, לשלוף קבצים או תוצרים מ-Images אחרים (באמצעות Multistage Build). עם זאת, רק בתהליך הבנייה מתבצע השימוש במספר Images, ובסופו של דבר נבנה Image אחד סופי שמכיל רק את מה שהועתק אליו מהשלבים הקודמים. לדוגמא, אם מימשתם בפרויקט שלכם התאמה ל-Docker, אז הקוד של הפרויקט הוא המקור שמתוכו נבנה ה-Image. בתוך תהליך הבנייה (Dockerfile שמיד נדבר עליו) ניתן להשתמש ב-Images נוספים (כמו Image של Python או Node.js) שמגיעים לרוב מ-Docker Hub, מאגר מרכזי של Images ציבוריים. בסיום תהליך הבנייה, כל ה-Images ששימשו בשלבי הביניים לא נשמרים בתוך ה-Image הסופי, אלא רק הקבצים והתוצרים שנבחרו מתוך אותם שלבים. התוצאה: Image יחיד שניתן להריץ אותו כקונטיינר.
- Container: קונטיינר הוא מופע (runtime instance) של Image. הוא יחידת הרצה מבודדת, אך משתף את אותו Karnel של מערכת ההפעלה עם קונטיינרים אחרים.
- Dockerfile: קובץ טקסט המגדיר את הוראות בניית ה-Image.
- Volume: מנגנון לאחסון נתונים מחוץ ל‑Layer השכבתית של ה‑Image, כך שתוכן הקבצים נשמר גם אם הקונטיינר נמחק או נבנה מחדש. ה-Volume מתלכד עם מערכת הקבצים של המארח (Host) או נשמר בכתובת ייעודית של Docker, בהתאם לסוג ה‑Volume.
- Docker Compose: כלי לניהול קבוצה של קונטיינרים (שירותים) דרך קובץ YAML אחד, כולל רשתות, volumes וקונפיגורציות. כיום נהוג להשתמש אך ורק בו להגדרת Docker.
- Network: פשוטו כמשמעו. אמנם המושגים קצת שונים, אבל אתם ככל הנראה מכירים זאת היטב ממכונות וירטואליות: הגדרה של איזה סוג רשת כל Container והאם הם יכולים לתקשר בניהם (כי הם באותה הרשת) או שמגדירים לכל אחד מהם רשת נפרדת.
- Bind Mount: מנגנון חיבור ישיר בין תיקיה במערכת ההפעלה של המארח (Host) לבין תיקיה בתוך הקונטיינר. בשונה מ-Volume, כאן אתם קובעים במפורש איזה נתיב מתוך ה-Host יוצמד לקונטיינר, מה שמאפשר גמישות מרבית בזמן פיתוח. לדוגמה, אם יש לכם קוד בפרויקט מקומי, ניתן למפות אותו לתוך הקונטיינר כך שכל שינוי שתבצעו בקבצים ב-Host ישתקף מידית בתוך הסביבה (Live Reload).
לצורך הדוגמא, אקח כלי שבניתי ב-2020 במסגרת הפוסט שכתבתי על פריצה והגנה למערכות WordPress ואדגים לכם כיצד ניתן להתאים אותו להרצה בסביבת Docker.
בשלב הראשון ייבאנו Image של Python שכן הכלי נכתב ב-Python, לאחר מכן מוודאים ש-Python לא ייצור קובץ pyc (המרת הקוד ל-Bytecode כדי שהפעם הבאה תהיה מהירה יותר שמיותרת ב-Docker שכן בכל הרצה מחודשת הקונטיינר רץ על מופע חדש ומאופס) וכן מוודאים ש-Python תדפיס מיד פלטים למסך (PYTHONUNBUFFERED). לאחר מכן אנו קובעים את ספריית העבודה של הקונטיינר (WORKDIR /app), מעתיקים אליה את קובץ ה-requirements ומריצים אותו רקורסיבית (תוך מניעת שמירה בדיסק עם -no-cache-dir, שוב, בגלל האיפוס מחדש של ה-Container בכל הרצה מחודשת), לאחר מכן מעתיקים את שאר קבצי הפרויקט לתיקיית העבודה ומגדירים את פקודת ההרצה.
# Use a lightweight Python 3.11 image as the base
FROM python:3.11-slim
# Avoid writing .pyc files and force stdout/stderr to be unbuffered
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Set the working directory inside the container
WORKDIR /app
# Copy the dependency list and install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code
COPY . .
# Define the default command to run the scanner
ENTRYPOINT ["python", "wordpress_scan.py"]
כעת הפרויקט שלנו מותאם ולא משנה היכן נריץ את ה-Docker, הוא אמור לעבוד בלי בעיות של תאימות וגרסאות, כי בכל המחשבים הוא רץ תחת אותה תאימות ואותם גרסאות (תחת ה-Docker).
ע"מ לבנות את ה-Container נריץ:
docker build -t wordpress-security-scan .
ולאחר מכן נריץ אותו (ה-rm בשל כך שאני רוצה הרצה חד פעמית בשל המבנה של הכלי ולאחר מכן מחיקה של ה-Container). במקרה של כלים אחרים, אפשר לוותר על דגל זה:
docker run --rm wordpress-security-scan https://another-site.com
היתרון המרכזי של Docker הוא היכולת להציע בידוד בין קונטיינרים, דבר שמאפשר למפתחים להפעיל אפליקציות שונות בצורה נפרדת לחלוטין וגם משפר מאד את האבטחה (גם אם התוכנה פגיעה, הרבה יותר קשה לתוקף לגשת לקבצים במחשב המארח או בקונטיינרים האחרים). זה אפשרי בזכות יכולות הבידוד שמספק ה-Linux Kernel, כמו Namespaces ו-Cgroups (ה-Namespaces מאפשר למערכת ההפעלה להפריד בין תהליכי הקונטיינרים כאשר כל קונטיינר מקבל Namespace משלו וה-Cgroups מאפשר למערכת ההפעלה להגביל את השימוש במערכות החומרה, כגון CPU, זיכרון, ואחסון עבור כל קונטיינר).
בואו נפרט קצת יותר ה-Namespace והבידוד הנפרד:
| Namespace | מה הוא מבודד |
|---|---|
| PID | מזהי תהליכים: הקונטיינר רואה רק את התהליכים של עצמו |
| Mount | מערכת הקבצים: כולל קבצים, תיקיות ונקודות עגינה |
| Network | ממשקי רשת, כתובות IP, פורטים, טבלאות ניתוב |
| User | מזהי משתמשים וקבוצות (UID/GID) |
| IPC | זיכרון משותף, semaphores ותורים בין-תהליכיים |
| UTS | Hostname ו-Domain name של המערכת |
| Cgroup | הגבלת שימוש במשאבים: CPU, RAM, דיסק, רשת |
Kubernets
Kubernetes (או בקיצור K8s) הוא כלי קוד פתוח אשר פותח במקור ע"י Google כדי לפתור את האתגרים הגדולים שמגיעים כשעובדים עם הרבה קונטיינרים (כפי שקורה בחברות הגדולות), כמו ניהול עומסים, אוטומציה, ניטור, גיבוי, זמינות גבוהה, סקיילינג אוטומטי, טיפול בנפילות, הפצות ועוד. כיום הפרויקט פועל תחת Cloud Native Computing Foundation (CNCF).
שלושה מושגים קצרים שחשוב מאד להכיר בכל הנוגע ל-Kubernets:
- Pod: יחידת ההרצה הבסיסית ביותר ב-Kubernetes. כל Pod כולל קונטיינר אחד או יותר, שמשתפים ביניהם את אותה כתובת IP, את אותו Volume ואת אותו Namespace.
- Node: השרת אשר מריץ את הקונטיינרים בפועל (ה-Host למעשה). כל Node כולל את הרכיבים הדרושים כדי להריץ Pods, כמו Kubelet (שמנהל את התקשורת מול ה-Control Plane של ה-Cluster) ו-kube-proxy (שמנהל את התקשורת הרשתית מול השירותים האחרים ב-Cluster).
- Cluster: קבוצת Nodes שמנוהלים יחד כמערכת אחת. כל Cluster כולל את רכיבי הניהול המרכזיים שנקראים Control Plane, ואת ה-Nodes שמריצים את הקונטיינרים בפועל (כלומר, את יחידות ה-Pod). ה-Cluster דואג לתזמון של משימות, ניהול עומסים, הפצות, ניטור וכן הלאה. בהקשר שלCluster חשוב להכיר את המונח etcd, זהו מסד נתונים מבוזר מסוג key-value, המשמש כמרכז האחסון של כל המידע הקריטי על מצב ה-Cluster.
הדרך לבצע שימוש ב-Kubernetes בארגונים מתחלק: יש ארגונים אשר עושים שימוש ב-Managed Kubernetes, שירות מנוהל שמציעות ספקי הענן הגדולים בעולם (EKS, AKS, GKE) ויש אשר משתמשים בהתקנה עצמאית (בדרך כלל באמצעות כלים מסייעים).
Container Breakouts/Escape (Privilege Escalation)
אחת מהחולשות החמורות בסביבות מבוססות קונטיינרים היא "בריחה" מהמכולה, כלומר, ניצול פרצה המאפשרת לקוד זדוני לצאת מגבולות הקונטיינר המבודד ולקבל גישה למכונה המארחת. מאחר שכל המכולות חולקות את אותה ליבת מערכת עם ה-host, פגיעויות בליבה או ברכיבי הרצת הקונטיינר (container runtime) יכולות לאפשר מעבר של תוקף מתוך המכולה אל מערכת ההפעלה המארחת.
במציאות הנוכחית בה מרבית האתרים קיימים בסביבות ענן, בתוך Containers, הבעיה הזו הופכת להיות משמעותית ופופולרית יותר ובפסקאות הבאות נבין עד כמה. חשוב עוד לציין כי פגיעות זה רלוונטית בדרך כלל בעיקר כאשר ה-Container רץ תחת הרשאות root (אך גם אם לא, תוקף יוכל לנסות ולממש Privilege Escalation ע"מ להפוך ל-root).
פגיעויות Container Escape התגלו במספר הזדמנויות והדגימו את שבירות הבידוד. לדוגמה, פגיעות CVE-2019-5736 (חולשה ב-runC) אפשרה למכולה זדונית לדרוס את קובץ ההרצה runC (רכיב ליבה בתשתית ה-Docker) על ה-host וכך להשיג הרשאות root במכונה המארח.
runC הוא container runtime, כלומר, רכיב תוכנה שאחראי בפועל להריץ ולנהל קונטיינרים ברמה הנמוכה ביותר של מערכת ההפעלה (הוא רץ כחלק מה-Host). זהו כלי שורת פקודה קטן שנכתב בשפת Go ופותח תחילה כחלק מ-Docker, אך עם הזמן הפך לסטנדרט עצמאי בקוד פתוח. הקובץ הבינארי של ה-runC נמצא לרוב בנתיב usr/bin/runc/.
הבעיה נבעה מהאופן שבו runC טיפל בפקודות הרצות באמצעות exec (שימוש ב-docker exec מאפשר להריץ פקודות בתוך ה-Docker). ה-runC היה מבצע קריאה ל-/proc/self/exe (קובץ בינארי שמייצג את התהליך של עצמו, כלומר, התהליך של runC היה נכנס מתוך ה-Host לתוך ה-Namespace הפנימי של ה-Container). בשלב הזה בדיוק, תוקף היה פותח את אותו קובץ (שעד עכשיו לא היה נגיש בגלל שהוא היה ב-Host), משאיר אותו פתוח ועורך אותו עם קוד זדוני ומההרצה הבאה של exec, מכיוון שהקובץ שוב יהיה נגיש בתוך ה-Container והפעם יפעיל את הקוד הזדוני, התוקף היה יכול להשיג שליטה במערכת, תחת הרשאות root (שכן runC רץ כמובן תחת הרשאות root).
באופן דומה, בשנת 2022 נחשפה פגיעות חמורה נוספת תחת השם Dirty Pipe (רשומה תחת CVE-2022-0847), שוב המחישה כמה ה"בידוד" הוא אינו מוחלט, בשל כך שה-Containers אולי מבודדים מבחינת ה-Namespace, אך בסופו של דבר רצים על אותו קרנל. Dirty Pipe היא חולשה בליבת לינוקס (Linux Kernel) המאפשרת לכותב בלתי מורשה לשכתב תוכן של קבצים לקריאה בלבד (read-only), כל עוד מתקיימים תנאים מסוימים ובכך לעקוף את מנגנוני האבטחה של המערכת.
פגיעות נוספת (ויש עוד הרבה) שאציין על-מנת להדגיש את הנושא היא CVE-2019-14271. פגיעות זו זכתה ל-CVSS 8.6 והיא מתמקדת באופן שבו הפקודה docker cp (שמעתיקה קבצים מתוך או אל תוך קונטיינר) מטפלת בנתיבי קבצים ובקובצי בינאריים עם יכולת להריץ קוד. כאשר משתמש (לרוב בעל הרשאות root) מריץ את הפקודה docker cp, ה-Docker משתמש בכלי בשם archive/tar כדי לחלץ את הקבצים מתוך הקונטיינר. כלי זה מעביר את הקבצים ל-Host ושם, כחלק מתהליך ההעתקה, מתבצע חילוץ הקבצים מתוך הארכיון, לרוב תוך כדי שינוי הרשאות, בעלות (UID/GID) והרצת סקריפטים פנימיים כחלק מהפקודות ש-Docker מפעיל ברקע.
החולשה העיקרית טמונה בעובדה שתוקף יכול להחדיר לתוך הקונטיינר קובץ בינארי זדוני, כזה שיבצע פעולות זדוניות בזמן ביצוע פעולות מערכת שגרתיות כמו chown, chmod או stat, פעולות אשר יכולות להתבצע בעת תהליך ההעתקה באמצעות docker cp ובכך התוקף יכול לשבור את שכבת הבידוד, לצאת מגבולות הקונטיינר ולבצע פעולות על ה-Host הראשי.
הבעיה היא לא רק בחולשות חדשות וישנות ששוברות את שכבת הבידוד, אלא גם בקונפיגורציה לא נכונה של ה-Docker שמסייעת בשבירת אותו בידוד ועל-כך עוד אפרט בחלק הקונפיגורציה. עם-זאת, בחלק זה כן אדגיש את הבעייתיות בהרצת Containers תחת הרשאות root, שכן הדבר מקל מאד על תוקפים את שבירת הבידוד וביצוע פעולות ב-Host, בטח במקרים בהם הוגדר bind mounts לתיקיות וקבצים במחשב ה-Host.
לצורך הדוגמא, כברירת מחדל, Docker לא מפעיל את ה-User Namespace (בשונה מה-Namespaces האחרים עליהם פירטתי שכן מופעלים כברירת מחדל). זה אומר שבעת הרצת קונטיינר בלי הגדרות מיוחדות, ה-UID וה-GID של התהליכים בתוך הקונטיינר לא ממופים ל-UID/GID אחרים על המארח, כלומר, UID 0 בתוך הקונטיינר נשאר UID 0 על המארח. כתוצאה מכך, כל פעולה שמבוצעת כ-root בתוך הקונטיינר מתבצעת בפועל מול ה-kernel של המארח עם אותן הרשאות וזה מקל מאוד על תוקף שמצליח להריץ קוד בקונטיינר לבצע פעולות מסוכנות על ה-host. ע"מ למנוע זאת ועדיין להישאר root, יש לבצע הגדרה בקובץ etc/docker/daemon.json של "userns-remap": "default".
ע"מ להריץ את ה-Docker כמשתמש unprivileged, נשתמש בu- בעת ביצוע docker run ונזין את שם המשתמש/ה-User ID. לחלופין, נגדיר ב-Dockerfile תחת הוראת USER את שם המשתמש.
מקרה קיצוני יותר (אפרופו פסקה ה-Misconfiguration שמיד אתמקד בה) יכול להיות כאשר ה-var/run/docker.sock/ (ה-IPC של תהליך ה-Docker), מופה בטעות לתוך הקונטיינר. מצב זה למעשה חושף את ה-socket של ה-daemon, התהליך המרכזי שמנהל את כל פעולות ה-Docker על גבי המארח. מאחר וה-daemon רץ בדרך כלל עם הרשאות root, כל תהליך בתוך הקונטיינר שמקבל גישה ל-var/run/docker.sock/ יכול לתקשר ישירות עם ה-daemon ולבצע דרכו פקודות מערכת כאילו היה root על ה-Host עצמו.
Misconfigurations
אחת הבעיות הנפוצות ביותר בעבודה עם Containers הוא פערי קונפיגורציה מבחינה אבטחתית (מתחבר היטב עם ה-CNAS-1 ב-OWASP TOP 10 שעליו אדבר בחלק ב').
אמנם Docker מאזין כברירת מחדל ל-Unix Socket מקומי (/var/run/docker.sock) וממילא ניתן לגשת אליו רק מקומית, אך יחד עם-זאת במקרה שבוצע שינוי (לא נדיר), כך שיאזין לפורט TCP (לדוגמא עבור ניהול מרחוק) ולא בוצעה הגנה של mTLS על פורט זה, תוקף יוכל בקלות לאתר את ה-Docker (לדוגמא באמצעות Shodan), לגשת אליו ולבצע בו פעולות (הפורטים 2375/2376 הם ה-Default Port של Docker, אך 2376 הוא הפורט כאשר מתבצע שימוש ב-mTLS).
וזה לא רק ה-Docker עצמו. ב2018 חוקרים של RedLock (לימים Palo Alto Networks) אתרו ממשק Kubernetes Dashboard פתוח באינטרנט, שכנראה היה שייך ל-Tesla (בתוך סביבת ה-AWS של החברה). הממשק היה פתוח ללא אף הגנה, כאשר בתוך אחד ה-Pods (אם אתם לא זוכרים מהו Pod, זה הזמן לחזור למבוא) נמצאו קבצים שכללו פרטי גישה לחשבון ה-AWS של Tesla ומידע נוסף. תוקפים עשו שימוש בפרטי גישה אלו ע"מ התחבר לשרתי EC2 ב-AWS ולבצע כרייה של מטבעות קריפטוגרפים.
בנוסף, כברירת מחדל, Docker מגיע עם סט רחב של יכולות שהוא יכול לרוץ עמם, גם אם חלק מהיכולות לא נדרשות להרצה הספציפית של ה-Container. ההמלצה היא תמיד להסיר כשלב ראשון את כל היכולות ואז לבחור ספציפית את היכולות שאנו צריכים עבור ה-Container הספציפי שאנו מריצים. לדוגמא:
docker run --cap-drop all --cap-add CHOWN alpine
כמובן שבשום מצב לא נעשה שימוש בדגל privileged– שנותן הרשאות מלאות וייפגע בבידוד של ה-Container.
עוד רכיבים שחשוב לשים לב אליהם בהקשר של הקונפיגורציה הם הגדרות ה-etcd וה-Kubelet (דיברנו עליהם במבוא). לדוגמא, עד לגרסה Kubernetes v1.10 (ההקשחה המוחלטת רק ב-v1.20), ה-Kubelet היה מאזין כברירת מחדל לשני פורטים: 10250 (פורט מאובטח (HTTPS) ניהול ואינטראקציה עם ה-Kubelet) ו-10255 (פורט Read-Only ללא אימות בכלל, שהחזיר מידע JSON בפורמט API עבור כל ה-Pods וה-Nodes). באופן דומה גם רכיב ה-etcd היה יכול להיות פתוח בפורט 2379 ללא אימות או הצפנה (כיום כבר פחות נפוץ שכן הן סביבות מנוהלות והן סביבות התקנה עצמאית מקשיחות אותו כברירת מחדל). לדוגמא, מחקר של RAPID7 מדצמבר 2020 איתר 2,560 שירותי etcd שהיו פתוחים באינטרנט ללא אימות או הצפנה.
בנוסף, חשוב לבצע הגדרה של Pod Security Standards אשר תמנע יצירת של Pods בתצורות בעיתיות מבחינה אבטחתית, כגון הרצה כ-root, הרשאות privileged, או גישה ישירה למשאבי מערכת של ה-Host. כברירת מחדל, כל מפתח שיכול ליצור Pod יכול להגדיר privileged: true אשר מעניק לקונטיינר גישה ישירה לפונקציות מערכת ברמת הקרנל או hostNetwork: true ו-hostPID: true אשר מאפשרים לקונטיינר גישה לממשקי הרשת של ה-Node כולו. הגדרות נוספות אפשריות ללא הגדרת ה-PSS הם hostPath שמאפשר למפות תיקיות מה-Host ישירות לתוך הקונטיינר או הרצה כ-root עם runAsUser: 0 או runAsNonRoot: false. ה-Pod Security Standards באה בדיוק למנוע את זה, היא מאפשרת לקבוע מדיניות ברורה כגון שלא תתאפשר הגדרה של privileged, שרק סוגי Volume מסוימים יהיו מותרים וכך גם אם המפתח ירצה להגדיר הגדרות שלא מומלצות מבחינה אבטחתית, אך לא יוכל לעשות זאת.
הגדרה נוספת שחשוב לוודא היא הצפנת קבצי ה-Secrets. ה-Secrets זהו אובייקט מובנה שמאפשר לאחסן מידע רגיש כמו סיסמאות, מפתחות API, תעודות (TLS/SSH) ו‑tokens מחוץ לקוד (בדומה למשתני סביבה), בתוך ה-etcd. כברירת מחדל, המידע נשמר מקודד ב-Base64 ולא מוצפן, כך שבמקרה ותוקף מצליח לגשת לקובץ, הוא יכול לבצע Decode למידע ולקרוא אותו בקלות. ע"מ למנוע זאת, חשוב לבצע הצפנה לקובץ באמצעות מימוש Encryption at rest.
Network Attacks
על חלק זה לא אפרט בצורה משמעותית, שכן בסופו של דבר הוא זהה לכללים וליסודות של ביצוע infrastructure PT. מה שחשוב להבין זה: בתוך אותו Pod, ה-Containers למעשה חולקים את אותה כתובת IP של ה-Pod ויכולים לתקשר אחד עם השני וכברירת מחדל, כל ה-Pods באותו Cluster נמצאים באותו subnet של כתובות IP.
פגיעות מעניינת שאותרה לאחרונה בהקשר של Network Attacks היא פגיעות SSRF שאתרה ב-Docker Desktop ב-Windows ו-macOS. בפגיעות זו התברר שה-Docker Desktop חשף ללא כל אימות את ה-Docker Engine (המנוע שמנהל בפועל את ה-Containers ואמור להיות נגיש רק ל-Host) לכל ה-Docker Subnet, בכתובת 192.168.65.7:2375, מה שאפשר למעשה ל-Container זדוני להיות עם שליטה מלאה על ביצוע פעולות ב-Containers השונים, יצירת Containers חדשים, הפסקת Containers וכו' ובכך למעשה שבירת כל ייתרונות הבידוד של Docker. הפגיעות קיבלה את ה-CVE-2025-9074 ותוקנה בגרסת 4.44.3.
Supply chain
אחד הסיכונים המשמעותיים בעבודה עם Docker היא כמובן הבעיה הכי פחות פתורה בעולמות הסייבר, שרשרת האספקה כמובן. Container אחד יכלול לרוב מספר Images (או בניסוח מדויק יותר: ה-Image אחד שממנו בנוי ה-Container יכול להיות מורכב מכמה שכבות שנבנו מ-images אחרים שקבציהם הורדו) ובעולם שבו שיתוף image באמצעות Docker Hub הוא פשוט וקל, תוקפים מבצעים העלאה של images זדוניים עם שמות דומים לשירותים לגיטימיים.
לצורך ההמחשה, דו"ח של Aqua Security מ-2021 חשף 5 Images זדוניים שהורדו יותר מ-130,000 פעמים מ-Docker Hub וביצעו כרייה של מטבעות Monero. הם השתמשו בשמות כמו openjdk ו-golang ע"מ להטעות שכביכול מדובר ב-images רשמיים מהימנים של OpenJDK ו-Golang. דוגמא למחקר נוסף שבוצע בהקשר הזה ובסדר גודל גדול-יותר תוכלו לקרוא כאן.
גם במקרה שה-Container שהורדתם הוא ממקור לגיטימי, כל עוד לא עברתם על הקוד שלו לעומק, הוא יכול להיות כר פורה לחולשות ושער הכניסה ל-Container, הרי אם ה-docker מכיל גרסאות שהתגלו בהם חולשות ולא עדכנתם את תמונת ה-Docker, אתם בבעיה. בהקשר זה, חשוב גם להבין שבחירה אוטומטית של הגרסה האחרונה כגון "FROM python:latest" עלולה להוות סיכון כשאנו מדברים על תהליכי CI/CD אוטומטיים, שכן ייתכן שהגרסה שנבדקה והתגלתה כתקינה היא גרסת ה-latest שהייתה בזמן היצירה הראשוני וכיום גרסת ה-latest מכילה חולשות כאלה ואחרות. בהתאם לכך, יש לבחור גרסה ספציפית ומעת לעת לעדכן מס' גרסה ספציפי באופן ידני.
מאמר שפורסם באפריל 2025 ע"י שורת חוקרים מאוניברסיטת שאנגחאי הציג מתודלוגיית ניתוח שבנו החוקרים כלפי Images ב-Docker Hub. במשך כשישה חודשים החוקרים ניתחו את המטא-דאטה והתלויות של Images ב-Docker Hub, סיננו את ה-Images המשמעותיים ביותר (כ-34 אלף Images) והריצו עליהם את הכלי שהם בנו. הכלי זיהה דליפת סודות ב-4,437 Images (הזיהוי בוצע באמצעות TruffleHog), כ-50 Images עם בעיות מיסקונפיגורציה, 24 Images זדוניים ו334 Images שהושפעו מה-Images הזדוניים.
עוד דוגמא לכך ניתן לראות במחקר של חברת NetRise שסרק 70 תמונות קונטיינר פופולריים והציג כי בכל Image בממוצע קיימים 389 רכיבי תוכנה (ספריות, רכיבי מערכת וכו') ו-604 פגיעויות ידועות (בשקלול כל הרכיבים ותתי הרכיבים, גם הלא פעילים). עם המספרים אפשר להתווכח, אך הרעיון ברור. מחקר נוסף של snyk מציג תוצאות של ניתוח אוטומטי שבוצע ע"י הפלטפורמה של החברה על עשר מה-Docker Images הפופולריים ביותר וזיהה כ-30 חולשות בממוצע בכל אחד מה-images.
במאמר זה השתדלתי לכסות כמבוא את סיכוני האבטחה המרכזיים ב-Docker ולגעת קצת בקונפיגורציה והגדרות נכונות. אבל כמובן שנגעתי רק בטיפה מהים ויש עוד הגדרות רבות ונושאים רבים שחשוב להכיר ואותם תוכלו ללמוד לעומק ב-Cheat Sheet המעולה של OWASP.
לסיום, אציג כלי אחד (מתוך לא מעט שקיימים) שנועד לסייע בסריקה אוטומטית של חולשות ב-Docker.
הכלי אותו אציג הוא Trivy. מדובר בסורק קוד פתוח פופולרי מבית Aqua Security אשר מבצע סריקה אוטומטית של Images, סורק את קוד המקור ואת קבצי הקונפיגורציה ומזהה פגיעויות (CVEs), סודות מודלפים, וקונפיגורציות לא מאובטחות.
ע"מ להריץ אותו, נוריד אותו ישירות בלינוקס עם apt (שימוש ב-snap יעבוד טוב יותר) או באמצעות הורדת ה-Docker Image שלו.
לאחר מכן נריץ אותו על ה-Image אותו אנו רוצים לסרוק:

ונקבל טבלה מסודרת עם רשימת הפגיעויות (נוכל גם לייצא את הנתונים בפורמט JSON כפי שעשיתי בפקודה המקורית):
