SSD Nodes Learn
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-07-24

Docker עוקף את UFW איך לתקן את זה

Docker משנה את חוקי iptables ומתעלל ב-UFW. למדו מדוע פורטים חסומים נשארים פתוחים לאינטרנט וכיצד לפתור זאת באמצעות הגדרת Docker host או שינוי chains.

Why Docker bypasses UFW

Docker עוקף את UFW מכיוון שפורטים שפורסמו בתוך קונטיינרים אינם עוברים דרך חוקי חומת האש שמנהל UFW. כאשר מריצים את docker run -p 8080:80, Docker כותב כלל DNAT (destination network address translation) בתוך ה-PREROUTING chain של טבלת ה-nat של ה-kernel. אותו כלל משנה את יעד כל חבילה (packet) לכתובת הפרטית של הקונטיינר לפני שה-kernel מחליט לאן החבילה אמורה להגיע. לאחר מכן, החבילה המשוכנה מועברת אל הקונטיינר דרך ה-FORWARD chain, שנמצא תחת השליטה של Docker. החוקים של UFW נמצאים ב-INPUT chain, והחבילה לעולם אינה נכנסת אליהם. לכן ufw status מציג חסימה כברירת מחדל, sudo ufw deny 8080 מדווח על הצלחה, ופורט 8080 עדיין מגיב לכל האינטרנט.

זו אינה תקלה ב-Docker, ו-UFW אינו שבור. שני הכלים מתכנתים את אותו חומת אש של ה-kernel. החוקים של Docker פועלים פשוט בנקודה מוקדמת יותר במסלול החבילה, ולכן לא מתבקש UFW לפעול. מדריך זה מדגים את העקיפה, מסביר את המנגנון, ומציג את שתי הדרכים לתיקון: פרסום פורטים ב-127.0.0.1, וסינון ב-DOCKER-USER chain. אם UFW אינו מוכר לך, הגדר אותו באמצעות the UFW firewall basics guide תחילה, מכיוון שחומת אש במצב default-deny היא עדיין הבסיס הנכון לכל שאר השירותים בשרת.

בדקו את המעקף בשרת שלכם

התחילו מ-VPS שבו UFW פעיל עם מדיניות deny ברירת מחדל לתעבורה נכנסת. הריצו web container עם port שפורסם:

sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpine

ufw status verbose מציג את Default: deny (incoming), allow (outgoing) וללא rule עבור port 8080. לפי הדיווח של ה-firewall, ה-port סגור. כעת בדקו ממכונה אחרת, לא מהשרת עצמו:

curl -I http://your-vps-ip:8080/
HTTP/1.1 200 OK

ה-container מגיב. הוסיפו explicit deny rule ובדקו שוב:

sudo ufw deny 8080/tcp

ה-port עדיין מגיב, מכיוון שה-deny rule נמצא ב-chain שה-packet לעולם לא מבקר בה. UFW לא נכשל. הוא מעולם לא נבדק. זו גם הסיבה שהבעיה נסתרת כל כך טוב: לא מודפסת שגיאה בשום מקום, ה-deploy עובד, ופלט ה-firewall status נראה בדיוק כמו שרת חסום ותקין.

המנגנון: PREROUTING רץ לפני INPUT

הליבה (kernel) מעבדת חבילה נכנסת בסדר קבוע, וכל הבעיה נובעת מהסדר הזה.

  1. PREROUTING רצה ראשונה. כללים כאן עשויים לשנות את יעד החבילה, והכלל של Docker עבור פורט שפורסם (published port) עושה בדיוק זאת.
  2. לאחר מכן מתקבלת החלטת הניתוב (routing decision). חבילה המיועדת למארח (host) עצמו עוברת לשרשרת INPUT. חבילה המיועדת למכונה אחרת עוברת לשרשרת FORWARD.
  3. הכללים של UFW נמצאים ב-INPUT. הכללים של Docker נמצאים ב-FORWARD.

הסתכלו על הכלל של Docker עבור הקונטיינר שהרצתם זה עתה:

sudo iptables -t nat -L DOCKER -n
Chain DOCKER (2 references)
target   prot opt source       destination
RETURN   0    --  0.0.0.0/0    0.0.0.0/0
DNAT     6    --  0.0.0.0/0    0.0.0.0/0    tcp dpt:8080 to:172.17.0.2:80

שורת ה-DNAT היא כל התמונה. כל חבילה המגיעה לפורט 8080 משנה את יעדها ל-172.17.0.2:80, הכתובת של הקונטיינר ברשת ה-bridge הפרטית של Docker. לאחר השינוי, החבילה כבר אינה מיועדת למארח, ולכן החלטת הניתוב שולחת אותה לנתיב FORWARD, שם Docker כבר הוסיף כללים המאשרים תעבורה לרשתות שלו. הכלל שלכם ב-deny 8080/tcp ממתין ב-INPUT לחבילה שבכלל לא תגיע.

ב-Ubuntu 24.04 הפקודה iptables היא ממשק (front end) עבור nftables, אך סדר השרשראות והתוצאה זהים. UFW ו-Docker כותבים לשירות ה-packet pipeline של הליבה, ונקודת הכניסה של Docker היא מוקדמת יותר.

הפתרון היומיומי: פרסום פורטים ב-127.0.0.1

רוב הקונטיינרים לא אמורים להיות חשופים לעולם בכלל. מסד נתונים, שרת אפליקציה מאחורי reverse proxy, לוח בקרה (admin panel), או נקודת קצה למדדים (metrics endpoint): אף אחד מאלה לא אמור להגיב ישירות לאינטרנט. פרסמו אותם בכתובת ה-loopback:

docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpine

או בקובץ Compose:

services:
  web:
    image: nginx:1.29-alpine
    ports:
      - "127.0.0.1:8080:80"

זה עובד מכיוון שכלל ה-DNAT של Docker תואם כעת רק חבילות המופנות ל-127.0.0.1. לחבילה מהאינטרנט לא יכול להיות יעד כזה באופן לגיטימי, ולכן ה-kernel משמיט אותה לפני הרצת כל כלל firewall. הפורט נגיש מה-host בלבד. בדקו את הקישור (binding):

sudo ss -tlnp | grep 8080

אתם אמורים לראות 127.0.0.1:8080 בפלט, ולא את 0.0.0.0:8080 או [::]:8080. לאחר מכן, ודאו ממכונה אחרת ש-curl http://your-vps-ip:8080/ נחסם (refused).

עבור שירותים שאמורים להיות חשופים לאינטרנט, הריצו reverse proxy אחד המחזיק את פורטים 80 ו-443 ומנתב לפי hostname, ופרסמו שום דבר אחר. זהו הדפוס שבו בנו את ה מדריך ל-Traefik reverse proxy, וזו הדרך שבה אפליקציה מארחת עצמית כמו Nextcloud on a VPS נשארת בלתי נגישה אלא דרך ה-proxy שלה. אופן הצהרת רשומות ה-ports: ושאר תהליך העבודה ב-Compose מוסברים ב מדריך הבסיס ל-Docker Compose.

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

ToolUFW rule generator

סינון אמיתי: ה-DOCKER-USER chain

לפעמים יש צורך להשאיר פורט של container פתוח לרשת אך מוגבל. לדוגמה, פורט של database replica שכתובת משרד אחת בלבד יכולה לגשת אליו. לצורך כך, Docker מספקת את ה-DOCKER-USER chain. כל packet הפונה לכל container עובר דרך DOCKER-USER לפני חוקי ה-accept של Docker, ו-Docker לעולם אינה כותבת חוקים בתוך ה-chain הזה. ה-chain נועד עבורכם, ו-Docker לא משנה את התוכן שלו בזמן הפעלה מחדש של ה-daemon.

מלכוד אחד לפני הפקודה: עד שה-packet מגיע ל-DOCKER-USER, ה-DNAT rewrite כבר התבצע. פורט היעד של ה-packet הוא פורט ה-container (80 בדוגמה שלנו), ולא הפורט המפורסם (8080). לכן, חוק שמתאים ל---dport 8080 לא יתאים לשום דבר. הדרך האמינה היא להתאים את הפורט שהלקוח התחבר אליו במקור, כפי ש-connection tracker של ה-kernel זוכר:

sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP

קראו זאת כך: עבור packets שנכנסו דרך eth0 ושייכים ל-connection שהפורט המקורי שלו היה 8080, יש להפיל (drop) כל דבר שלא נשלח מ-10.0.0.10. ההתאמה ל---ctdir ORIGINAL מגבילה את החוק לכיוון של client-to-container, כך שחבילות תשובה (reply packets) לא ייחסמו בטעות. החליפו את eth0 בממשק הציבורי שלכם; ip route | grep default מציין אותו. בדקו זאת באותה דרך כמו קודם: curl מכתובת מורשית יצליח, ומכל מקום אחר תתרחש timeout בחיבור.

חוקים שנוספו באמצעות הפקודה iptables יימחקו לאחר reboot. מכיוון ש-UFW כבר מנהלת את ה-firewall הזה, המקום הנכון לשמור אותם הוא ב-/etc/ufw/after.rules. הוסיפו בלוק בסוף הקובץ:

*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP
COMMIT

לאחר מכן הריצו את sudo ufw reload. UFW מפעילה מחדש את הקובץ הזה בכל reload ובכל boot, כך שסינון ה-container שלכם נמצא כעת באותו מקום כמו שאר ה-firewall, והוא נשמר גם לאחר reboot וגם לאחר שדרוג של Docker.

מדוע אין לבטל את אינטגרציית ה-iptables של Docker

תשובות ישנות לבעיה זו מציעות להגדיר את { "iptables": false } בתוך /etc/docker/daemon.json. אל תעשו זאת. כללי חומת האש של Docker עושים הרבה יותר מאשר רק פרסום פורטים (ports). כלל ה-masquerade הוא שאפשר למכולות (containers) לקבל גישה לאינטרנט החיצוני דרך כתובת המארח (host), לכן כאשר האינטגרציה כבויה, המכולות לא יוכלו למשוך images, להגיע למראי חבילות (package mirrors), או לפנות לכל API חיצוני. כללי ה-DNAT הם אלו שמאפשרים ל--p לעבוד, ולכן פורטים שפורסמו יפסיקו לעבוד לחלוטין. גם כללי הבידוד (isolation rules) המפרידים בין רשתות Compose שונות יבוטלו. פתרון ה-bypass יגרום לשבירת רשת המכולות, וכל אחד מהכללים הללו יהיה עליכם לכתוב ולתחזק ידנית. התיעוד הרשמי של Docker מתאר את ההגדרה הזו כפתרון עבור אנשים שמתכוונים לעשות בדיוק את זה. השרשרת DOCKER-USER קיימת בדיוק כדי שאף אחד לא יזדקק למתג הזה.

ההיבט של IPv6 באותו הבעיה

ראשית, בדקו כיצד נראה הפורט המפורסם (published port) ב-IPv6:

sudo ss -tlnp | grep 8080

החל מ-Docker Engine 27, Docker מנהל את ip6tables כברירת מחדל. ברשת Docker עם IPv6 מופעל, פורט מפורסם מקבל את אותה טיפול DNAT בטבלאות ה-IPv6. לכן, אותה דרך מעקף קיימת גם שם ואותה תיקון תקף: ה-chain ‏DOCKER-USER קיים גם ב-ip6tables. יש לשקף את הכלל שלכם באמצעות sudo ip6tables -I DOCKER-USER ... ולבדוק מבחוץ באמצעות curl מול כתובת ה-IPv6 הציבורית של השרת, לדוגמה curl -6 http://[2001:db8:2a::1]:8080/.

ברשת ללא IPv6, לקוחות IPv6 מטופלים על ידי docker-proxy, תהליך user-space רגיל המאזין ב-[::]:8080 ומעביר את התעבורה אל הקונטיינר באמצעות IPv4. תעבורה לתהליך host עוברת דרך INPUT, לכן UFW יכול לסנן נתיב זה, אך זאת רק כאשר UFW מנהל IPv6 בכלל. האם הוא מנהל, ודרכים אחרות שבהן נוצר פער IPv6 ב-VPS, הם נושא המדריך ל-UFW ו-IPv6.

פרסום (Publishing) על loopback עוקף את כל השאלה: -p 127.0.0.1:8080:80 קושר (binds) רק את ה-IPv4 loopback, לכן אין מאזין IPv6 ואין דבר שניתן להגיע אליו מבחוץ באף אחד מהמצבים (stacks).

התבנית המגנה

  • פרסם כל פורט פנימי ב-127.0.0.1, כדי שלא יהיה חשוף מלכתחילה.
  • הקצה את הצד הציבורי ל-reverse proxy אחד המחזיק את פורטים 80 ו-443.
  • הגדר ב-UFW deny כברירת מחדל עבור ה-host, תוך מתן הרשאה ל-SSH ולפורטים של ה-proxy.
  • סנן פורטים ציבוריים אמיתיים של ה-container ב-DOCKER-USER, על בסיס פורט היעד המקורי, שנשמר ב-/etc/ufw/after.rules.
  • השאר את ה-iptables integration של Docker פעיל.

הגדרה חד-פעמית זו מונעת הפתעות: ufw status מתאר את ה-host, ו-DOCKER-USER מתאר את ה-containers. שום דבר לא יפורסם בטעות, ובפעם ה-docker run -p הבאה שתקליד, תחשוף בדיוק את מה שהתכוונת לחשוף.

FAQ

מדוע אני מצליח לגשת ל-Docker container למרות ש-UFW חוסם את ה-port?

מכיוון ש-Docker מפרסם את ה-port באמצעות rule מסוג DNAT ב-chain של PREROUTING. rule זה משנה את יעד ה-packet לכתובת של ה-container לפני כל פעולת סינון. ה-packet עובר בנתיב FORWARD, בעוד ש-rules של UFW נמצאים ב-INPUT, chain שה-packet לעולם לא נכנס אליה. ה-firewall אינו נבדק, ולכן ל-deny rules שלו אין השפעה על ports מפורסמים של containers.

איך אני גורם ל-UFW לחסום את ה-ports המפורסמים של Docker?

UFW עצמו אינו יכול לעשות זאת, כיוון שה-rules שלו נמצאים ב-chain הלא נכון. ניתן להפסיק את החשיפה של ה-port על ידי פרסום שלו כ-127.0.0.1:8080:80 כך שרק ה-host יוכל לגשת אליו, או לסנן ב-chain של DOCKER-USER באמצעות iptables rule התואם ל-original destination port דרך conntrack. יש לשמור את ה-rule ב-/etc/ufw/after.rules כדי שישרוד לאחר reboots ו-ufw reload.

האם כדאי להגדיר "iptables": false ב-Docker's daemon.json?

לא. הגדרה זו מסירה את כל ה-firewall ו-NAT rules של Docker, מה שגורם לנזק רב יותר מאשר מעקף. ה-containers מאבדים גישה לאינטרנט ב-outbound מכיוון ש-masquerade rule נעלם, וה-published ports מפסיקים לעבוד מכיוון ש-DNAT rules נעלמו. השתמש ב-loopback publishing וב-chain של DOCKER-USER במקום; אלו פותרים את החשיפה מבלי לשבור את ה-container networking.

האם Docker עוקף את UFW גם ב-IPv6?

בגרסאות Docker Engine 27 ומעלה, ניהול ip6tables פעיל כברירת מחדל. לכן, port שפורסם ברשת Docker התומכת ב-IPv6 משונה בדרך עוקפת את UFW בדיוק כמו ב-IPv4, ודורש את אותו rule של DOCKER-USER המשוכפל עם ip6tables. ברשתות ללא IPv6, תהליך docker-proxy מאזין ב-[::] ותעבורה זו עוברת דרך INPUT, שם UFW יכול לסנן אותה אם הוא מנהל IPv6. פרסום ב-127.0.0.1 מונע את שני המקרים, כיוון ששום דבר לא מאזין ב-IPv6 בכלל.